后悔身份:有没有办法强制插入指定身份列?

TTT*_*TTT 6 sql-server identity sql-server-2017

为了防止XY 问题,这是我们要解决的实际问题:

问题:

不幸的是,我们有一堆查找表,它们是在主键上使用标识列创建的,这是一个int. 我们希望可以简单地删除身份,但是,我们有一些带有指向身份列的外键的大型表,我的理解是在这种情况下删除身份很困难。我们对身份感到遗憾的原因是因为这些表需要跨多个环境同步,而开发人员通过编写脚本将数据插入到这些表中,而我们在多个环境上运行这些脚本但不一定总是按照相同的顺序,所以我们问开发人员始终:

  1. 启用身份插入
  2. 插入具有硬编码整数 ID 的行
  3. 禁用身份插入

如果每个人都这样做,数据要么保持同步,要么脚本失败,我们可以立即采取纠正措施来解决冲突。当然,有时开发者忘记遵守规则,直接无标识地插入,不同环境下以不同顺序运行的不同脚本的自动增量导致它们不同步,从而出现问题。

一个想法:

我们可以强制开发人员始终指定标识列吗?我认为没有办法简单地禁用这些表上的身份。如果我们将身份重新播种到较低的数字会怎样?当种子值已经存在时,任何未指定所有列的插入都会失败,并且会继续失败,直到插入尝试的次数超过现有(连续)行的数量。但是,在一次正确的插入之后,就会重新为表设定种子,下一次不正确的插入将再次使用自动增量。因此,这个想法的推断是在每次插入后(也许使用触发器,这感觉很奇怪,但可能有效?),或者按计划,或者可能每次我们运行开发人员的脚本时,将表重新设置为一个较低的现有数字。

这是一个合理的想法,和/或有更好的解决方案吗?

旁注:我们确实有一些其他想法,我认为这些想法超出了这个问题的范围,例如:

  1. 门控签入将解析脚本以插入某些表而不指定标识列,如果我们检测到这一点,则会失败。
  2. 将所有这些数据存储在源中并在部署时更新整个表。(而不是使用运行一次插入脚本。)
  3. 不要运行在所有环境上更改这些表的数据脚本,而是使用复制或其他同步机制。

尽管从长远来看,这些其他想法可能会更好,但似乎最容易实现的目标只是重新播种这些表,因此不正确的插入将会失败。

Mar*_*ith 7

与其“将表重新播种到现有的较低数字”,然后必须在每次插入后重置种子,“正确”地执行此操作,更好的想法(假设通常是正增量值)可能是将其设置为最大值数据类型支持的值。

这应该会导致IDENTITY值的自动生成被破坏,除非有人再次重新播种。

示例(两个 catch 块中返回的错误均为“将 IDENTITY 转换为 int 数据类型时出现算术溢出错误。”)

DROP TABLE IF EXISTS dbo.MyLookupTable

CREATE TABLE dbo.MyLookupTable
(
IdentityColumnOfWoe INT IDENTITY PRIMARY KEY,
SomeValue VARCHAR(30)
)

SET IDENTITY_INSERT dbo.MyLookupTable ON

INSERT dbo.MyLookupTable(IdentityColumnOfWoe, SomeValue)
VALUES (1, 'Explicit value 1'),
       (2, 'Explicit value 2');

SET IDENTITY_INSERT dbo.MyLookupTable OFF

DBCC CHECKIDENT ('dbo.MyLookupTable', RESEED, 2147483647);

BEGIN TRY
INSERT dbo.MyLookupTable VALUES ('Bad Insert')
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE()
END CATCH


SET IDENTITY_INSERT dbo.MyLookupTable ON

INSERT dbo.MyLookupTable(IdentityColumnOfWoe, SomeValue)
VALUES (3, 'Explicit value 3');

SET IDENTITY_INSERT dbo.MyLookupTable OFF

BEGIN TRY
INSERT dbo.MyLookupTable VALUES ('Still broken?')
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE()
END CATCH
Run Code Online (Sandbox Code Playgroud)

对此的一个变体(取决于数据类型提供了多少备用容量)可能是为错误插入保留空间并使用检查约束阻止它们。这可以提供更多信息的错误消息。

ALTER TABLE dbo.MyLookupTable ADD CONSTRAINT CK_MyLookupTable_AlwaysUseSetIdentityInsert CHECK (IdentityColumnOfWoe <= 2000000000)
DBCC CHECKIDENT ('dbo.MyLookupTable', RESEED, 2000000000);
Run Code Online (Sandbox Code Playgroud)

消息 547,级别 16,状态 0,第 21 行 INSERT 语句与 CHECK 约束“CK_MyLookupTable_AlwaysUseSetIdentityInsert”冲突。冲突发生在数据库“Test”、表“dbo.MyLookupTable”、列“IdentityColumnOfWoe”中。


Pau*_*ite 5

已触发

另一种选择是使用而不是触发器

CREATE TABLE dbo.Regret 
(
    i integer IDENTITY(1, 1) NOT NULL 
        CONSTRAINT [PK dbo.Regret i] 
        PRIMARY KEY CLUSTERED,
    v integer NOT NULL
);

