【问题标题】:How to update primary key如何更新主键
【发布时间】:2010-03-23 10:55:08
【问题描述】:

这是我的问题 - 我有 2 张桌子:

  1. WORKER,列 |ID|OTHER_STAF| ,其中 ID 是主键
  2. FIRM,包含 |FPK|ID|SOMETHING_ELSE| 列,其中 FPK 和 ID 组合构成主键,ID 也是引用 WORKER.ID 的外键(不为空,并且必须与 WORKER 中的值相同)。

我想创建存储过程UPDATE_ID_WORKER,我想更改 WORKER 中特定 ID 的值,以及 FIRM 中特定 ID 值的所有实例。

【问题讨论】:

  • 我会争辩说,即使可以按照您的要求去做,但它不太可能是正确的做法。如果您遇到需要更改 PK 值的情况,那么您实际上是在尝试更改数据的身份。通常没有必要这样做,这表明您的数据模型可能是错误的。如果您确实必须这样做,请克隆您的原始数据,并在需要时删除原始数据。该操作无疑会表明您的意图。

标签: sql-server sql-server-2008 foreign-keys composite-primary-key


【解决方案1】:

您实际上不应该这样做,而是插入一条新记录并以这种方式更新它。
但是,如果您确实需要,可以执行以下操作:

  • 暂时禁用强制执行 FK 约束(例如 ALTER TABLE foo WITH NOCHECK CONSTRAINT ALL
  • 然后更新你的PK
  • 然后更新您的 FK 以匹配 PK 更改
  • 最终启用反向执行 FK 约束

【讨论】:

  • 我不喜欢引入新柱子的想法,所以这就是我想要的。谢谢大家
  • 这会“起作用”,但仍然是一个非常糟糕的主意。当其他用户更改数据时,您不仅会禁用您的 FK,而且还会支持糟糕的设计。有一天,下一个在这个数据库上工作的开发者会问(这里是 SO)如何修复这个 PK 更新混乱。
  • 对于需要完成这项工作的人来说,这是一个很好的答案。但值得展示有关如何禁用和重新启用约束的示例
  • 如果你在你的答案之上加上一个大胆的免责声明,那就是这样做是一个错误的决定,并且是糟糕的数据库设计的结果,这将有所帮助。此外,如果您能列举这些 cmets 中提到的与该方法相关的所有问题,这将有所帮助,以阻止人们在没有进一步考虑的情况下这样做。
【解决方案2】:

首先,我们选择稳定(非静态)的数据列来形成主键,正是因为我们希望避免更新关系数据库中的键(其中引用是通过键)。

  1. 对于这个问题,如果密钥是关系密钥(“由数据组成”)并因此具有关系完整性、能力和速度,或者“密钥”是否为一个记录 ID,没有关系完整性、能力和速度。效果是一样的。

  2. 我之所以这么说,是因为有很多无知者的帖子,他们认为这就是记录 ID 在某种程度上优于关系键的确切原因。

  3. 关键是,密钥或记录 ID 被迁移到需要引用的任何地方。

其次,如果您必须更改 Key 或 Record ID 的值,那么您必须更改它。这是符合 OLTP 标准的方法。注意高端厂商不允许“级联更新”。

  • 编写一个过程。 Foo_UpdateCascade_tr @ID,其中Foo为表名

  • 开始交易

  • 首先 INSERT-SELECT 父表中的新行,从旧行中使用新的 Key 或 RID 值

  • 其次,对于所有子表,从上到下工作,使用新的 Key 或 RID 值从旧行中插入选择新行

  • 第三,删除子表中具有旧 Key 或 RID 值的行,从下到上工作

  • 最后,删除父表中具有旧 Key 或 RID 值的行

  • 提交事务

关于其他答案

其他答案不正确。

  • 在更新所需行(父项加上所有子项)之后禁用约束然后启用它们不是人们在在线生产环境中会做的事情,如果他们希望继续工作的话。该建议适用于单用户数据库。

  • 需要更改 Key 或 RID 的值并不表示存在设计缺陷。这是一个普通的需求。通过选择稳定的(非静态的)密钥可以缓解这种情况。可以缓解,但不能消除。

  • 替代自然键的替代品不会产生任何影响。在您给出的示例中,“键”是代理项。并且需要更新。

    • 拜托,只是代理,没有“代理键”这样的东西,因为每个词都相互矛盾。要么是密钥(由数据组成),要么不是。代理不是由数据组成的,它是明确的非数据。它没有 Key 的任何属性。
  • 级联所有必需的更改没有什么“棘手”的。参考上面给出的步骤。

  • 没有什么可以阻止宇宙的变化。它改变。处理它。而且由于数据库是关于宇宙的事实的集合,当宇宙发生变化时,数据库将不得不改变。这就是大城市的生活,不适合新玩家。

  • 人们结婚和刺猬被埋葬都不是问题(尽管这些例子被用来暗示它一个问题)。因为我们不使用名称作为键。我们使用小的、稳定的标识符,例如用于识别宇宙中的数据。

    • 名称、描述等仅在一行中存在一次。密钥存在于它们被迁移的任何地方。如果“key”是一个 RID,那么 RID 也存在于它被迁移的任何地方。
  • 不要更新 PK! 是我最近读过的第二个最搞笑的东西。 新增一列是最多的。

【讨论】:

  • 您建议如何执行此操作-“第二,对于所有子表,从上到下工作,使用新的 Key 或 RID 值从旧行中插入选择新行”?您将一直受到 PK 约束。您不想更新子表吗?
  • 您说高端厂商不允许“级联更新”,但这不是最合适的情况吗?我认为这些答案中描述的复杂多步骤过程比依赖“级联更新”更危险且更容易出错。如果真的偏执,是否可以暂时添加“级联更新”约束,然后更改密钥,然后删除约束?这将始终确保参照完整性,并简化流程。
  • @SaebAmini 这不是逻辑,它是从期望的结果训练回推测的原因。逻辑是向前的,而不是向后的。不要担心我的信念,看看例子中的事实。 1) 由于给出的原因,应通过设计避免。 Identifier 必须由稳定的(不是不可变的)列组成,这很容易,因为在现实世界中识别某物的事物是稳定的。故事结束。 2)不能完全避免。在这些情况下,我给出了正确的解决方案。不同的故事结束。
  • @MladenB。 1)谢谢。 2) 这是真的。客观事实不在乎你是否同意。你应用不正确。我说需要改变一个Key,任何Key,都是普通的。这意味着您最好为每个表都为其编码正确的事务。 3)现在,那么,它是罕见的。那又怎样,必须得照顾。因此,它与密钥是否可能改变无关。 4) 选择NonKeys 使得NonKeys 永远不会改变的立场,意味着你不了解Key 是什么;真正的钥匙有什么属性;它在 NonKey 中丢失了什么。
  • 5) 指定 PK 是一个实现问题,无论您声明它是什么。当然,充其量,它是真正的关系密钥。通常不是。最坏的情况是ID
