MS SQL Server:触发器失败,因为存储过程失败

时间:2020-04-05 11:16:33

标签: sql-server triggers

我有一个应用程序,可将多达20个设备条形码数据发送到SQL-Server表“设备使用情况”。

在该表的AFTER INSERT触发器中,我有一个存储过程,该过程将条形码从BC1,BC2,BC3等列传输到另一个表BC usage中。

BC usage设置为IGNORE_DUP_KEY = ON,因为可能会对BC进行两次扫描。

如果扫描了重复的BC,则第一个表“设备使用情况”中的插入也会失败!

为什么?

尽管表IGNORE_DUP_KEY = ON中的BC usage仅在存储过程中使用,但我不明白为什么初始插入表“设备使用情况”会失败。

ALTER TRIGGER [dbo].[UpdatePersonanenDaten] 
ON [dbo].[ScanIT_tblGeraeteerfassung] 
AFTER INSERT
AS 
BEGIN
    DECLARE @Personalnummer varchar(10)
    DECLARE @Mitarbeiter varchar(100)
    DECLARE @InOut char(1)=''
    DECLARE @ID int
    DECLARE @StartID int

    SELECT 
        @Personalnummer = a.[Personalnummer], 
        @Mitarbeiter = a.[DeviceUser] 
    FROM 
        [dbo].[ScanIT_tblDevices] a 
    INNER JOIN 
        inserted i ON a.[DeviceID] = i.[DeviceID]

    SELECT @ID = (SELECT ID FROM inserted)

    BEGIN 
        UPDATE [dbo].[ScanIT_tblGeraeteerfassung] 
        SET [Mitarbeiter] = @Mitarbeiter,
            [Personalnummer] = @Personalnummer,
            [InOut] = CASE 
                         WHEN t.[AufAbbau] = 'Aufbau (Start)' 
                            THEN 'S' 
                            ELSE 'E' 
                      END
        FROM ScanIT_tblGeraeteerfassung t
        INNER JOIN inserted i ON t.ID = i.ID
    END

    -- IN THIS SP THE INSERT INTO tbl "BC USAGE"  HAPPENS  and the Dup Key error let the trigger fail!!

    EXEC [dbo].[ScanIT_spGeraeteEinsatz1] @ID

感谢您的帮助

迈克尔

编辑:

这是存储过程ScanIT_spGeraeteEinsatz1 在此之前,我曾试图简单地描述整个工作,但实际上,发送到服务器表的BC后面是设备的计数器读数。 因此,它需要查找此BC之前该记录的最后一个计数器读数。 S代表开始时间,E代表设备使用时间的结束时间。 可能还会出现一个问题,那就是该设备之前从未在客户端使用过,然后我们从盘点中取出计数器,将初始计数器设置为Start。

现在,如果有人扫描此设备BC两次,则必须将其阻止,因为该BC的第一次出现是强制性的,因此必须以“开始”作为起点,而下一次出现的则是“结束”。即使有人错误地将BC添加到了“开始”记录中,尽管该设备之前已经在另一个记录中进行了扫描。

我希望我能解释一下您了解整个过程的工作。

ALTER PROCEDURE [dbo].[ScanIT_spGeraeteEinsatz1]
    @GeraeteerfassungID int

AS
BEGIN

