在触发器

时间:2016-06-08 21:22:35

标签: c# sql-server exception-handling triggers sqlclr

我整天都陷入了这个问题,似乎无法在网上找到任何可能导致它的原因。

我在Logger类中有以下日志记录方法,以下代码调用记录器。当没有异常发生时,所有日志语句都能正常工作,但是当发生异常时,日志语句根本不运行(但它们确实从Web服务调用运行)。

记录器记录方法:

    public static Guid WriteToSLXLog(string ascendId, string masterDataId, string masterDataType, int? status,
        string request, string requestRecieved, Exception ex, bool isError)
    {
        var connection = ConfigurationManager.ConnectionStrings["AscendConnectionString"];

        string connectionString = "context connection=true";

        // define INSERT query with parameters
        var query =
            "INSERT INTO " + AscendTable.SmartLogixLogDataTableName +
            " (LogID, LogDate, AscendId, MasterDataId, MasterDataType, Status, Details, Request, RequestRecieved, StackTrace, IsError) " +
            "VALUES (@LogID, @LogDate, @AscendId, @MasterDataId, @MasterDataType, @Status, @Details, @Request, @RequestRecieved, @StackTrace, @IsError)";

        var logId = Guid.NewGuid();

        using (var cn = new SqlConnection(connectionString))
        {
            if (!cn.State.Equals(ConnectionState.Open))
            {
                cn.Open();
            }
            // create command
            using (var cmd = new SqlCommand(query, cn))
            {
                try
                {
                    // define parameters and their values
                    cmd.Parameters.Add("@LogID", SqlDbType.UniqueIdentifier).Value = logId;
                    cmd.Parameters.Add("@LogDate", SqlDbType.DateTime).Value = DateTime.Now;
                    if (ascendId != null)
                    {
                        cmd.Parameters.Add("@AscendId", SqlDbType.VarChar, 24).Value = ascendId;
                    }
                    else
                    {
                        cmd.Parameters.Add("@AscendId", SqlDbType.VarChar, 24).Value = DBNull.Value;
                    }
                    cmd.Parameters.Add("@MasterDataId", SqlDbType.VarChar, 50).Value = masterDataId;
                    cmd.Parameters.Add("@MasterDataType", SqlDbType.VarChar, 50).Value = masterDataType;

                    if (ex == null)
                    {
                        cmd.Parameters.Add("@Status", SqlDbType.VarChar, 50).Value = status.ToString();
                    }
                    else
                    {
                        cmd.Parameters.Add("@Status", SqlDbType.VarChar, 50).Value = "2";
                    }

                    if (ex != null)
                    {
                        cmd.Parameters.Add("@Details", SqlDbType.VarChar, -1).Value = ex.Message;
                        if (ex.StackTrace != null)
                        {
                            cmd.Parameters.Add("@StackTrace", SqlDbType.VarChar, -1).Value =
                                ex.StackTrace;
                        }
                        else
                        {
                            cmd.Parameters.Add("@StackTrace", SqlDbType.VarChar, -1).Value = DBNull.Value;
                        }
                    }
                    else
                    {
                        cmd.Parameters.Add("@Details", SqlDbType.VarChar, -1).Value = "Success";
                        cmd.Parameters.Add("@StackTrace", SqlDbType.VarChar, -1).Value = DBNull.Value;
                    }

                    if (!string.IsNullOrEmpty(request))
                    {
                        cmd.Parameters.Add("@Request", SqlDbType.VarChar, -1).Value = request;
                    }
                    else
                    {
                        cmd.Parameters.Add("@Request", SqlDbType.VarChar, -1).Value = DBNull.Value;
                    }

                    if (!string.IsNullOrEmpty(requestRecieved))
                    {
                        cmd.Parameters.Add("@RequestRecieved", SqlDbType.VarChar, -1).Value = requestRecieved;
                    }
                    else
                    {
                        cmd.Parameters.Add("@RequestRecieved", SqlDbType.VarChar, -1).Value = DBNull.Value;
                    }

                    if (isError)
                    {
                        cmd.Parameters.Add("@IsError", SqlDbType.Bit).Value = 1;
                    }
                    else
                    {
                        cmd.Parameters.Add("@IsError", SqlDbType.Bit).Value = 0;
                    }

                    // open connection, execute INSERT, close connection
                    cmd.ExecuteNonQuery();
                }
                catch (Exception e)
                {
                    // Do not want to throw an error if something goes wrong logging
                }
            }
        }

        return logId;
    }

发生日志记录问题的方法:

