【问题标题】:How to apply complex constraints to a database table in MySQL?如何对 MySQL 中的数据库表应用复杂的约束?
【发布时间】:2017-07-23 03:33:27
【问题描述】:

由于我正处于为我的一个项目设置数据库的最后阶段,我想到了一个需要添加到任务表中的额外约束(见下图),但我不确定如何这可以在 MySQL 中实现。

原始数据库架构(无标记): Click here.

带有标记的数据库架构:

为每个作业 (job) 分配一个 WBS 代码列表 (wbscodelist)。这些列表中的每一个都包含许多适用于该工作的 WBS 代码 (wbscodeitem)。一个例子是:

Job A uses WBS Code List #1
Job B uses WBS Code List #2
Job C uses WBS Code List #1
etc.

WBS Code List #1 has codes: [100, 105, 110, 115, 120]
WBS Code List #2 has codes: [2180, 2190]
etc.

目前,task.fk_wbsCodeItemIDwbscodeitem.wbsCodeItemID 的外键(标记为橙色)。

我在这里面临的问题是 task 可能使用不适用于 job 的 WBS 代码。

我想进一步限制task.fk_wbsCodeItemID,以便它可以采用的值取决于wbscodeitem.fk_wbsCodeListIDjob.wbscodeitem.fk_wbsCodeListID 在该工作中是否相等(标记为红色)。

如何在 MySQL 中为此数据库架构包含此约束?这个问题可能是由于这个数据库的当前设计(我需要改变它)吗?

我知道这可能需要更多细节,因此我可以在必要时提供更多细节或澄清。

【问题讨论】:

  • 我更喜欢将约束逻辑放在app代码中,而不是尝试使用FK的有限能力。

标签: mysql database database-design relational-database


【解决方案1】:

一种方法是通过受控冗余。您可以将函数依赖关系 jobNumber -> fk_wbsCodeListID 非规范化为 joblocationtask,并使用复合 FK 约束来防止不一致。类似地,函数依赖wbsCodeItemID -> fk_wbsCodeListID 可以非规范化为tasktask 中的重叠复合 FK 约束将强制执行您的要求:

CREATE TABLE `wbscodelist` (
  `wbsCodeListID` int(11) NOT NULL,
  `description` varchar(45) NOT NULL,
  PRIMARY KEY (`wbsCodeListID`)
) ENGINE=InnoDB;

CREATE TABLE `wbscodeitem` (
  `wbsCodeItemID` int(11) NOT NULL,
  `wbsCode` varchar(10) NOT NULL,
  `description` varchar(50) NOT NULL,
  `notes` varchar(80) NOT NULL,
  `fk_wbsCodeListID` int(11) NOT NULL,
  PRIMARY KEY (`wbsCodeItemID`),
  UNIQUE KEY (`wbsCodeItemID`,`fk_wbsCodeListID`),
  KEY (`fk_wbsCodeListID`),
  FOREIGN KEY (`fk_wbsCodeListID`) REFERENCES `wbscodelist` (`wbsCodeListID`) ON UPDATE CASCADE
) ENGINE=InnoDB;

CREATE TABLE `job` (
  `jobNumber` varchar(15) NOT NULL,
  `jobName` varchar(45) NOT NULL,
  `fk_wbsCodeListID` int(11) NOT NULL,
  `isActive` bit(1) NOT NULL,
  PRIMARY KEY (`jobNumber`),
  UNIQUE KEY (`jobNumber`,`fk_wbsCodeListID`),
  KEY (`fk_wbsCodeListID`),
  FOREIGN KEY (`fk_wbsCodeListID`) REFERENCES `wbscodelist` (`wbsCodeListID`) ON UPDATE CASCADE
) ENGINE=InnoDB;

CREATE TABLE `joblocation` (
  `jobLocationID` int(11) NOT NULL,
  `roomNumber` varchar(25) NOT NULL,
  `fk_jobNumber` varchar(15) NOT NULL,
  `fk_wbsCodeListID` int(11) NOT NULL,
  PRIMARY KEY (`jobLocationID`),
  KEY (`fk_jobNumber`,`fk_wbsCodeListID`),
  KEY (`jobLocationID`,`fk_wbsCodeListID`),
  FOREIGN KEY (`fk_jobNumber`, `fk_wbsCodeListID`) REFERENCES `job` (`jobNumber`, `fk_wbsCodeListID`) ON UPDATE CASCADE
) ENGINE=InnoDB;