【解决方案3】:

如果您确定此更改适合您工作的环境:将辅助表上的 FK 条件设置为 UPDATE CASCADING。

例如,如果使用 SSMS 作为 GUI:

  1. 右键单击该键
  2. 选择修改
  3. 折叠“插入和更新特定”
  4. 对于“更新规则”,选择级联。
  5. 关闭对话框并保存密钥。

当您随后更新主表中 PK 列中的值时,其他表中的 FK 引用将更新为指向新值,从而保持数据完整性。

【讨论】:

  • 这是我预期的工作方式,但在 MS SQL 上,当尝试更新 PK 值时,我收到错误消息“无法更新标识列”。当您无论如何都无法更改主键时,我真的想知道“更新级联”有什么好处。
  • @Cito:标识列与主键不同,但可以选择标识列作为表的主键。您的错误与尝试更新主键无关 - 这是尝试更新标识列的错误。
  • @pbeentje 你说得对,似乎这就是他们在 T-SQL 中调用自动增量列的方式。所以如果你有一个自动递增的 PK,你就不能在 T-SQL 中更新它的值。我尝试将 IDENTITY_INSERT 设置为 OFF,但顾名思义,这仅对插入有帮助,对更新没有帮助。诚然,自然 PK 比这些自动递增 PK 更经常需要更改 PK 值。
【解决方案4】:

当您发现有必要更新主键值以及所有匹配的外键时,则需要修复整个设计。

