触发器适用于 UPDATE/INSERT,但在通过 BCP 运行时失败(日期格式问题)

Posted

技术标签:

【中文标题】触发器适用于 UPDATE/INSERT,但在通过 BCP 运行时失败(日期格式问题)【英文标题】:Trigger works on UPDATE/INSERT but fails when run through BCP (date format issue) 【发布时间】:2021-10-19 09:45:28 【问题描述】:

我有一个非常简单的表格 - 四列:

CREATE TABLE [dbo].[SampleTable]
(
    [item]   VARCHAR(32)   NOT NULL,
    [symbol] VARCHAR(64)   NOT NULL,
    [date]   DATE          NOT NULL,
    [value]  NVARCHAR(255) NOT NULL,

    CONSTRAINT [PK_SampleTable] 
        PRIMARY KEY CLUSTERED ([symbol] ASC, [item] ASC, [date] DESC)
)

为了更新表格,我使用了视图和触发器:

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT [item], [symbol], [date], [value]
    FROM [dbo].[SampleTable]
GO

CREATE TRIGGER [dbo].[SampleTable_in_trig] 
ON [dbo].[SampleTable_in]
/*
    Trigger on the insert view to facilitate:
    1. Insertions into the table overwrite (update) any records that already exist
    2. Sparse value updates (if the last value in the series is the same, value is not inserted)
    3. Insertions where value is null are considered DELETES
*/
INSTEAD OF INSERT, UPDATE
AS
    SET NOCOUNT, XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION
            /* When inserted symbol, item, date exists and values don't match, update */
            UPDATE u
            SET [value] = i.[value]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = i.[date]
            WHERE i.[value] IS NOT NULL
              AND i.[value] != u.[value]

            /* When inserted symbol, item, date exists and inserted value is null, delete */
            DELETE [dbo].[SampleTable]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = i.[date]
            WHERE i.[value] IS NULL

            /* When inserted symbol, item does not exist and inserted value is not null, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], i.[date], i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                WHERE 
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL

            /* When inserted symbol, item, date does not exist and value from prior date does not match inserted value, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], i.[date], i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                                          AND u.[date] = i.[date]
                WHERE
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL
                    AND i.[value] != (SELECT TOP 1 [value] 
                                      FROM [dbo].[SampleTable] uu 
                                      WHERE uu.[symbol] = i.[symbol] 
                                        AND uu.[item] = i.[item] 
                                        AND uu.[date] < i.[date] 
                                      ORDER BY [date] DESC)

        COMMIT TRANSACTION
    END TRY

    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK
        EXEC proc_error_handler
    END CATCH
GO

我的输入数据以 TSV 格式文件提供,以 &lt;item&gt;\t&lt;symbol&gt;\t&lt;date&gt;\t&lt;value&gt; 格式提供,日期以 YYYYMMDD 格式提供。当我尝试 bcp 直接进入视图(启用 FIRE_TRIGGERS)时,我无法更改输入格式并且 SQL 正在提高 Invalid character value for cast specification

如果我将基础表中的日期格式从 DATE 更改为 SMALLDATETIME,bcp 的工作方式与预期完全一样,但我的理解是这种旧格式不应该用于新工作(我想避免出于各种原因进行此更改)。

同样,如果我将输入日期格式更改为 YYYY-MM-DD,这将按预期工作,但是正如所解释的,我无法更改源数据格式,并且必须在此处构建一个额外的处理步骤来执行此操作

我尝试更改视图,使用 CASTCONVERT 将日期字段格式化为 SMALLDATETIME 和常规 INSERT 工作正常,但 bcp 仍然不满意,现在使用触发器扔一个Cannot insert the value NULL into column 'date'

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT [item], [symbol], CAST([date] AS SMALLDATETIME) AS [date], [value]
    FROM [dbo].[SampleTable]
GO

进一步深入研究,当我创建该视图并使用触发器将输入重定向到所有列设置为 NULL 的表时,日期将替换为输入上的 NULL(我假设进入 SQL 之前的 bcp 进程)

除了更改目标表或输入数据之外,是否有人对如何解决此问题有任何想法?

编辑:我使用的 bcp 命令是bcp TestDB.dbo.SampleTable_in in test_file.tsv -c -b 50000 -T -h FIRE_TRIGGERS -k

数据匹配这个小样本(但比这更多的行):

SOURCE  M01 20210813    FOO
SOURCE  M02 20210813    FOO
SYMBOL  M01 20210813    M01
SYMBOL  M02 20210813    M02
DESC    M01 20210813    A short description
DESC    M02 20210813    Some other desc

当我更改视图以更改日期字段的格式(CASTCONVERTFORMAT)时,日期字段似乎仅作为 NULL 值发送

【问题讨论】:

理想情况下,您应该更改传入的数据。也许创建一个 new 视图来放置instead of 触发器,这个应该有date 作为varchar ,那么您可以在触发器内进行转换 是什么让你认为错误来自触发器?这是一个 OLE DB 错误,在这种情况下,它们通常来自 BCP 而不是来自任何触发器。 @RBarryYoung - 完整的错误文本是 Error = [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]*** [SampleTable_in_trig],第 43 行。Errno 515:无法插入值NULL 到列“日期”,表“TestDB.dbo.SampleTable”;列不允许空值。插入失败。 SQLState = 37000, NativeError = 3609 错误 = [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]事务在触发器中结束。该批次已中止。 【参考方案1】:

使用输入源和您的 BCP 命令,我无法对此进行测试,但我会尝试以下方法:

首先,将您的视图定义更改为:

CREATE VIEW [dbo].[SampleTable_in]
AS
    SELECT  [item], 
            [symbol], 
            CONVERT(NVARCHAR(8), [date], 112) AS [date],
            [value]
    FROM [dbo].[SampleTable]
GO

然后把触发器定义改成这样:

CREATE TRIGGER [dbo].[SampleTable_in_trig] 
ON [dbo].[SampleTable_in]
/*
    Trigger on the insert view to facilitate:
    1. Insertions into the table overwrite (update) any records that already exist
    2. Sparse value updates (if the last value in the series is the same, value is not inserted)
    3. Insertions where value is null are considered DELETES
*/
INSTEAD OF INSERT, UPDATE
AS
    SET NOCOUNT, XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION
            /* When inserted symbol, item, date exists and values don't match, update */
            UPDATE u
            SET [value] = i.[value]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = CONVERT(DATE, i.[date], 112)
            WHERE i.[value] IS NOT NULL
              AND i.[value] != u.[value]

            /* When inserted symbol, item, date exists and inserted value is null, delete */
            DELETE [dbo].[SampleTable]
            FROM [dbo].[SampleTable] u
            INNER JOIN INSERTED i ON u.[symbol] = i.[symbol]
                                  AND u.[item] = i.[item]
                                  AND u.[date] = CONVERT(DATE, i.[date], 112)
            WHERE i.[value] IS NULL

            /* When inserted symbol, item does not exist and inserted value is not null, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], CONVERT(DATE, i.[date], 112), i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                WHERE 
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL

            /* When inserted symbol, item, date does not exist and value from prior date does not match inserted value, insert */
            INSERT INTO [dbo].[SampleTable] ([item], [symbol], [date], [value])
                SELECT
                    i.[item], i.[symbol], CONVERT(DATE, i.[date], 112), i.[value]
                FROM
                    INSERTED i
                LEFT OUTER JOIN 
                    [dbo].[SampleTable] u ON u.[symbol] = i.[symbol]
                                          AND u.[item] = i.[item]
                                          AND u.[date] = CONVERT(DATE, i.[date], 112)
                WHERE
                    i.[value] IS NOT NULL
                    AND u.[symbol] IS NULL
                    AND i.[value] != (SELECT TOP 1 [value] 
                                      FROM [dbo].[SampleTable] uu 
                                      WHERE uu.[symbol] = i.[symbol] 
                                        AND uu.[item] = i.[item] 
                                        AND uu.[date] < CONVERT(DATE, i.[date], 112)
                                      ORDER BY [date] DESC)

        COMMIT TRANSACTION
    END TRY

    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK
        --EXEC proc_error_handler
    END CATCH