BEGIN TRANSACTION;   --neu
SAVE TRANSACTION MySavePoint;

    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    declare @tablename as varchar(255)   -- tbl, aus der ausglesen wird: 'ScanIT_tblGeraeteerfassung'
    declare @temptable table (BarcodeName varchar(100))    -- im @temptable werden die Feldnamen Geraetebarcode1, Zaehlerstand1, ... eingelesen
    declare @sqlDynamicString nvarchar(400)
    declare @sqlDynamicString2 nvarchar(400)
    declare @NumberBC int, @Counter int
    declare @temptable2 table (Barcode varchar(10), Zaehlerstand varchar(20))
    declare @strCounter varchar(2)
    declare @Geraetebarcode int

    declare @ID int
    declare @L_ID int

    declare @L_InOut char(1)


    declare @StartID int
    declare @GeraeteerfassungIDOUT int
    --declare @Einsatzvon datetime

    Declare @StartZaehlerstand varchar(25)
    Declare @StartDatum datetime

    declare @BC_AnzahlVerwendungen int=0

    DECLARE @LastDS TABLE
            ([ID] [int],
            [GeraeteerfassungIDOUT] [int] ,
            [GeraeteerfassungIDIN] [int] ,
            [Projektnummer] [varchar](20) ,
            [Geraetebarcode] [varchar](100) ,
            [ZaehlerstandOUT] [varchar](50) ,
            [ZaehlerstandIN] [varchar](50) ,
            [Einsatzvon] [datetime] ,
            [Einsatzbis] [datetime] ,
            [InOut] [char](1) ,
            [PaarID] [int] )


    set @tablename = 'ScanIT_tblGeraeteerfassung'
    set @Counter=1

    -- Einsatzvon-Datum/Zeit un d Projektnummer bleiben für alle Datensätze gleich
    --set @Projektnummer=(Select Arbeitsauftrag from [dbo].[ScanIT_tblGeraeteerfassung] where [ID]=@GeraeteerfassungsID);
    --set @Einsatzvon=(Select Sendtime from [dbo].[ScanIT_tblGeraeteerfassung] where [ID]=@GeraeteerfassungsID);
    --set @L_InOut=(select [InOut] from [dbo].[ScanIT_tblGeraeteerfassung] where [ID]=@GeraeteerfassungsID))   

    --select @Projektnummer=Arbeitsauftrag, @Einsatzvon=Sendtime, @L_InOut=InOut from [dbo].[ScanIT_tblGeraeteerfassung] where [ID]=@GeraeteerfassungsID


    --Insert into @temptable (BarcodeName)
    --wie viele Gerätefelder gibt es in dem SCAN-IT Formular bzw. in der Einlesetabelle?
    set @NumberBC=(SELECT count(c.name) FROM sys.columns c WHERE c.object_id = OBJECT_ID(@tablename) and  c.name like 'Geraetebarcode%' ) ;

    -- hier beginnt der Loop NEU    
        while(@Counter <= @NumberBC)
        begin
                    set @strCounter=convert(varchar(2),@Counter)

                    set @sqlDynamicString2 = 'SET @GeraeteBC=(SELECT Geraetebarcode' + @strCounter + ' FROM [dbo].[ScanIT_tblGeraeteerfassung] WHERE ID=' + convert(nvarchar(8),@GeraeteerfassungsID) + ' AND Len(Geraetebarcode' + @strCounter + ')>0)' 

                    EXECUTE sp_executesql @sqlDynamicString2, N'@GeraeteBC nvarchar(8) OUTPUT',@GeraeteBC=@Geraetebarcode OUTPUT

                    --zuerst muß einmal geschaut werden, ob dieses Gerät überhaupt schon mal im Einsatz war, daher wird die [dbo].[ScanIT_tblGeraeteeinsatz] ausgezählt
                    --bei ERSTMALIGER Verwendung ist die Anzahl 0
                    Set @BC_AnzahlVerwendungen=(Select Count(*) from [dbo].[ScanIT_tblGeraeteeinsatz] where [Geraetebarcode]=@Geraetebarcode)

                    print @Geraetebarcode
                    print @BC_AnzahlVerwendungen

                    -- es wird vorerst der Zählerstand, die Sendtime und die ID für in/out eingegeben, weil erst danach bewertet wird, ob das ein IN oder OUT Datensatz ist
                    set @sqlDynamicString=concat('SELECT  ID, ID ,', @Projektnummer, ', Geraetebarcode' + @strCounter, ', Zaehlerstand' + @strCounter, ', Zaehlerstand' + @strCounter, ', Sendtime',', Sendtime',', InOut',
                                                 ' FROM [dbo].[ScanIT_tblGeraeteerfassung] WHERE ID= ', @GeraeteerfassungsID, ' AND Len(Geraetebarcode' + @strCounter, ')>0' )

                    print @sqlDynamicString

                    BEGIN TRY    --neu
                    Insert into [dbo].[ScanIT_tblGeraeteeinsatz] ([GeraeteerfassungIDOUT],
                                                                  [GeraeteerfassungIDIN],
                                                                  [Projektnummer],
                                                                  [Geraetebarcode],
                                                                  [ZaehlerstandOUT],
                                                                  [ZaehlerstandIN],  
                                                                  [Einsatzvon],
                                                                  [Einsatzbis],
                                                                  [InOut])
                    exec (@sqlDynamicString)

                    COMMIT TRANSACTION 
                    END TRY
                        BEGIN CATCH
                            IF @@TRANCOUNT > 0
                            BEGIN
                                ROLLBACK TRANSACTION MySavePoint; -- rollback to MySavePoint
                            END
                        END CATCH


                    --die ID des eben eingegebenen Datensatzes
                    set @ID=SCOPE_IDENTITY() 


                    -- nun wird geschaut, ob der Barcode schon einmal vokam
                    -- Gerät war noch NIE im Einsatz, d.h. den Startzählerwert aus der [dbo].[Lager_tblInventarArtikel] suchen
                        if @BC_AnzahlVerwendungen = 0   

                            begin           

                                Update a  set   --a.InOut= 'S', 
                                                a.[ZaehlerstandOUT]=coalesce(a.[ZaehlerstandOUT],b.[Anfangsstand]),  --zuerste wird geschaut, ob ein Zählerstand angliefert wird, wenn nicht wird im Lagerstand geschaut
                                                a.[ZaehlerstandIN]='', 
                                                a.[Einsatzbis]=NULL, 
                                                a.[GeraeteerfassungIDIN]= NULL,
                                                a.[PaarID]=a.ID
                                    from [dbo].[ScanIT_tblGeraeteeinsatz] a 
                                    left join [dbo].[Lager_tblInventarArtikel] b on a.[Geraetebarcode]=b.[Barcode] 
                                    where a.ID=@ID

                            end         

                    --Gerät war schon im Einsatz, daher suchen aus [dbo].[ScanIT_tblGeraeteeinsatz]

                    if @BC_AnzahlVerwendungen > 0

                    begin           
                        --print @inout
                        --ID des Startdatensatzes suchen

                        print @Projektnummer
                        print @Einsatzvon 
                        print @L_InOut
                        --set @L_InOut=(select [InOut] from @LastDS)


                        --print @L_InOut


                        if @L_InOut='S'
                        --zuerst den zuvor gelieferten Datensatz suchen, um den Anfangstand zu setzen
                        begin
                                --Befüllen der Table Variablen mit dem vorletzten Datensatz 
                                INSERT INTO @LastDS 
                                            ([ID] ,
                                            [GeraeteerfassungIDOUT]  ,
                                            [GeraeteerfassungIDIN]  ,
                                            [Projektnummer]  ,
                                            [Geraetebarcode]  ,
                                            [ZaehlerstandOUT]  ,
                                            [ZaehlerstandIN]  ,
                                            [Einsatzvon]  ,
                                            [Einsatzbis]  ,
                                            [InOut]  ,
                                            [PaarID] ) 


                                SELECT Top 1 [ID] ,
                                            [GeraeteerfassungIDOUT]  ,
                                            [GeraeteerfassungIDIN]  ,
                                            [Projektnummer]  ,
                                            [Geraetebarcode]  ,
                                            [ZaehlerstandOUT]  ,
                                            [ZaehlerstandIN]  ,
                                            [Einsatzvon]  ,
                                            [Einsatzbis]  ,
                                            [InOut]  ,
                                            [PaarID]
                                from [dbo].[ScanIT_tblGeraeteeinsatz] 
                                where [Geraetebarcode]=@Geraetebarcode and ID < @ID order by ID desc 
                        end         

                        begin
                            Update a set  
                                     a.[GeraeteerfassungIDIN]=NULL  --weil GeraeteerfassungIDOUT existiert, braucht man sie nicht setzen
                                     ,a.[ZaehlerstandOut]= coalesce(a.[ZaehlerstandOut],x.[ZaehlerstandOUT])  --wenn ZaehlerstandOut abgelesen wurde, braucht man ihn nicht setzen, wenn nicht, dann aus letztem DS verwenden
                                     ,a.[ZaehlerstandIN]=''
                                    -- ,a.[Einsatzvon]=x.[Einsatzvon]               --weil Einsatzvon existiert, braucht man sie nicht setzen
                                     ,a.[Einsatzbis]= null
                                     ,a.[PaarID]=a.ID  
                            from [dbo].[ScanIT_tblGeraeteeinsatz] a 
                              left join  @LastDS x on a.[Geraetebarcode]=x.[Geraetebarcode]
                              where a.[ID]=@ID 
                        end

                        if @L_InOut='E'
                        --hier muß man nach dem letzten DS suchen, weil es einen S geben muß!!
                        --da diese Nummer schon verwendet wurde, kann man nach dem S Datensatz zu diesem Gerät suchen
                        begin
                                --Befüllen der Table Variablen mit dem vorletzten Datensatz 
                                INSERT INTO @LastDS 
                                            ([ID] ,
                                            [GeraeteerfassungIDOUT]  ,
                                            [GeraeteerfassungIDIN]  ,
                                            [Projektnummer]  ,
                                            [Geraetebarcode]  ,
                                            [ZaehlerstandOUT]  ,
                                            [ZaehlerstandIN]  ,
                                            [Einsatzvon]  ,
                                            [Einsatzbis]  ,
                                            [InOut]  ,
                                            [PaarID] ) 


                                SELECT Top 1 [ID] ,
                                            [GeraeteerfassungIDOUT]  ,
                                            [GeraeteerfassungIDIN]  ,
                                            [Projektnummer]  ,
                                            [Geraetebarcode]  ,
                                            [ZaehlerstandOUT]  ,
                                            [ZaehlerstandIN]  ,
                                            [Einsatzvon]  ,
                                            [Einsatzbis]  ,
                                            [InOut]  ,
                                            [PaarID]
                                from [dbo].[ScanIT_tblGeraeteeinsatz] 
                                where [Geraetebarcode]=@Geraetebarcode and ID < @ID order by ID desc 
                        end         


                        select * from @LastDS

                        begin
                        Update a set 
                                     a.[GeraeteerfassungIDIN]=x.[GeraeteerfassungIDOUT] 
                                     ,a.[ZaehlerstandOut]= x.[ZaehlerstandOut] 
                                     --,a.[ZaehlerstandIN]= NULL   --steht schon drinnen
                                     ,a.[Einsatzvon]=x.[Einsatzvon] 
                                     --,a.[Einsatzbis]= NULL        --steht schon drinnen
                                     ,a.[PaarID]=x.ID  
                                     ,a.[Verbrauch]= coalesce(cast(a.[ZaehlerstandIN] as float),0)-coalesce(cast(x.[ZaehlerstandOUT] as float),0) 
                                     ,a.[Standzeit]= datediff(d, coalesce(x.[Einsatzvon],0), @Einsatzvon)+1 

                              from [dbo].[ScanIT_tblGeraeteeinsatz] a 
                              left join  @LastDS x on a.[Geraetebarcode]=x.[Geraetebarcode]
                              where a.[ID]=@ID 
                        end

                    end

                    -- zuletzt @Counter hochzählen, dann loop
                    set @Geraetebarcode=null
                    set @BC_AnzahlVerwendungen=null 

                    delete from @LastDS --tablevariable leeren

                    set @Counter=@Counter+1

            end     

    --  Select @StartZaehlerstand, @StartDatum, @StartID,@InOut, @ID, @Counter