public static void CallInsertTruckService(string id, string code, string vinNumber, string licPlateNo)
    {
        Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "1", "", null, false);
        try
        {
            var truckList = new TruckList();
            var truck = new Truck();

            truck.TruckId = code;

            if (!string.IsNullOrEmpty(vinNumber))
            {
                truck.VIN = vinNumber;
            }
            else
            {
                truck.VIN = "";
            }

            if (!string.IsNullOrEmpty(licPlateNo))
            {
                truck.Tag = licPlateNo;
            }
            else
            {
                truck.Tag = "";
            }

            if (!string.IsNullOrEmpty(code))
            {
                truck.BackOfficeTruckId = code;
            }

            truckList.Add(truck);

            Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "2", "", null, false);

            if (truckList.Any())
            {
                // Call SLX web service
                using (var client = new WebClient())
                {
                    var uri = SmartLogixConstants.LocalSmartLogixIntUrl;


                    uri += "SmartLogixApi/PushTruck";

                    client.Headers.Clear();
                    client.Headers.Add("content-type", "application/json");
                    client.Headers.Add("FirestreamSecretToken", SmartLogixConstants.FirestreamSecretToken);

                    var serialisedData = JsonConvert.SerializeObject(truckList, new JsonSerializerSettings
                    {
                        ReferenceLoopHandling = ReferenceLoopHandling.Serialize
                    });

                    // HTTP POST
                    var response = client.UploadString(uri, serialisedData);

                    var result = JsonConvert.DeserializeObject<SmartLogixResponse>(response);

                    Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "3", "", null, false);

                    if (result == null || result.ResponseStatus != 1)
                    {
                        // Something went wrong
                        throw new ApplicationException("Error in SLX");
                    }

                    Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, result.ResponseStatus, serialisedData,
                        null, null, false);
                }
            }
        }
        catch (Exception ex)
        {
            Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "4", "", null, false);
            throw;
        }
        finally
        {
            Logger.WriteToSLXLog(id, code, MasterDataType.TruckType, 4, "5", "", null, false);
        }
    }

如您所见,我在整个方法中添加了几个日志语句。如果没有抛出异常,那么除了catch块中的那些之外的所有这些日志语句都是成功的。如果抛出异常,那么它们都不会成功。对于他们中的大多数,无论是否存在异常,值都完全相同,因此我知道它不是传递的值的问题。我在想一些奇怪的事情正在发生导致回滚或其他什么,但我没有在这里使用交易或任何东西。最后一件事是这个DLL通过SQL CLR运行,这就是我使用&#34; context connection = true&#34;为我的连接字符串。

提前致谢。

编辑:

我尝试添加以下内容作为我的连接字符串但是在尝试时我得到了一个异常。现在打开连接,说明&#34;另一个会话使用的事务上下文&#34;。我认为这与我通过触发器调用此SQL CLR过程有关。我试过的连接字符串是

 connectionString = "Trusted_Connection=true;Data Source=(local)\\AARONSQLSERVER;Initial Catalog=Demo409;Integrated Security=True;";  

此处还有触发器:

CREATE TRIGGER [dbo].[PushToSLXOnVehicleInsert]
   ON  [dbo].[Vehicle] AFTER INSERT
AS 
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE @returnValue int
DECLARE @newLastModifiedDate datetime = null
DECLARE @currentId bigint = null
DECLARE @counter int = 0;
DECLARE @maxCounter int
DECLARE @currentCode varchar(24) = null
DECLARE @currentVinNumber varchar(24)
DECLARE @currentLicPlateNo varchar(30)
declare @tmp table
(
  id int not null
  primary key(id)
)

insert @tmp
select VehicleID from INSERTED

SELECT @maxCounter = Count(*) FROM INSERTED GROUP BY VehicleID

BEGIN TRY
WHILE (@counter < @maxCounter)
BEGIN       
    select top 1 @currentId = id from @tmp

    SELECT @currentCode = Code, @currentVinNumber = VINNumber, @currentLicPlateNo = LicPlateNo FROM INSERTED WHERE INSERTED.VehicleID = @currentId

    if (@currentId is not null)
    BEGIN
        EXEC dbo.SLX_CallInsertTruckService
            @id = @currentId,
            @code = @currentCode,
            @vinNumber = @currentVinNumber,
            @licPlateNo = @currentLicPlateNo
    END

    delete from @tmp where id = @currentId

    set @counter = @counter + 1;
END
END TRY
BEGIN CATCH
    DECLARE @ErrorMessage NVARCHAR(4000);
    DECLARE @ErrorSeverity INT;
    DECLARE @ErrorState INT;

    SELECT 
        @ErrorMessage = ERROR_MESSAGE(),
        @ErrorSeverity = ERROR_SEVERITY(),
        @ErrorState = ERROR_STATE();
    IF (@ErrorMessage like '%Error in SLX%')
    BEGIN
        SET @ErrorMessage = 'Error in SLX.  Please contact SLX for more information.'
    END
    RAISERROR (@ErrorMessage, -- Message text.
               @ErrorSeverity, -- Severity.
               @ErrorState -- State.
               );
END CATCH;
END
GO

1 个答案:

答案 0 :(得分:1)

这里的主要问题是从触发器内调用SQLCLR存储过程。触发器始终在事务的上下文中运行(将其绑定到启动触发器的DML操作)。触发器还隐式将XACT_ABORT设置为ON,如果发生任何错误,则取消事务。这就是抛出异常时没有任何日志记录语句持续存在的原因:事务是自动回滚的,带有在同一个Session中所做的任何更改,包括日志记录语句(因为Context Connection是相同的Session),以及原始的DML语句。

