在 WHILE 循环中插入记录时出现奇怪的 TRY CATCH 行为

Strange TRY CATCH behavior that occurs when inserting records inside a WHILE LOOP

下面是我 运行 的代码(准备复制粘贴到 SQL 服务器)。我本质上是在尝试使用 WHILE LOOP 将记录从一个 table 一个接一个地插入到一个新的 table 中,并使用第三个 table 记录每次插入的结果。一旦我解决了这个问题,这段代码将被调整并用于几个存储过程。

代码如下:

SET NOCOUNT ON;

BEGIN TRY
    BEGIN TRANSACTION

    --Create dummy tables

    DROP TABLE IF EXISTS #OldTable
    DROP TABLE IF EXISTS #NewTable
    DROP TABLE IF EXISTS #LoggingTable

    CREATE TABLE #OldTable (
        ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,OldValue varchar(64)
    )

    INSERT INTO #OldTable
    VALUES
        ('1')
        ,('2')
        ,('3')
        ,('Four')
        ,('Five')
        ,('6')
        ,('Seven')

    CREATE TABLE #NewTable (
        ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,NewValue int
    )

    CREATE TABLE #LoggingTable (
        ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,OldTableID int NULL
        ,NewTableID int NULL
        ,InsertStatus varchar(MAX)
    )

    --Begin insert loop

    DECLARE @currentID int = NULL

    DECLARE THIS_CURSOR CURSOR FAST_FORWARD FOR
        SELECT ID FROM #OldTable ORDER BY ID

    OPEN THIS_CURSOR
    FETCH NEXT FROM THIS_CURSOR INTO @currentID

    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            --Perform insert
            INSERT INTO #NewTable
            OUTPUT @currentID, INSERTED.ID, 'Insert successful' INTO #LoggingTable (OldTableID, NewTableID, InsertStatus)
            SELECT CAST(OldValue AS int) FROM #OldTable WHERE ID = @currentID

            FETCH NEXT FROM THIS_CURSOR INTO @currentID
        END TRY
        BEGIN CATCH
            INSERT INTO #LoggingTable (OldTableID, NewTableID, InsertStatus) VALUES (@currentID, NULL, 'Error occurred during insert operation')
            FETCH NEXT FROM THIS_CURSOR INTO @currentID
        END CATCH   
    END

    CLOSE THIS_CURSOR
    DEALLOCATE THIS_CURSOR

    SELECT * FROM #OldTable
    SELECT * FROM #NewTable
    SELECT * FROM #LoggingTable

    COMMIT TRANSACTION
END TRY
BEGIN CATCH
    PRINT 'An error has occurred';

    -- Test if the transaction is uncommittable.  
    IF XACT_STATE() = -1  
    BEGIN  
        PRINT 'The transaction is in an uncommittable state. Rolling back transaction.'; 
        ROLLBACK TRANSACTION;  
    END;

    -- Test if the transaction is committable.  
    IF XACT_STATE() = 1  
    BEGIN  
        PRINT 'The transaction is committable. Committing transaction.';
        COMMIT TRANSACTION;     
    END;

    THROW;
END CATCH

这是我收到的错误消息:

The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.

违规行似乎是这一行:

INSERT INTO #LoggingTable (OldTableID, NewTableID, InsertStatus) VALUES (@currentID, NULL, 'Error occurred during insert operation')

但是,如果我注释掉这一行(我仍然需要它!),它只会移动到代码中的另一行,这通常意味着还有其他事情正在发生。我已经在网上彻底搜索过了,我仍然不确定这个错误的原因是什么。

关于此错误最奇怪的部分是,如果您删除外部 TRY CATCHTRANSACTION,脚本将完全按预期运行,没有任何问题;但是,恕我直言,这不是一个可行的解决方案,因为一旦包含在存储过程中就需要适当的错误捕获和事务处理。

我向上移动了 "COMMIT TRANSACTION" 几行并且它工作正常(抛出异常是因为它试图在回滚事务之前写入 catch 块中的 table):

SET NOCOUNT ON;