CREATE TABLE `task` (
  `taskID` int(11) NOT NULL,
  `fk_JobLocationID` int(11) NOT NULL,
  `fk_JobNumber` varchar(15) NOT NULL,
  `fk_wbsCodeItemID` int(11) NOT NULL,
  `fk_wbsCodeListID` int(11) NOT NULL,
  PRIMARY KEY (`taskID`),
  KEY (`fk_wbsCodeItemID`,`fk_wbsCodeListID`),
  KEY (`fk_JobLocationID`,`fk_wbsCodeListID`),
  FOREIGN KEY (`fk_JobLocationID`, `fk_wbsCodeListID`) REFERENCES `joblocation` (`jobLocationID`, `fk_wbsCodeListID`) ON UPDATE CASCADE,
  FOREIGN KEY (`fk_wbsCodeItemID`, `fk_wbsCodeListID`) REFERENCES `wbscodeitem` (`wbsCodeItemID`, `fk_wbsCodeListID`) ON UPDATE CASCADE
) ENGINE=InnoDB;

注意复合索引以匹配复合 FK 约束。

另一种选择是创建触发器以检查插入/更新的 FK 值是否通过连接关联:

DELIMITER ;;

CREATE TRIGGER check_task_insert BEFORE INSERT ON task
    FOR EACH ROW
    BEGIN
        IF NOT EXISTS (
            SELECT 1
            FROM joblocation loc
            JOIN job ON loc.fk_jobNumber = job.jobNumber
            JOIN wbscodeitem itm ON job.fk_wbsCodeListID = itm.fk_wbsCodeListID
            WHERE loc.jobLocationID = new.fk_jobLocationID
            AND itm.wbsCodeItemID = new.fk_wbsCodeItemID
        ) THEN
            SIGNAL SQLSTATE '45000'   
            SET MESSAGE_TEXT = 'fk_wbsCodeItemID doesn\'t match fk_wbsCodeListID of associated job';
        END IF;
    END;
;;

CREATE TRIGGER check_task_update BEFORE UPDATE ON task
    FOR EACH ROW
    BEGIN
        IF NOT EXISTS (
            SELECT 1
            FROM joblocation loc
            JOIN job ON loc.fk_jobNumber = job.jobNumber
            JOIN wbscodeitem itm ON job.fk_wbsCodeListID = itm.fk_wbsCodeListID
            WHERE loc.jobLocationID = new.fk_jobLocationID
            AND itm.wbsCodeItemID = new.fk_wbsCodeItemID
        ) THEN
            SIGNAL SQLSTATE '45000'   
            SET MESSAGE_TEXT = 'fk_wbsCodeItemID doesn\'t match fk_wbsCodeListID of associated job';
        END IF;
    END;
;;

DELIMITER ;