级联所有必要的外键更改是很棘手的。最好不要更新主键,如果您认为有必要,您应该使用Surrogate Primary Key,它不是从应用程序数据派生的键。因此,它的值与业务逻辑无关,并且永远不需要更改(并且应该对最终用户不可见)。然后您可以更新并显示其他列。

例如:

BadUserTable
UserID     varchar(20) primary key --user last name
other columns...

如果您创建了许多表,这些表具有与 UserID 的 FK,以跟踪用户所做的所有工作,但该用户随后结婚并希望 ID 与他们的新姓氏相匹配,那么您就不走运了。

GoodUserTable
UserID    int identity(1,1) primary key
UserLogin varchar(20) 
other columns....

您现在将代理主键 FK 用于所有其他表,并在必要时显示 UserLogin,允许他们使用该值登录,当他们需要更改它时,您只能在一行的一列中更改它。

【讨论】:

  • Slavisa 一个代理人。如果“密钥”是真正的密钥或记录 ID,则没有区别。当“键”值改变时,它必须改变。
  • 当PK要改变时,这意味着你当前的数据需要用不同的/新的值来标识,这实际上是数据身份的改变。如果你正确地设计你的数据库,这种情况永远不会发生。通常,这种情况表明您的数据的 PK 选择不当。
【解决方案5】:

不要更新主键。如果您有任何其他表引用它,这可能会导致您保持数据完整无缺。

理想情况下,如果您想要一个可更新的唯一字段,请创建一个新字段。

【讨论】:

  • 我在其他程序中非常小心,所以我希望不会发生这种冲突。有没有办法更新这个主键?
  • @Slavisa,克隆您的数据,如果需要,删除原始数据。你正在寻求的是身份改变,如果你明智地选择你的 PK,它就不应该发生。
【解决方案6】:

您可以使用此递归函数来生成必要的 T-SQL 脚本。