CREATE TABLE dbo.Consequence 
(
    ri integer NOT NULL
        CONSTRAINT [FK dbo.Regret i]
        FOREIGN KEY (ri)
        REFERENCES dbo.Regret (i)
        ON DELETE CASCADE
        ON UPDATE CASCADE,
    c integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)
CREATE OR ALTER TRIGGER MustUseIdentityInsert
ON dbo.Regret
INSTEAD OF INSERT
AS
IF @@ROWCOUNT = 0 RETURN;
SET NOCOUNT, XACT_ABORT ON;
SET ROWCOUNT 0;

INSERT dbo.Regret 
    (i, v)
SELECT 
    -- The identity column arrives containing zero if
    -- an explicit value wasn't provided by the original statement
    NULLIF(I.i, 0), I.v
FROM Inserted AS I;
GO
Run Code Online (Sandbox Code Playgroud)

测试:

--Msg 544, Level 16, State 1, Procedure MustUseIdentityInsert
--Cannot insert explicit value for identity column in table 'Regret' 
--when IDENTITY_INSERT is set to OFF.
INSERT dbo.Regret (v) 
VALUES (1);

--Msg 544, Level 16, State 1
--Cannot insert explicit value for identity column in table 'Regret' 
--when IDENTITY_INSERT is set to OFF.
INSERT dbo.Regret (i, v) 
VALUES (1, 1);

SET IDENTITY_INSERT dbo.Regret ON;

--Msg 515, Level 16, State 2, Procedure MustUseIdentityInsert
--Cannot insert the value NULL into column 'i', table 'Sandpit.dbo.Regret'; 
--column does not allow nulls. INSERT fails.
INSERT dbo.Regret (v) 
VALUES (1);
Run Code Online (Sandbox Code Playgroud)
-- Success (with IDENTITY_INSERT still on)
INSERT dbo.Regret 
    (i, v) 
VALUES 
    (1, 1),
    (2, 2);

-- Also succeeds
MERGE dbo.Regret AS R
USING 
(
    SELECT SV.number
    FROM master.dbo.spt_values AS SV
    WHERE SV.[type] = N'P'
    AND SV.number > 0
) AS U (n)
    ON U.n = R.i
WHEN NOT MATCHED THEN
    INSERT (i, v) 
    VALUES (U.n, CHECKSUM(NEWID()));

SET IDENTITY_INSERT dbo.Regret OFF;
Run Code Online (Sandbox Code Playgroud)

这一想法的一个缺点是您不能使用显式的身份值零。

我可能更喜欢其中一种重新播种方法,但这是另一种选择。

删除身份

这是一种痛苦,需要直接的语言支持。

在某些涉及大型桌子的情况下,使用众所周知的程序可以减少创伤SWITCH。这并不真正适用于您的情况,因为查找表很小而引用表很大。重要的是,重新创建外键将需要一段时间并保持Sch-M锁定,从而阻止两个表上的所有其他活动。

尽管如此:

IF COLUMNPROPERTY(OBJECT_ID(N'dbo.Regret', 'U'), N'i', 'IsIdentity') = 1
BEGIN TRY
    BEGIN TRANSACTION;

        DROP TABLE IF EXISTS dbo.NoRegret;

        -- Switch target with a compatible schema but no IDENTITY
        CREATE TABLE dbo.NoRegret 
        (
            i integer NOT NULL 
                CONSTRAINT [PK dbo.NoRegret i] 
                PRIMARY KEY,
            v integer NOT NULL
        );

        -- Drop the FK
        ALTER TABLE dbo.Consequence 
            DROP CONSTRAINT [FK dbo.Regret i];

        -- Metadata-only switch
        ALTER TABLE dbo.Regret 
            SWITCH TO dbo.NoRegret;

        -- Don't need the original now
        DROP TABLE dbo.Regret;

        -- Fix up the names
        EXECUTE sys.sp_rename
            @objname = N'dbo.NoRegret',
            @newname = N'Regret',
            @objtype = 'OBJECT';

        EXECUTE sys.sp_rename
            @objname = N'[PK dbo.NoRegret i]',
            @newname = N'[PK dbo.Regret i]',
            @objtype = 'OBJECT';

        -- Add the FK back
        ALTER TABLE dbo.Consequence 
            WITH CHECK ADD
                CONSTRAINT [FK dbo.Regret i]
                FOREIGN KEY (ri) 
                REFERENCES dbo.Regret;

    COMMIT TRANSACTION;
END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
    THROW;
END CATCH;
Run Code Online (Sandbox Code Playgroud)

抽象

最后一点是,当涉及到一些重定向时,更改事情会更容易。在您的情况下,部署机制与数据库物理设计(身份属性)紧密耦合。

如果没有对表的直接数据访问,并且所有数据更改都通过存储过程进行,那么进行此更改会更容易。对存储过程代码添加检查或更改通常很简单。

在理想的情况下,部署脚本通常应该能够针对任何当前状态运行(和重新运行),使代码和数据库达到相同的最终状态。毫无疑问你知道这一切。