【讨论】:

    【解决方案2】:

    您能否明确限制任务仅引用适用于其各自工作的代码项?是的。问题更多的是您是否需要在数据库级别这样做,以及增加架构的复杂性是否值得。

    哪些代码可用于哪些任务实际上是业务逻辑问题的教科书示例,虽然像您建议的那样将业务逻辑合并到架构中并非闻所未闻,但它已接近关系数据库的边界最擅长。数据库提供结构,并且可以通过强制直接关系对其一致性做出基本保证;您所拥有的是一种间接关系(任务与与作业相关的代码项),您需要直接建立或通过验证您对数据库的输入来强制执行。

    要在架构中实施约束,您需要一个新表 wbscodelist_items,它充当 wbscodelistwbscodeitem 之间的联结表,将 wbscodeitem.fk_wbsCodeListID 定义的 one:many 关系替换为 many : 很多关系。这会打开它自己的蠕虫罐——现在您可以在多个列表中拥有代码项,这可能不是您想要的!但是task 可以引用wbscodelist_items,这确保它不能有不在正确列表中的代码项。

    尽管如此,仍然不能保证列表属于任务的工作。简单地给wbscodelist_items 一个外键给job 不会减少它——你最终会得到两个重复的关系(任务通过joblocationwbscodelist_items 分配给作业,并通过wbscodelist_items 将项目代码分配给作业和wbscodelist) 完全未经检查,因此可能不一致。为了在不给自己带来更严重的结构性问题的情况下加强这种关系,您必须将wbscodelist 转为wbscodelist_itemsjoblocation 转为task,复制数据并通常使生活变得更加困难。

    总而言之,我会保留您的结构并验证您的输入。

    【讨论】:

      【解决方案3】:

      我只关注关键属性,总共有六个关系(表)。所有约束都通过PK和FK解决,不需要触发器。诚然,我不太了解 code_list 的概念,如果您需要一个列表来轻松管理代码集,那么您可以为此添加另一个关系。

      注意:

      [Px] = 谓词 x [cx.y] = 约束 x.y PK = 主键 AKn = 备用键(唯一) FKn = 外键


      [P1] 作业 (job_ID) 存在。

      [c1.1] Job 由 job_ID 标识。

      工作{job_ID}——P1 PK {job_ID} -- c1.1


      [P2]位置 (loc_ID) 存在。

      [c2.1] 位置由 loc_ID 标识。

      位置 {loc_ID} -- P2 PK {loc_ID}——c2.1


      [P3] 将作业 (job_ID) 分配给位置 (loc_ID)。

      [c3.1] 每个工作都可以分配到多个位置;对于每个位置,该位置可能有多个分配的工作。

      [c3.2] 如果一个工作被分配到一个位置,那么这个工作必须存在。

      [c3.3] 如果将工作分配到某个位置,则该位置必须存在。

      job_location {job_ID, loc_ID} -- P3 PK {job_ID, loc_ID} -- c3.1 FK1 {job_ID} 参考作业 {job_ID} -- c3.2 FK2 {loc_ID} 参考位置 {loc_ID} -- c3.3


      [P4] WBS 代码 (wbs_ID) 存在。

      [c4.1] WBS 代码由 wbs_ID 标识。

      wbs_code {wbs_ID} -- P4 PK {wbs_ID}——c4.1


      [P5] 为作业 (job_ID) 分配 wbs 代码 (wbs_ID)。

      [c5.1] 每个作业可以分配多个 wbs 代码;对于每个 wbs 代码,可以将 wbs 代码分配给多个工作。

      [c5.2] 如果为作业分配了 wbs 代码,则该作业必须存在。

      [c5.3] 如果为作业分配了 wbs 代码,则该 wbs 代码必须存在。

      job_wbs {job_ID, wbs_ID} -- P5 PK {job_ID, wbs_ID} -- c5.1 FK1 {job_ID} 参考作业 {job_ID} -- c5.2 FK2 {wbs_ID} 参考 wbs_code {wbs_ID} -- c5.3


      [P6] 作业 (job_ID) 的任务编号 (job_task_No) 在位置 (loc_ID) 执行,并分配有 wbs 代码 (wbs_ID)。

      [c6.1] 任务由job_ID和job_task_No组合识别。

      [c6.2] 如果执行了作业的任务,那么该作业必须存在。

      [c6.3] 如果某项工作的任务在某个位置执行,则该工作必须分配到该位置。

      [c6.4] 如果使用分配 wbs 代码执行作业的任务,则必须将 wbs 代码分配给该作业。

      任务 {job_ID, job_task_No, loc_ID, wbs_ID} -- P6 PK {job_ID, job_task_No} -- c6.1 FK1 {job_ID} 参考作业 {job_ID} -- c6.2 FK2 {job_ID, loc_ID} REFERENCES job_location {job_ID, loc_ID} -- c6.3 FK3 {job_ID, wbs_ID} 参考 job_wbs {job_ID, wbs_ID} -- c6.4

      【讨论】:

        【解决方案4】:

        解决方案相当简单。只需对设计稍作调整即可。

        看起来一个工作地点只能托管一个工作,但一个工作可以分布在多个地点。此外,一个列表可能包含多个代码项,一个项目只能出现在一个列表中,每个列表只与一个作业相关联。

        让我们暂时处理一下。

        create table Jobs(
          ID           int  primary key,
          Name         vachar( 45 ),
          IsActive     bit
        );
        
        create table Locations(
          ID           int primary key,
          RoomNum      varchar( 25 )
        );
        
        create table JobLocations(
          LocID        int references Locations( ID ),
          JobID        int unique references Jobs( ID ),
          constraint PK_JobLocations primary key( LocID, JobID )
        );
        
        create table Items(
          ID           int primary key,
          Code         varchar( 10 ),
          Description  varchar( 50 ),
          Notes        varchar( 80 )
        );
        
        create table ItemLists(
          ID           int primary key,
          Description  varchar( 45 )
        );
        
        create table ListItems(
          ListID       int references ItemList( ID ),
          ItemID       int unique references Items( ID ),
          constraint PK_ListItems primary key( ListID, ItemID )
        );
        

        现在每个列表都必须与一个工作相关联。

        create table JobLists(
          JobId        int references Jobs( ID ),
          ListID       int references ItemLists( ID ),
          constraint PK_JobLists primary key( JobID, listID )
        );
        

        现在您有一项任务与特定位置的特定工作的一个项目相关联。为了具有完整的数据完整性,该任务必须引用一个作业、它的位置、与该作业关联的列表以及列表中出现的项目。这意味着您必须向 Tasks 表中添加两个字段。

        create table Tasks(
          ID           int primary key,
          LocID        int,
          JobID        int,
          ListID       int,
          ItemID       int,
          constraint FK_TaskJobLoc( LocID, JobID ) references JobLocations( LocID, JobID ),
          constraint FK_TaskJobList( JobID, ListID ) references JobLists( JobID, ListID ),
          constraint FK_TaskListItem( ListID, ItemID ) references ListItems( ListID, ItemID )
        );
        

        现在您已保证列表必须与特定位置的工作相关联,并且项目必须与列表相关联。因此,该项目必须与作业相关联。

        我很快就将其汇总在一起,因此请仔细检查完整的参考链。但是,您可以看到,如果要引用引用链头部的对象该链末尾的对象,则必须使用中间引用。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-02-10
          • 2022-11-17
          • 2021-11-12
          • 2020-08-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多