【问题标题】:How do I rollback a SQL CLR stored procedure when a user cancels the query?当用户取消查询时,如何回滚 SQL CLR 存储过程?
【发布时间】:2017-12-05 16:25:07
【问题描述】:

我正在尝试创建一个 SQL CLR 存储过程,它将创建一个表,将表名传递给一个服务,该服务将向其中批量插入一些数据,显示表的结果,然后清理表。

到目前为止我已经尝试过:

  1. 使用SqlTransaction。取消事务有效,但它使我的查询窗口进入我无法继续处理它的状态。

    此会话中活动的事务已被另一个会话提交或中止

  2. 使用TransactionScope。与 1 相同的问题。

  3. 通过发出DROP TABLE SqlCommandfinally 子句中手动清理表。这似乎没有运行,尽管在发出命令之前我的SqlContext.Pipe.Send() 运行。它似乎与任何时间限制无关,因为如果我在打印另一行之前发出Thread.Sleep(2000),它仍然会打印第二行,而command.ExecuteNonQuery() 会在打印第二行之前停止。

  4. 将手动清理代码放入 CER 或 SafeHandle。这不起作用,因为拥有 CER 需要一些保证,包括不分配额外的内存或调用未用 ReliabilityContract 修饰的方法。

我在这里遗漏了一些明显的东西吗?如何处理用户取消查询?

编辑:每个场景的代码都有多次迭代,但采取的一般行动如下:

    [SqlProcedure]
    public static void GetData(SqlString code)
    {
        Guid guid = Guid.NewGuid();
        using (var connection = new SqlConnection("context connection=true"))
        {
            connection.Open();

            try
            {
                SqlContext.Pipe?.Send("Constrain");

                SqlCommand command1 = new SqlCommand($"CREATE TABLE qb.{code}_{guid:N} (Id INT)", connection);
                command1.ExecuteNonQuery();
                SqlContext.Pipe?.Send($"Create: qb.{code}_{guid:N}");

                //emulate service call
                Thread.Sleep(TimeSpan.FromSeconds(10));

                SqlContext.Pipe?.Send($"Done: qb.{code}_{guid:N}");
            }
            finally
            {
                SqlContext.Pipe?.Send("1");
                //drop table here instead of sleep
                Thread.Sleep(2000);
                SqlContext.Pipe?.Send("2");
            }
        }
    }

【问题讨论】:

  • 添加相关代码部分
  • 一个简单的答案是如果用户取消查询然后立即删除表。该服务在执行工作之前首先测试该表是否存在,如果不存在,则假定用户取消了查询,因此它什么也不做。
  • @Rob 问题是在用户取消查询后删除表。 finally 块不会像尝试 3 中提到的那样执行 DROP TABLE
  • 带有查询窗口的部分在哪里?
  • @SirRufo 我没有包括它,因为它只是一个电话EXEC clrproc 'parameter'

标签: c# sql-server sqlclr


【解决方案1】:

不幸的是,SQLCLR 不能很好地处理查询取消。但是,考虑到错误消息,这似乎暗示取消是自己的ROLLBACK。您是否尝试过不在 SQLCLR 代码中使用事务,而是从外部处理它?如:

  1. BEGIN TRAN;
  2. EXEC SQLCLR_Stored_Procedure;
  3. IF (@@TRANCOUNT > 0) ROLLBACK TRAN;

需要强制执行上述工作流程。这可以通过创建一个执行这 3 个步骤的包装 T-SQL 存储过程并且只授予包装存储过程EXECUTE 权限来轻松完成。如果 SQLCLR 存储过程需要权限,则可以使用模块签名相当容易地完成:

  1. 在与 SQLCLR 存储过程相同的数据库中创建非对称密钥
  2. 使用该非对称密钥创建用户
  3. GRANT SQLCLR 存储过程的基于密钥的用户 EXECUTE 权限
  4. 使用 ADD SIGNATURE 使用该非对称密钥对包装 T-SQL 存储过程进行签名

这 4 个步骤允许 T-SQL wrapper proc 执行 SQLCLR proc,而实际的应用程序 Login 只能执行 T-SQL wrapper proc :-)。而且,如果取消在执行ROLLBACK 之前中止执行,则在连接关闭时应自动回滚事务。

另外,您是否将XACT_ABORT 设置为ONOFF更新: O.P. 声明它设置为OFF,设置为ON 的行为似乎没有任何不同。

您是否尝试过检查finally 块中的连接状态?我很确定SqlConnection 在取消时是Closed。您可以在 finally 块中尝试以下方法:

  1. 测试连接状态,如果是Closed,重新打开SqlConnection,然后执行非查询命令。

    更新: O.P. 声明连接仍处于打开状态。好的,关闭再打开怎么样?

    更新 2: O.P. 测试发现无法重新打开连接。

  2. 由于上下文仍然可用,正如您的打印命令工作所证明的那样,请使用 SqlContext.Pipe.ExecuteAndSend(new SqlCommand("DROP TABLE..;")); 之类的东西

    更新: O.P. 声明这不起作用。