CREATE FUNCTION dbo.Update_Delete_PrimaryKey
(
    @TableName      NVARCHAR(255),
    @ColumnName     NVARCHAR(255),
    @OldValue       NVARCHAR(MAX),
    @NewValue       NVARCHAR(MAX),
    @Del            BIT
)
RETURNS NVARCHAR 
(
    MAX
)
AS
BEGIN
    DECLARE @fks TABLE 
            (
                constraint_name NVARCHAR(255),
                table_name NVARCHAR(255),
                col NVARCHAR(255)
            );
    DECLARE @Sql                  NVARCHAR(MAX),
            @EnableConstraints     NVARCHAR(MAX);

    SET @Sql = '';
    SET @EnableConstraints = '';

    INSERT INTO @fks
      (
        constraint_name,
        table_name,
        col
      )
    SELECT oConstraint.name     constraint_name,
           oParent.name         table_name,
           oParentCol.name      col
    FROM   sys.foreign_key_columns sfkc
           --INNER JOIN sys.foreign_keys sfk
           --     ON  sfk.[object_id] = sfkc.constraint_object_id

           INNER JOIN sys.sysobjects oConstraint
                ON  sfkc.constraint_object_id = oConstraint.id
           INNER JOIN sys.sysobjects oParent
                ON  sfkc.parent_object_id = oParent.id
           INNER JOIN sys.all_columns oParentCol
                ON  sfkc.parent_object_id = oParentCol.object_id
                AND sfkc.parent_column_id = oParentCol.column_id
           INNER JOIN sys.sysobjects oReference
                ON  sfkc.referenced_object_id = oReference.id
           INNER JOIN sys.all_columns oReferenceCol
                ON  sfkc.referenced_object_id = oReferenceCol.object_id
                AND sfkc.referenced_column_id = oReferenceCol.column_id
    WHERE  oReference.name = @TableName
           AND oReferenceCol.name = @ColumnName
    --AND (@Del <> 1 OR sfk.delete_referential_action = 0)
    --AND (@Del = 1 OR sfk.update_referential_action = 0)

    IF EXISTS(
           SELECT 1
           FROM   @fks
       )
    BEGIN
        DECLARE @Constraint     NVARCHAR(255),
                @Table          NVARCHAR(255),
                @Col            NVARCHAR(255)  

        DECLARE Table_Cursor CURSOR LOCAL 
        FOR
            SELECT f.constraint_name,
                   f.table_name,
                   f.col
            FROM   @fks AS f

        OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @Constraint, @Table,@Col  
        WHILE (@@FETCH_STATUS = 0)
        BEGIN
            IF @Del <> 1
            BEGIN
                SET @Sql = @Sql + 'ALTER TABLE ' + @Table + ' NOCHECK CONSTRAINT ' + @Constraint + CHAR(13) + CHAR(10);
                SET @EnableConstraints = @EnableConstraints + 'ALTER TABLE ' + @Table + ' CHECK CONSTRAINT ' + @Constraint 
                    + CHAR(13) + CHAR(10);
            END

            SET @Sql = @Sql + dbo.Update_Delete_PrimaryKey(@Table, @Col, @OldValue, @NewValue, @Del);
            FETCH NEXT FROM Table_Cursor INTO @Constraint, @Table,@Col
        END

        CLOSE Table_Cursor DEALLOCATE Table_Cursor
    END

    DECLARE @DataType NVARCHAR(30);
    SELECT @DataType = t.name +
           CASE 
                WHEN t.name IN ('char', 'varchar', 'nchar', 'nvarchar') THEN '(' +
                     CASE 
                          WHEN c.max_length = -1 THEN 'MAX'
                          ELSE CONVERT(
                                   VARCHAR(4),
                                   CASE 
                                        WHEN t.name IN ('nchar', 'nvarchar') THEN c.max_length / 2
                                        ELSE c.max_length
                                   END
                               )
                     END + ')'
                WHEN t.name IN ('decimal', 'numeric') THEN '(' + CONVERT(VARCHAR(4), c.precision) + ',' 
                     + CONVERT(VARCHAR(4), c.Scale) + ')'
                ELSE ''
           END
    FROM   sys.columns c
           INNER JOIN sys.types t
                ON  c.user_type_id = t.user_type_id
    WHERE  c.object_id = OBJECT_ID(@TableName)
           AND c.name = @ColumnName

    IF @Del <> 1
    BEGIN
        SET @Sql = @Sql + 'UPDATE [' + @TableName + '] SET [' + @ColumnName + '] = CONVERT(' + @DataType + ', ' + ISNULL('N''' + @NewValue + '''', 'NULL') 
            + ') WHERE [' + @ColumnName + '] = CONVERT(' + @DataType + ', ' + ISNULL('N''' + @OldValue + '''', 'NULL') +
            ');' + CHAR(13) + CHAR(10);
        SET @Sql = @Sql + @EnableConstraints;
    END
    ELSE
        SET @Sql = @Sql + 'DELETE [' + @TableName + '] WHERE [' + @ColumnName + '] = CONVERT(' + @DataType + ', N''' + @OldValue 
            + ''');' + CHAR(13) + CHAR(10);
    RETURN @Sql;
END
GO

DECLARE @Result NVARCHAR(MAX);
SET @Result = dbo.Update_Delete_PrimaryKey('@TableName', '@ColumnName', '@OldValue', '@NewValue', 0);/*Update*/
EXEC (@Result)
SET @Result = dbo.Update_Delete_PrimaryKey('@TableName', '@ColumnName', '@OldValue', NULL, 1);/*Delete*/
EXEC (@Result)
GO

DROP FUNCTION Update_Delete_PrimaryKey;

【讨论】:

  • 在编写这么多代码之前,编写大量代码真的更容易吗,我怀疑甚至 OP 都没有耐心阅读,而不是先用人类语言解释你的逻辑?
  • 考虑您有两个相互关联的表或一个具有自关系的表,在第一种情况下,第一个关系可以在更新和删除时级联,但第二个关系不能级联,因为可能会导致循环或多个级联路径,对于第 2 种情况也是如此。如果要更新第二个主键,首先必须禁用关系,然后更新 2 个表,最后启用关系,此函数生成这些步骤。为了更好地理解,我将通过示例更新我的答案以用于这 2 个场景。 @MladenB。
  • 非常感谢您对您的回答的简要解释,非常感谢。如果数据库在生产服务器上,禁用关系可能会导致问题,远程客户端可能会同时添加/更新数据,这会破坏数据的一致性。最安全的方法是复制/克隆原始数据并使用所需值设置新记录。
猜你喜欢
  • 2014-07-14
  • 2019-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-15
  • 2018-03-15
  • 1970-01-01
相关资源
最近更新 更多