【问题标题】:How to design a database with Revision History?如何设计带有修订历史的数据库?
【发布时间】:2011-11-19 21:10:55
【问题描述】:

我是一个团队的一员,该团队正在为我们的公共网站构建一个新的内容管理系统。我正在尝试找到内置修订控制机制的最简单和最好的方法。对象模型非常基础。我们有一个抽象的BaseArticle 类,其中包括版本无关/元数据的属性,例如HeadingCreatedBy。许多类都继承自此,例如DocumentArticle,它具有属性URL,它将是文件的路径。 WebArticle 也继承自 BaseArticle 并包括 FurtherInfo 属性和 Tabs 对象的集合,其中包括将保存要显示的 HTML 的 Body(Tab 对象不派生自任何东西)。 NewsArticleJobArticle 继承自 WebArticle。我们还有其他派生类,但这些提供了足够的示例。

我们为修订控制提出了两种持久性方法。我称它们为 Approach1Approach2。我用SQL Server做了一个基本图:

  • 使用 Approach1,计划是通过数据库更新 保存Articles 的新版本。将为更新设置一个触发器,并将旧数据插入到xxx_Versions 表中。我认为需要在每个表上配置触发器。这种方法确实有一个优点,即每篇文章的唯一head 版本保存在主表中,旧版本被分离出来。这使得将文章的头部版本从开发/暂存数据库复制到 Live 数据库变得很容易。
  • 使用 Approach2,计划将Articles 的新版本插入到数据库中。文章的头部版本将通过视图来识别。这似乎具有更少的表和更少的代码(例如,不是触发器)的优势。

请注意,对于这两种方法,计划都是为映射到相关对象的表调用 Upsert 存储过程(我们必须记住处理添加新文章的情况)。这个 upsert 存储过程将为它派生的类调用它,例如upsert_NewsArticle 会调用 upsert_WebArticle 等等。

我们使用的是 SQL Server 2005,尽管我认为这个问题与数据库风格无关。我已经对互联网进行了一些广泛的拖网搜索,并找到了对这两种方法的参考。但是我还没有发现任何东西可以将两者进行比较并显示其中一个更好。我认为在世界上所有的数据库书籍中,这种方法的选择一定是以前出现的。

我的问题是:这些方法中哪一种最好?为什么?