OR,由于您在代码中创建了保证唯一的表名,您可以尝试将表创建为全局临时表(即以两个井号为前缀:##TableName),这将a)可用于批量导入过程,并且 b)在连接完全关闭时自行清理。在这种方法中,您在技术上不需要执行任何手动清理。

当然,当启用连接池时,只有在重新打开连接并且执行第一个命令后,才会进行自动清理。为了强制立即清理,您必须在禁用连接池的情况下连接到 SQL Server。是否可以仅在要执行包含Pooling=false; 的存储过程时使用不同的连接字符串?鉴于此存储过程的使用方式,您似乎不会因仅在这一特定调用上禁用连接池而遭受任何明显性能下降。为了更好地了解连接池(启用或禁用)如何影响临时对象的自动清理,请参阅我刚刚发布的详细说明此行为的博文:

Sessions, Temporary Objects, and the Afterlife

这种方法可能是总体上最好的方法,因为您可能无法保证ROLLBACK 会被执行(提到的第一种方法)或 finally 子句会被执行(假设您曾经让它工作)。最后,未提交的事务将被回滚,但如果有人通过 SSMS 执行此操作并且它在没有ROLLBACK 的情况下中止,那么他们仍然处于打开的事务中并且可能不知道它。另外,想想连接被强行关闭,或者会话被杀死,或者服务器被关闭/重新启动。在这些情况下,tempdb 是您的朋友,无论是通过使用全局临时表,还是至少在 tempdb 中创建永久表,以便在下次 SQL Server 启动时自动删除它(由于 @987654349 @ 在每次启动 SQL Server 服务时被创建为 model 的副本)。

【讨论】:

  • 1.刚才测试了一下,连接状态其实还是打开的。 2.这个也试过了,也没执行。
  • 我对全局临时表的担忧是,如果有人正在使用此 proc 并多次取消它,它将把所有数据保存在 tempdb 中并可能占用大量空间直到他们关闭他们的标签。这也可能导致其他不相关查询的性能问题。
  • 好的,很有趣,很高兴知道。此外,如果连接仍然打开。我想知道你是否可以关闭它并重新打开它。但是,等等,我又想到了两件事。我会更新我的答案。
  • 我更新了一些额外的想法和问题。更新是穿插的,所以请阅读全文..
  • 我不能指望所有使用这个过程的开发者都用一个事务来包围它。 XACT_ABORTOFF,所以在这种情况下应该没关系。试图关闭并重新打开连接,但它不会执行connection.Open()。我将不得不进一步研究连接池,因为我不熟悉它的工作原理。
【解决方案2】:

退一步说,可能有更好的方法将数据从 CLR 过程中传递出去。

1) 您可以简单地使用 SqlContext 管道返回结果集,而无需创建表。

2) 您可以在调用代码中创建一个临时表 (#) 并从 CLR 过程内部访问它。您可能需要引入一个 TSQL 包装过程以方便使用。

无论如何使用 BEGIN TRAN/COMMIT | ROLLBACK 为我工作:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Threading;

static class SqlConnectionExtensions
{
    public static DataTable ExecuteDataTable(this SqlConnection con, string sql, params SqlParameter[] parameters)
    {
        var cmd = new SqlCommand(sql, con);
        foreach (var p in parameters)
        {
            cmd.Parameters.Add(p);
        }
        using (var dr = cmd.ExecuteReader())
        {
            var dt = new DataTable();
            dt.Load(dr);
            return dt;
        }
    }

    public static int ExecuteNonQuery(this SqlConnection con, string sql, params SqlParameter[] parameters)
    {
        var cmd = new SqlCommand(sql, con);
        foreach (var p in parameters)
        {
            cmd.Parameters.Add(p);
        }
        return cmd.ExecuteNonQuery();
    }

}

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void GetData(SqlString code)
    {
        Guid guid = Guid.NewGuid();
        using (var connection = new SqlConnection("context connection=true"))
        {
            connection.Open();

            try
            {
                connection.ExecuteNonQuery("begin transaction;");
                SqlContext.Pipe?.Send("Constrain");

                connection.ExecuteNonQuery($"CREATE TABLE qb.{code}_{guid:N} (Id INT)");

                SqlContext.Pipe?.Send($"Create: qb.{code}_{guid:N}");

                //emulate service call
                Thread.Sleep(TimeSpan.FromSeconds(10));

                SqlContext.Pipe?.Send($"Done: qb.{code}_{guid:N}");
                connection.ExecuteNonQuery("commit transaction");

            }
            catch (Exception ex)
            {
                connection.ExecuteNonQuery("rollback;");
                throw;
            }

        }

    }
}

【讨论】:

  • #1 不起作用,因为外部服务的特定工作流程需要先将数据加载到该表中。由于另一个会话正在加载数据,#2 将不起作用。这就是为什么我建议改用全局临时表的原因。如果catch 块仍然有打开的连接,而finally 块似乎没有,那么#3 会很有趣。虽然我仍然更喜欢不依赖手动清理,因为无论发生什么情况都会自动清理全局临时表。
  • “外部服务需要首先将数据加载到该表中” CLR proc 可以加载一个临时表,然后将内容通过管道传输到客户端。 “另一个会话正在加载的数据”“上下文连接”是不是不同的会话。它只是一个 API,允许 CLR 代码访问调用 CLR 存储过程的会话。
  • 是的,我知道上下文连接是什么。问题的最开始状态是:“将表名传递给将向其中批量插入一些数据的服务”。您甚至在此处的代码中复制了 //emulate service call 的 C# 注释。 将是一个不同的会话。
  • @DavidBrowne-Microsoft 服务将拥有的连接不会是上下文连接,据我所知,无法将此连接从 SQL CLR 传递到服务。我知道您可以在不创建表的情况下返回结果,但这会将我的业务逻辑放在 SQL CLR 函数中,我不希望这样。
  • 啊,明白了。在这种情况下,您根本无法使用事务,因为外部服务将无法看到在未提交事务中创建的表。
【解决方案3】:

虽然不建议使用它,但这将是 sp_bindsession 的理想匹配。使用sp_bindsession,您可以从上下文会话中调用sp_getbindtoken,将令牌传递给服务器并从服务连接中调用sp_bindsession

之后,这两个连接表现为“一体”,临时连接和事务被透明地传播。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-22
    • 1970-01-01
    • 2023-04-09
    • 2011-06-01
    • 1970-01-01
    • 2022-01-06
    • 2015-11-20
    相关资源
    最近更新 更多