【问题标题】:Enforce Uniqueness of Related Entities强制相关实体的唯一性
【发布时间】:2017-08-24 13:59:01
【问题描述】:

在关系数据库 (SQL) 中,我有一个父实体,它可以有 0..n 个相关的子实体。父实体部分由其相关子实体的集合唯一标识,因此我不应该有两个相似的父实体具有相同的子实体集合。

所以我可以让父 1 与子 1 和子 2,父 2 与子 2 和子 3,但我不能有另一个父与子 2 和子 3。

理想情况下,我想使用数据库约束来强制执行这种唯一性。我考虑过将所有子记录的哈希值与父项一起存储,但想知道是否有更简单/更标准的方法来完成此操作。

有什么想法吗?

【问题讨论】:

  • 棘手。我们是否允许 Parent 3 与孩子 1、2 3 扩展您的示例?另外,请记住,当两件事绝对不同时,哈希会告诉您,但不要告诉您两件事是相同的。
  • 另外,孩子的总数是否有(合理的)上限?
  • @Damien_The_Unbeliever 是的。我们可以有一个父集合,该集合是另一个集合的超集,但没有两个父集合应该相等。至于哈希,我希望它足够大以使碰撞机会可以忽略不计。我会说我需要哈希来区分不超过 100 个独特的孩子组合。
  • 通过其子代识别父代似乎不是一个合适的关系模型。除非您违反第一范式,否则禁止同一组孩子与不同父母相关联的业务规则不能通过约束来强制执行。
  • @DanGuzman 有道理。您将使用哪些实体来模拟这种情况?

标签: sql-server database-design relational-database


【解决方案1】:

这种约束很棘手,因为 SQL 没有关系相等运算符,即没有简单的方法来评估 A=B,其中 A 和 B 是行集。标准 SQL 确实支持嵌套表,但不幸的是 SQL Server 不支持。

一个可能的答案是如下的谓词,它检查表中是否存在任何相同的族:

NOT EXISTS (
    SELECT 1
    FROM family f, family g
    WHERE f.child = g.child
    AND f.parent <> g.parent
    GROUP BY f.parent, g.parent
    HAVING COUNT(*) = (SELECT COUNT(*) FROM family WHERE parent = f.parent)
    AND COUNT(*) = (SELECT COUNT(*) FROM family WHERE parent = g.parent)
    )

请注意,此查询不会尝试处理没有孩子的家庭。在集合论的术语中,两个空集必然相同。如果您想允许没有孩子的家庭,那么您必须决定两个没有孩子的家庭是否应该被视为相同。

SQL 不是一种真正的关系语言,它远远达不到关系语言应该具备的能力。教程 D 是一个真正的关系语言示例,确实支持关系相等和关系值属性。在教程 D 中,您原则上可以将每个族表示为 relvar 中单个属性的值。该族属性也可以是键,因此不允许重复的族。

【讨论】:

  • 很好,看起来我们有相似的想法......这很漂亮,看起来它会比我的答案更好。谢谢。
  • 我太仓促了。我的原始查询没有给出正确的结果,所以我对其进行了编辑。
【解决方案2】:

感谢那些建议使用触发器的人的帮助。这大致就是我所拥有的并且似乎正在工作。

CREATE TRIGGER [dbo].[trig_Parent_Child_Uniqueness]
ON [dbo].[Parent_Child]
AFTER INSERT, UPDATE
AS
BEGIN
    IF EXISTS (
        SELECT 1
        FROM Parent p1
        --Compare each pair of parents
        JOIN Parent p2 ON p1.ParentId <> p2.ParentId
        WHERE NOT EXISTS (
            --Find any children that are different
            SELECT 1
            FROM (
                SELECT ChildId FROM Parent_Child c1
                WHERE c1.ParentId = p1.ParentId
            ) as c1
            FULL OUTER JOIN (
                SELECT ChildId FROM Parent_Child c2
                WHERE c2.ParentId = p2.ParentId
            ) as c2 ON c2.ChildId = c1.ChildId
            WHERE c1.ChildId IS NULL OR c2.ChildId IS NULL
        )
    ) ROLLBACK;