【问题讨论】:

  • 您是否考虑过购买 CMS 并对其进行定制?构建它们看似困难且耗时。它最终可能会非常昂贵。
  • 当您从愿景转向实施时,它肯定会变得相当复杂。但我认为我们有能力构建我们需要的东西......我只是想确保我们尽可能地做好后端。此外,如果我们选择了一个现成的解决方案,我仍然会留下一个理论问题,即采用哪种方法:-(顺便说一句,请参阅我在 Blender 的帖子中制作的 cmets 以获得有趣页面的链接。跨度>
  • 同意 Rex M,这不是你应该重新发明的东西。有无数的边缘情况。

标签: sql database database-design version-control content-management-system


【解决方案1】:

一般来说,历史/审计边表的最大优势是性能:

  • 查询的任何活动/活动数据都可以从小得多的主表中查询

  • 任何“仅实时”查询都不需要包含活动/最新标志(或者上帝禁止对时间戳执行相关子查询以找出最新行),从而简化了开发人员和数据库引擎优化器的代码。

但是,对于具有 100 或 1000 行(而不是数百万行)的小型 CMS,性能提升将非常小。

因此,对于小型 CMS,方法 3 会更好,因为设计更简单/代码更少/移动部分更少。

方法 3 几乎与方法 2 类似,除了每个需要历史记录/版本控制的表都有一个明确的列,其中包含一个真/假“活动”(又名实时 - 又名最新) - 标志列。

您的 upsert 负责在插入行的新实时版本(或删除当前实时版本)时正确管理该列。

通过在任何查询中添加“AND mytable.live = 1”,您在 UPSERT 之外的所有“实时”选择查询都可以轻松修改。

此外,希望显而易见,但任何表上的任何索引都应以“活动”列开头,除非另有保证。

这种方法结合了方法 2 的简单性(没有额外的表/触发器)和方法 1 的性能(无需对任何表执行相关子查询来查找最新/当前行 - 您的 upsert 通过活动标志管理)

【讨论】:

    【解决方案2】:

    我的实现可能有点复杂。

    首先,您只有一个表来处理所有事情,以便仅在一个点上保持模型设计和数据完整性。

    这是基本思想,如果需要,您可以使用created_byupdated_by 列扩展设计。

    在 MySQL 上实现

    下面的实现是针对MySQL的,但是这个想法也可以在其他类型的SQL数据库上实现。

    表格

    DROP TABLE IF EXISTS `myTable`;
    
    CREATE TABLE `myTable` (
      `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
      `version` int(11) NOT NULL DEFAULT 0 COMMENT 'Version',
      `title` varchar(32) NOT NULL COMMENT 'Title',
      `description` varchar(1024) DEFAULT NULL COMMENT 'Description',
      `deleted_at` datetime DEFAULT NULL COMMENT 'Record deleted at',
      `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Record created at'
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    ALTER TABLE `myTable`
      ADD PRIMARY KEY (`id`, `version`) USING BTREE,
      ADD KEY `i_title` (`title`);
    
    • 记录 IDidversion 定义。
    • 使用deleted_at,此模型支持软删除功能。

    观看次数

    获取当前版本

    获取当前记录版本:

    CREATE OR REPLACE VIEW vMyTableCurrentVersion AS
    SELECT
        `id`
      , MAX(`version`) AS `version`
      , MIN(`created_at`) AS `created_at`
    FROM `myTable`
    GROUP BY `id`;
    

    获取所有记录(包括已删除的记录)

    获取所有记录,包括软删除记录:

    CREATE OR REPLACE VIEW vMyTableAll AS
    SELECT
        T.id
      , T.version
    
      , T.title
      , T.description
    
      , T.deleted_at
      , _T.created_at
      , T.created_at AS `updated_at`
    FROM
      `myTable` AS T
      INNER JOIN vMyTableCurrentVersion AS _T ON
        T.id = _T.id
        AND T.version = _T.version;
    

    获取记录

    获取记录,从结果中删除软删除记录。

    CREATE OR REPLACE VIEW vMyTable AS
    SELECT *
    FROM `vMyTableAll`
    WHERE `deleted_at` IS NULL;
    

    触发器和验证

    对于这个例子,我将实现一个唯一的title 验证

    DROP PROCEDURE IF EXISTS myTable_uk_title;
    DROP TRIGGER IF EXISTS myTable_insert_uk_title;
    DROP TRIGGER IF EXISTS myTable_update_uk_title;
    
    DELIMITER //
    
    CREATE PROCEDURE myTable_uk_title(id INT, title VARCHAR(32)) BEGIN
      IF (
        SELECT COUNT(*)
        FROM vMyTable AS T
        WHERE
          T.id <> id
          AND T.title = title
      ) > 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Duplicated "title"', MYSQL_ERRNO = 1000;
      END IF;
    END //
    
    CREATE TRIGGER myTable_insert_uk_title BEFORE INSERT ON myTable
    FOR EACH ROW
    BEGIN
      CALL myTable_uk_title(NEW.id, NEW.title);
    END //
    
    CREATE TRIGGER myTable_update_uk_title BEFORE UPDATE ON myTable
    FOR EACH ROW
    BEGIN
      CALL myTable_uk_title(NEW.id, NEW.title);
    END //
    
    DELIMITER ;
    

    使用示例

    选择

    SELECT * FROM `vMyTable`;
    

    选择删除记录

    SELECT * FROM `vMyTableAll`;
    

    插入/添加/新建

    INSERT INTO myTable (`title`) VALUES ('Test 1');
    

    更新/编辑

    更新操作应使用以下代码完成,而不是UPDATE ...

    INSERT INTO myTable (`id`, `version`, `title`, `description`)
    SELECT
        `id`
      , `version` + 1 as `version` -- New version
      , `title`
      , 'New description' AS `description`
      FROM `vMyTable`
      WHERE id = 1;
    

    软删除

    软删除动作是历史的另一点:

    INSERT INTO myTable (`id`, `version`, `title`, `description`, `deleted_at`)
    SELECT
        `id`
      , `version` + 1 as `version` -- New version
      , `title`
      , `description`
      , NOW() AS `deleted_at`
      FROM `vMyTable`
      WHERE id = 1;
    

    恢复软删除记录

    INSERT INTO myTable (`id`, `version`, `title`, `description`, `deleted_at`)
    SELECT
        `id`
      , `version` + 1 as `version` -- New version
      , `title`
      , `description`
      , null AS `deleted_at`
      FROM `vMyTableAll` -- Get with deleted
      WHERE id = 1;
    

    删除记录和历史

    删除相关历史记录:

    DELETE FROM `myTable` WHERE id = 1;
    

    记录历史

    SELECT *
    FROM `myTable`
    WHERE id = 1
    ORDER BY `version` DESC;
    

    缺点

    • 唯一键约束是不可能的,但您可以创建一个触发器来处理它。
    • 如果要保存历史记录,则无法同时更新多条记录 (UPDATE ...)。
    • 如果要保存历史记录,则无法同时删除多条记录 (DELETE ...)。

    参考

    【讨论】:

      【解决方案3】:

      为了保留所有历史记录,我用两个表实现了一些东西。

      这是基本思路!您可以根据自己的要求编辑titledescription 列。

      在 MySQL 上实现

      下面的实现是针对MySQL的,但是这个想法也可以在其他类型的SQL数据库上实现。

      -- Tables
      
      DROP TABLE IF EXISTS `users`;
      CREATE TABLE IF NOT EXISTS `users` (
        `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
        `name` varchar(32) NOT NULL,
        PRIMARY KEY (`id`),
        UNIQUE KEY `name` (`name`) USING BTREE
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
      
      DROP TABLE IF EXISTS `myTable`;
      CREATE TABLE IF NOT EXISTS `myTable` (
        `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
        `title` varchar(32) NOT NULL,
        `description` varchar(2048) DEFAULT NULL,
        `edited_by` int(10) UNSIGNED DEFAULT NULL,
        `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `updated_at` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`),
        UNIQUE KEY `title` (`title`) USING BTREE,
        KEY `myTalbe_users_edited_by_fk` (`edited_by`)
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
      
      DROP TABLE IF EXISTS `myTable_history`;
      CREATE TABLE IF NOT EXISTS `myTable_history` (
        `id` int(10) UNSIGNED NOT NULL COMMENT 'ID',
        `version` int(10) UNSIGNED NOT NULL,
        `title` varchar(32) NOT NULL,
        `description` varchar(2048) DEFAULT NULL,
        `edited_by` int(10) UNSIGNED DEFAULT NULL,
        `created_at` timestamp NULL DEFAULT NULL,
        `updated_at` timestamp NULL DEFAULT NULL,
        `deleted_at` timestamp NULL DEFAULT NULL,
        `history_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`,`version`) USING BTREE,
        KEY `title` (`title`),
        KEY `history_users_edited_by_fk` (`edited_by`)
      ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
      
      ALTER TABLE `myTable`
        ADD CONSTRAINT `myTalbe_users_edited_by_fk` FOREIGN KEY (`edited_by`) REFERENCES `users` (`id`) ON UPDATE CASCADE;
      ALTER TABLE `myTable_history`
        ADD CONSTRAINT `history_users_edited_by_fk` FOREIGN KEY (`edited_by`) REFERENCES `users` (`id`);
      
      -- Triggers
      
      DROP TRIGGER IF EXISTS myTable_insert_history;
      DROP TRIGGER IF EXISTS myTable_update_history;
      DROP TRIGGER IF EXISTS myTable_delete_history;
      
      DELIMITER //
      
      CREATE TRIGGER myTable_insert_history AFTER INSERT ON myTable
      FOR EACH ROW
      BEGIN
        INSERT INTO myTable_history (
            `id`
          , `version`
          , `title`
          , `description`
          , `edited_by`
          , `created_at`
          , `updated_at`
        ) VALUES (
            NEW.id
          , 0
          , NEW.title
          , NEW.description
          , NEW.edited_by
          , NEW.created_at
          , NEW.updated_at
        );
      END //
      
      CREATE TRIGGER myTable_update_history AFTER UPDATE ON myTable
      FOR EACH ROW
      BEGIN
        INSERT INTO myTable_history (
            `id`
          , `version`
          , `title`
          , `description`
          , `edited_by`
          , `created_at`
          , `updated_at`
        )
        SELECT
            NEW.id
          , MAX(`version`) + 1
          , NEW.title
          , NEW.description
          , NEW.edited_by
          , NEW.created_at
          , NEW.updated_at
        FROM myTable_history
        WHERE id = OLD.id;
      END //
      
      CREATE TRIGGER myTable_delete_history AFTER DELETE ON myTable
      FOR EACH ROW
      BEGIN
        INSERT INTO myTable_history (
            `id`
          , `version`
          , `title`
          , `description`
          , `edited_by`
          , `created_at`
          , `updated_at`
          , `deleted_at`
        )
        SELECT
            OLD.id
          , MAX(`version`) + 1
          , OLD.title
          , OLD.description
          , OLD.edited_by
          , OLD.created_at
          , OLD.updated_at
          , NOW()
        FROM myTable_history
        WHERE id = OLD.id;
      END //
      
      DELIMITER ;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-13
        相关资源
        最近更新 更多