你有三个相当简单的选择,虽然它们会给你带来整体架构问题,或者是一个不那么困难但又多一点工作的选项,可以解决当前的问题以及更大的架构问题。首先,三个简单的选择:

  1. 您可以在触发器的开头执行SET XACT_ABORT OFF;。这将允许TRY ... CATCH构造按预期工作。但是,这也会将责任转移到您发出ROLLBACK(通常在CATCH块中),除非您希望原始DML语句无论如何都能成功,即使Web服务调用和日志记录失败也是如此。当然,如果您发出ROLLBACK,那么即使Web服务仍然注册了所有成功的调用(如果有的话),也不会持续存在任何日志记录语句。

  2. 您可以单独留下SET XACT_ABORT并使用与SQL Server的常规/外部连接。常规连接将是完全独立的连接和会话,因此它可以独立于事务进行操作。与SET XACT_ABORT OFF;选项不同,这将允许触发器正常运行&#34;通常&#34; (即任何错误都会回滚在Trigger和原始DML语句中本地进行的任何更改),同时仍允许日志记录INSERT语句持久存在(因为它们是在本地事务之外进行的)。 p>

    您已经在调用Web服务,因此程序集已具备执行此操作所需的权限,而无需进行任何其他更改。您只需要使用正确的connection string(语法中有一些错误),可能类似于以下内容:

    connectionString = @"Trusted_Connection=True; Server=(local)\AARONSQLSERVER; Database=Demo409; Enlist=False;"; 
    

    "Enlist=False;"部分(滚动到最右侧)非常重要:没有它,您将继续获得另一个会话使用的&#34; 事务上下文&#34 ;错误。

  3. 如果您想坚持使用Context Connection(速度稍快一点)并允许 之外的任何错误回滚原始DML语句所有日志记录语句,同时忽略来自Web服务的错误,甚至忽略日志INSERT语句中的错误,那么您就不能在catch块中重新抛出异常CallInsertTruckService。您可以改为设置变量以指示返回代码。由于这是一个存储过程,因此它可以返回SqlInt32而不是void。然后,您可以通过声明INT变量并将其包含在EXEC调用中来获取该值,如下所示:

    EXEC @ReturnCode = dbo.SLX_CallInsertTruckService ...;
    

    只需在CallInsertTruckService的顶部声明一个变量,然后将其初始化为0。然后将其设置为catch块中的其他值。在方法结束时,请添加return _ReturnCode;

  4. 话虽如此,无论你选择哪一种选择,你仍然会遇到两个相当大的问题:

    1. Web服务调用阻止DML语句及其系统启动的事务。交易将保持开放的时间超过应有的时间,这至少可以增加与Vehicle表相关的阻塞。虽然我肯定是通过SQLCLR进行Web服务调用的倡导者,但我强烈建议不要在触发器中这样做。

    2. 如果插入的每个VehicleID都应该传递给Web服务,那么如果一个Web服务调用中出现错误,将跳过剩余的VehicleID,并且即使它们不是(上面的选项#3会继续处理@tmp中的行),但至少那个刚刚发生错误的行将不会在以后重试。

    3. 因此,解决这两个相当重要的问题以及初始日志记录问题的理想方法是转移到断开连接的异步模型。您可以设置一个队列表来保存要根据每个INSERT处理的Vehile信息。 Trigger会做一个简单的事情:

      INSERT INTO dbo.PushToSLXQueue (VehicleID, Code, VINNumber, LicPlateNo)
        SELECT VehicleID, Code, VINNumber, LicPlateNo
        FROM   INSERTED;
      

      然后创建一个Stored Procedure,它从队列表中读取一个项目,调用Web Service,如果成功,则从队列表中删除该条目。从SQL Server代理作业安排此存储过程每10分钟或类似情况运行一次。

      如果有永远不会处理的记录,那么您可以在队列表中添加RetryCount列,默认为0,在Web服务收到错误时,增加RetryCount而不是删除行。然后你可以更新&#34; get条目来处理&#34; SELECT查询以包含WHERE RetryCount < 5或您想要设置的任何限制。

      这里有一些问题,影响程度各不相同:

      1. 为什么id是T-SQL代码中的BIGINT,而C#代码中却是一个字符串?

      2. 仅使用FYI,与使用实际WHILE (@counter < @maxCounter)相比,CURSOR循环效率低且容易出错。我将摆脱@tmp表变量和@maxCounter

        至少将SELECT @maxCounter = Count(*) FROM INSERTED GROUP BY VehicleID更改为SET @maxCounter = @@ROWCOUNT; ;-)。但换一个真正的CURSOR将是最好的。

      3. 如果CallInsertTruckService(string id, string code, string vinNumber, string licPlateNo)签名是用[SqlProcedure()]修饰的实际方法,那么您确实应该使用SqlString而不是string。使用string参数的.Value属性从每个参数中获取本机SqlString值。然后,您可以使用[SqlFacet()]属性设置适当的大小,如下所示:

        [SqlFacet(MaxSize=24)] SqlString vinNumber
        
      4. 有关使用SQLCLR的更多信息,请参阅SQL Server Central上关于此主题的系列文章:Stairway to SQLCLR(阅读该网站上的内容需要免费注册)。