END

1 个答案:

答案 0 :(得分:1)

TL; DR :一个显而易见的问题是:为什么还要插入重复的键值?您应该解决问题的根源,而不是寻找无法解决任何问题并造成更多问题的解决方法。

如果不难检查表中是否已存在记录并跳过插入。换句话说,您应该从源头上确保数据的质量,而不是在清理之后对重复项进行排序。

简短的回答:没有容易解决的问题,因为您遇到的问题范围很广,我们也没有提供数据和环境的副本来提供深入的建议。 长答案:请参见下面的一些建议。


在没有对您的应用程序有全面了解的情况下,我仍然觉得该方法过时且不必要。将记录转移到另一个表是重复,是浪费的数据存储,如果触发器的编码不正确,则不可靠。

为什么不建立指向原始表视图(或存储过程)?我什至不认为您应该尝试修复此代码,而是简化代码。重新考虑您的数据模型和操作流程。

您的过程已经足够复杂,就我所知,您甚至拥有动态SQL 。它很少是合理的,因为它增加了一层复杂性,并且可能带来安全风险。

当您使事情变得比原本应该复杂的多时,代码将变得难以阅读,不那么可靠且难以调试。有太多的hoopla可以执行相对简单的基本操作。

我们没有您的数据副本,因此很难再现您的问题。如果您发布具有索引和约束的表结构,并且一些示例数据可能会更进一步。