BEGIN TRY
    SET XACT_ABORT ON;
    BEGIN TRANSACTION;

    --Create dummy tables
    DROP TABLE IF EXISTS #OldTable;
    DROP TABLE IF EXISTS #NewTable;
    DROP TABLE IF EXISTS #LoggingTable;

    CREATE TABLE #OldTable
    (
        ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
        OldValue VARCHAR(64)
    );

    INSERT INTO #OldTable
    VALUES
    ('1'),
    ('2'),
    ('3'),
    ('Four'),
    ('Five'),
    ('6'),
    ('Seven');

    CREATE TABLE #NewTable
    (
        ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
        NewValue INT
    );

    CREATE TABLE #LoggingTable
    (
        ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
        OldTableID INT NULL,
        NewTableID INT NULL,
        InsertStatus VARCHAR(MAX)
    );

    --Begin insert loop

    DECLARE @currentID INT = NULL;

    DECLARE THIS_CURSOR CURSOR FAST_FORWARD FOR
    SELECT ID
    FROM #OldTable
    ORDER BY ID;

    OPEN THIS_CURSOR;
    FETCH NEXT FROM THIS_CURSOR
    INTO @currentID;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            --Perform insert
            INSERT INTO #NewTable
            OUTPUT @currentID,
                   INSERTED.ID,
                   'Insert successful'
            INTO #LoggingTable
            (
                OldTableID,
                NewTableID,
                InsertStatus
            )
            SELECT CAST(OldValue AS INT)
            FROM #OldTable
            WHERE ID = @currentID;

            FETCH NEXT FROM THIS_CURSOR
            INTO @currentID;
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            INSERT INTO #LoggingTable
            (
                OldTableID,
                NewTableID,
                InsertStatus
            )
            VALUES
            (@currentID, NULL, 'Error occurred during insert operation');
            FETCH NEXT FROM THIS_CURSOR
            INTO @currentID;
        END CATCH;
    END;

    CLOSE THIS_CURSOR;
    DEALLOCATE THIS_CURSOR;

    SELECT *
    FROM #OldTable;
    SELECT *
    FROM #NewTable;
    SELECT *
    FROM #LoggingTable;


    SET XACT_ABORT OFF;
END TRY
BEGIN CATCH
    PRINT 'An error has occurred';

    -- Test if the transaction is uncommittable.  
    IF XACT_STATE() = -1
    BEGIN
        PRINT 'The transaction is in an uncommittable state. Rolling back transaction.';
        ROLLBACK TRANSACTION;
    END;

    -- Test if the transaction is committable.  
    IF XACT_STATE() = 1
    BEGIN
        PRINT 'The transaction is committable. Committing transaction.';
        COMMIT TRANSACTION;
    END;

    THROW;
END CATCH;

老实说,我认为这里的交易只是让事情变得比他们需要的更复杂。每个 DML 语句周围都有一个隐式事务。如果插入失败,游标的主体只不过是带有 catch 块的插入语句。我删除了一堆处理显式事务的无关代码,您的示例完美运行。

--Create dummy tables

DROP TABLE IF EXISTS #OldTable
DROP TABLE IF EXISTS #NewTable
DROP TABLE IF EXISTS #LoggingTable

CREATE TABLE #OldTable (
    ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
    ,OldValue varchar(64)
)

INSERT INTO #OldTable
VALUES
    ('1')
    ,('2')
    ,('3')
    ,('Four')
    ,('Five')
    ,('6')
    ,('Seven')

CREATE TABLE #NewTable (
    ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
    ,NewValue int
)

CREATE TABLE #LoggingTable (
    ID int IDENTITY(1,1) NOT NULL PRIMARY KEY
    ,OldTableID int NULL
    ,NewTableID int NULL
    ,InsertStatus varchar(MAX)
)

--Begin insert loop

DECLARE @currentID int = NULL

DECLARE THIS_CURSOR CURSOR FAST_FORWARD FOR
    SELECT ID FROM #OldTable ORDER BY ID

OPEN THIS_CURSOR
FETCH NEXT FROM THIS_CURSOR INTO @currentID

WHILE @@FETCH_STATUS = 0
BEGIN
    BEGIN TRY
        --Perform insert
        INSERT INTO #NewTable
        OUTPUT @currentID, INSERTED.ID, 'Insert successful' INTO #LoggingTable (OldTableID, NewTableID, InsertStatus)
        SELECT CAST(OldValue AS int) FROM #OldTable WHERE ID = @currentID

        FETCH NEXT FROM THIS_CURSOR INTO @currentID
    END TRY
    BEGIN CATCH
        INSERT INTO #LoggingTable (OldTableID, NewTableID, InsertStatus) VALUES (@currentID, NULL, 'Error occurred during insert operation')
        FETCH NEXT FROM THIS_CURSOR INTO @currentID
    END CATCH   
END

close THIS_CURSOR
deallocate THIS_CURSOR

SELECT * FROM #OldTable
SELECT * FROM #NewTable
SELECT * FROM #LoggingTable