父母和祖父母的外键

时间:2014-12-09 15:08:47

标签: c# sql sql-server linq

我有3个名为“Projects”,“Contracts”和“Incidents”的数据库表。该设计用于基于项目的维护系统。客户能够在项目上建立合同以维护各种安装。此外,可能或可能与项目合同无关的孤立事件应该是可报告的,例如有缺陷的安装。

项目与合同有1对多的关系(每个项目可以有多个合同,或者没有合同)。来自事故的记录最终必须可以解析为项目,但并不总是要求合同存在。在某些情况下,项目可能没有任何合同,但它应该能够发生事故。

我们的数据库设计师建议事件持有项目和合同的外键。实际上,这是一种与父母和祖父母分开的关系,以允许没有父记录。 另一种方法是创建一个“虚拟”合同。这两种解决方案都没有我的偏好。

更糟糕的是,合同还引用了另一个表中的“债务人”。因此,在没有合同的情况下,事件也应该能够引用债务人。

我不禁感到提议的方法违反了所有正常形式,并且有可能产生未来的问题,包括成为维护问题,因此我正在寻找一种能够保持完整性的替代解决方案表格。另外,是否有人熟悉这种方法可能导致的其他问题?

对于它的价值,我是负责编写将与此数据库一起使用的应用程序的开发人员。该项目将在WPF中使用LINQ over SQL创建。一个要求是它应该能够查询项目记录中的所有事件,包括通过合同引用的事件。

我在SO上寻找过类似的问题,虽然有许多处理祖父母的密钥,但它们似乎都不符合我的问题。

5 个答案:

答案 0 :(得分:1)

这是一种更简单的方法。为每个项目制定虚拟合同,用于没有正式合同的事件。该合同将始终用于解决项目。

这简化了数据库设计,但确实引入了其他问题。例如,要查找没有合同的事件,您不会在合同列中查找NULL。你会寻找"不是真正的合同"在合同表中。根据具体情况,这可能是一个更优雅的解决方案。这也解决了Debtor的问题。

这确实带来了另一个问题,即可能出现在多个合同上的事件。实际上,您最终可能需要支持另一个表,即事件和项目之间的n-m映射。

答案 1 :(得分:0)

我也没有偏好或经验。乍一看,我喜欢“假人”的想法。合同。如果您这样做,我建议在合同中添加一个特定的列,以便您可以轻松查看它是虚拟合同还是真实合同。

单个虚拟合同可以容纳所有无合同事件。当您开始使用虚拟合同的字段(如Debtor)时,就会产生虚拟合同的风险。 如果没有合同,那么同一项目的所有事件的债务人是否相同?如果不是这意味着你将最终得到多个虚拟合同(每个债务人一个)。也许将来你还有其他领域,最终会导致每次事故的虚假合同。

我不了解您的业务,但这些无合约事件可能会对您的设计造成非常危险。

另一种方法是将合同用作事件的蓝图/模板。在这种情况下,您在事件级别上拥有debtorId,contractId和projectId(...)。创建事件并将其链接到合同时,会将某些合同信息复制到事件中。这为事件级别提供了最大的灵活性,这是您无需合同的事件所需要的。如果存在相关合同,您可以决定对这些事件字段进行只读和同步。

答案 2 :(得分:0)

我不是所有“虚拟”数据的粉丝。如果事件只涉及一个合同,或者没有合同,那么我会采用这样的方法:

CREATE TABLE dbo.Project 
(
        ProjectID INT IDENTITY,
        Filler CHAR(1) NULL,
    CONSTRAINT PK_Project__ProjectID PRIMARY KEY (ProjectID)
);

CREATE TABLE dbo.Contract 
(
        ContractID INT IDENTITY,
        ProjectID INT NOT NULL,
        Filler CHAR(1) NULL,
    CONSTRAINT PK_Contract__ContractID PRIMARY KEY (ContractID),
    CONSTRAINT FK_Contract__ProjectID FOREIGN KEY (ProjectID) REFERENCES dbo.Project (ProjectID),
    CONSTRAINT UQ_Contract__ContractID_ProjectID UNIQUE (ContractID, ProjectID)
);

CREATE TABLE dbo.Incident
(
        IncidentID INT IDENTITY,
        ProjectID INT NOT NULL,
        ContractID INT NULL,
        Filler CHAR(1) NULL,
    CONSTRAINT PK_Incident__IncidentID PRIMARY KEY (IncidentID),
    CONSTRAINT FK_Incident__ProjectID FOREIGN KEY (ProjectID) REFERENCES dbo.Project (ProjectID),
    CONSTRAINT FK_Incident__ContractID FOREIGN KEY (ContractID, ProjectID) REFERENCES dbo.Contract (ContractID, ProjectID)
);

-- CREATE TWO DUMMY PROJECTS
INSERT dbo.Project DEFAULT VALUES;
INSERT dbo.Project DEFAULT VALUES;

-- ADD A CONTRACT TWO EACH
INSERT dbo.Contract (ProjectID)
SELECT  ProjectID
FROM    Project;

-- ADD AN INCIDENT TO EACH WITH NO CONTRACT
INSERT dbo.Incident (ProjectID)
SELECT  ProjectID
FROM    Project;

-- ADD A VALID INCIDENT TO EACH CONTRACT
INSERT dbo.Incident (ContractID, ProjectID)
SELECT  ContractID, ProjectID
FROM     dbo.Contract;

-- TRY AND ADD INVALID CONTRACT TO FIRST PROJECT
INSERT dbo.Incident (ContractID, ProjectID)
SELECT  c.ContractID, p.ProjectID
FROM    dbo.Project AS p
        CROSS JOIN dbo.Contract AS c
WHERE   c.ProjectID != p.ProjectID;

这将失败并显示错误:

>The INSERT statement conflicted with the FOREIGN KEY constraint "FK_Incident__ContractID". The conflict occurred in database "TestDB", table "dbo.Contract".

外键可以引用Contract上的唯一约束,它允许您在dbo.Incident中强制执行完整性,即您无法输入未正确映射到正在输入的合同的项目。该方案唯一真正的缺点是,当ProjectID填充时,您正在重复ContractID,但我不认为这是一个大问题。当然(在我看来)比虚拟数据更少的问题。

识别虚拟合同非常简单:

SELECT  *
FROM    dbo.Incident
WHERE   ContractID IS NULL;

答案 3 :(得分:0)

我认为在系统中引入虚拟合同是可行的方法。我通常做的是我会制作一个虚拟合同,将其标记为已删除(这样在任何查询中都不会被选中)。然后我会在你的解决方案中创建另一个项目来保存常量。那我就有这样的一句话:

public static readonly int DummyContractId = 25; // Or whatever the ID is of your dummy contract ID.

现在,您可以在代码中构建查询,以排除或包含具有虚拟合同的事件。

答案 4 :(得分:0)

我会使用触发器来强制完整性。现在,事件中始终需要ProjectID。当ContractID作为外键添加时,触发器将检查插入的ContractID的ProjectID是否与已插入的ProjectID匹配,否则不允许插入。这始终可以确保您不会以腐败的关系结束。此外,这使得生成项目中的所有事件或特定合同中的所有事件的报告变得更加容易。