END;

编辑:或者更好的解决方案,改编自@sqlvogel

CREATE TRIGGER [dbo].[trig_Parent_Child_Uniqueness]
ON [dbo].[Parent_Child]
AFTER INSERT, UPDATE
AS
BEGIN
    IF EXISTS (
        SELECT 1
        FROM Parent_Child p1
        FULL JOIN Parent_Child p2 ON p1.ParentId <> p2.ParentId
            AND p1.ChildId = p2.ChildId
        GROUP BY p1.ParentId
        HAVING COUNT(p1.ParentId) = COUNT(*) 
            AND COUNT(p2.ParentId) = COUNT(*)
    ) ROLLBACK;
END;

【讨论】:

    【解决方案3】:

    这有点恶心,因为它包括触发器和光标:(

    它在父表中包含一个基于子表的列

    设置:

    CREATE TAble Parent
    (
        Id INT  Primary Key,
        Name VARCHAR(50),
        ChildItems VARCHAR(200) NOT NULL UNIQUE
    )
    CREATE TABLE Child
    (
        Id INT Primary Key,
        Name VARCHAR(50)
    )
    
    CREATE TABLE ParentChild
    (
        Id INT Identity Primary Key,
        ParentId INT,
        ChildId Int
    )
    

    触发器

    -- This gives the unique colmn a default based upon the id of the parent
    CREATE TRIGGER trg_Parent ON Parent
    INSTEAD OF Insert
    AS
        SET NOCOUNT ON
        INSERT INTO Parent (Id, Name, ChildItems)
        SELECT Id, Name, '/' + CAST(Id As Varchar(10)) + '/'
        FROM Inserted
    GO
    
    -- This updates the parent with a path based upon child items
    -- If a the exact same child items exist for another parent then this fails
    -- because of the unique index
    
    CREATE Trigger trg_ParentChild ON ParentChild
    AFTER Insert, Update
    AS
        DECLARE @ParentId INT = 0
        DECLARE @ChildItems VARCHAR(8000)    = ''
    
        DECLARE parentCursor CURSOR FOR 
            SELECT DISTINCT ParentId
            FROM Inserted
    
        OPEN parentCursor
        FETCH NEXT FROM parentCursor INTO @ParentId 
    
        WHILE @@FETCH_STATUS = 0
        BEGIN
            SELECT @ChildItems =   COALESCE(@ChildItems + '/ ', '') + CAST(ChildID As Varchar(10))
            FROM ParentChild
            WHERE ParentId = @ParentId
            ORDER BY ChildId
    
            UPDATE Parent
                SET ChildItems = @ChildITems
            WHERE Id = @ParentId
    
            FETCH NEXT FROM parentCursor INTO @ParentId 
            SET @ChildItems = ''
        END
        CLOSE parentCursor
        DEALLOCATE parentCursor
    
    GO
    

    数据设置

    INSERT INTO Parent (Id, Name)
    VALUES (1, 'Parent1'), (2,'Parent2'), (3, 'Parent3')
    
    
    
    INSERT INTO Child (Id, Name)
    VALUES (1,'Child1'), (2,'Child2'), (3,'Child3'), (4,'Child4')
    

    现在插入一些数据

    -- This one succeeds
    INSERT INTO ParentChild (ParentId, ChildId)
    VALUES (1,1),(1,2),(2,2),(2,3)
    
    -- This one Fails
    INSERT INTO ParentChild (ParentId, ChildId) VALUES (3,1),(3,2)
    

    【讨论】:

      猜你喜欢
      • 2021-01-01
      • 2014-06-20
      • 2016-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-01-09
      相关资源
      最近更新 更多