另一个问题是您的代码中没有全面的错误处理。从现在开始,您应该开始这样做,以使代码更可靠,更健壮。 然后,当发生错误时,您可以捕获某些变量的内容并获得有关违规数据的更多信息。 例如,在这里看看:How to implement error handling in SQL Server。 仅10行代码就会有所作为。

您有一些交易,但这还不够。它们使用不当,没有涵盖整个过程。

使用触发器通常是一个不好的解决方案,尤其是当您已经具有存储过程来插入/更新数据时。如果可能,您应该将逻辑集中在一个地方。在错误的假设下,触发器经常被误解和错误编码。

如果您仍在阅读本文,现在这里有一些可能会影响您的潜在重要信息。我认为这是@smor所暗示的,但我会强调这一点。

插入触发器的一个鲜为人知的方面(至少在MSSQL中是),当您在一批中插入多行时,触发器将为整个操作调用一次,而不是为每行调用一次(重点是我的):< / p>

因为INSERT INTO(table_name)可以触发INSERT触发器 SELECT语句,插入许多行可能导致单个 触发调用。

来源:Create DML Triggers to Handle Multiple Rows of Data

所以这也可能是您的问题之一(这种问题通常在很长一段时间内都未被发现...)。


不幸的是,您的代码很难看懂,但是您可以做的事情花费很少,并带来一些好处:

  • 为变量和对象名称使用有意义的名称,以避免混淆并提高可读性。
  • 摆脱动态SQL-仅此一项将使代码更易于理解和理解
  • 改善代码的形状-制表很重要
  • 尽快添加错误处理-可能会发生许多错误,您可能不会注意到并破坏了数据的完整性
  • 从代码中删除未使用的内容

例如,在存储过程ScanIT_spGeraeteEinsatz1中,您定义了一个内存表:

declare @temptable table (BarcodeName varchar(100))

,但显然您并未真正使用它。 @temptable2不再使用或不再使用。只会带来混乱。

简而言之,现在是缩减代码长度的时候了。认真考虑您的需求并简化。当您可以避免问题时,请勿尝试解决问题。改进代码并自己编写文档,即使这需要花费时间。从长远来看,错误的代码很昂贵,因为当您必须解决错误和缺陷时,它很难维护并且会产生更多的工作。