GO

总而言之,我认为这不是您的触发器的问题,而是您的 BCP 命令和/或视图定义的问题。简而言之,BCP 无法弄清楚如何将 YYYYMMDD 输入字符串转换为日期数据类型。简单的答案是从 BCP 中解决这个问题,让触发器解决它。


根据 OP 的反馈,这也将 NULL 放在日期列中。

因此,我必须得出结论,问题是 BCP 在我们修改它时不会写入 [date] 列,因为它看起来是派生的,因此在视图中是不可写的列(请参阅规则可写视图)没有后备存储。一方面,使用像这样的触发器来创建直写视图是最先进的 T-SQL,并且它对于这样的案例/条件的行为并不总是有很好的文档记录。另一方面,BCP 是 SQL Server 中最古老的工具之一,它使用 SQL Server 的(不再记录)特殊接口,该接口并不总是遵循类似 INSERT 的规则。

总而言之,我只想说,我认为不可能有一种方法可以让这种方法按照您想要的方式工作。所以在这一点上,我认为你唯一可靠的做法是放弃使用触发器的直写视图方法,而是使用临时表。

我会这样定义临时表:

CREATE TABLE [dbo].[SampleTable_in]
(
    [item]   VARCHAR(32)   NOT NULL,
    [symbol] VARCHAR(64)   NOT NULL,
    [date]   NVARCHAR(8)   NOT NULL,
    [value]  NVARCHAR(255) NOT NULL
)

这将替换您的视图,您可以对其应用(修改的)触发器,或者将它们的逻辑合并到 BCP 完成后运行的存储过程中。

【讨论】:

感谢您的建议。不幸的是,这不起作用 - 当我进行这些更改时,错误会从无法转换日期变为无法将 NULL 值插入日期字段。如果我将输出重定向到允许在所有列中使用 NULL 的表,则 列都会正确填充,但 列是 NULL @SimonPratt 您确定有问题的行没有 NULL/缺失日期字段吗? @SimonPratt 然后问题几乎可以肯定是 BCP 不会写入它认为是计算列的内容,因为(根据可写视图规则)它没有后备存储列来写入它到。您已经深入了解未记录的功能,在这一点上,我认为您唯一可靠的选择是放弃视图并改用临时表。 啊,我希望避免这种情况 :) 感谢您的反馈,看看是否有人有任何其他想法,然后重新创建临时表或更改基础表 我使用您对视图的评论并使用临时表通过添加类型表并更新视图以将 [date] 列替换为 varchar 列来制作解决方案。这很好用 - 您想将您的评论扩展为我可以标记为答案的回复吗?

以上是关于触发器适用于 UPDATE/INSERT,但在通过 BCP 运行时失败(日期格式问题)的主要内容,如果未能解决你的问题,请参考以下文章

oracle中的trigger有几种啊

触发器

关于sql触发器调试问题

适用于 Windows 10 的通用应用程序。如何触发后台任务?

sybase创建触发器

SqlServer触发器的基础知识