这是 SqlCLR 的有效用例吗?调整存储过程结果

Is this a valid use case for SqlCLR? Adapting stored procedure results

我已经实现了下面描述的 CLR 存储过程,并且运行良好。但我不确定我是否真的需要 CLR 来完成此任务,或者中间层解决方案是否具有同样的性能和可维护性。

在现有代码库中,该公司多年来积累了 500 多个搜索存储过程。现在他们要我编写一个适用于所有这些存储过程的聚合引擎。他们系统中的每个搜索存储过程都遵循类似的格式,所以我知道如何使用正确的参数等以编程方式调用它们。

我不想以任何方式修改每个搜索存储过程。我更愿意做的是首先将存储过程的结果插入到临时 table 中。然后我可以通过查询临时 table.

运行 我的聚合引擎

问题是,在 SQL 服务器中,您无法插入存储过程的结果,除非您知道存储过程结果的确切架构。但这实际上是不可能的,因为存储过程可以根据参数 return 不同的结果模式。

因此,为了保证存储过程 return 我所期望的精确模式,我创建了一个 "SP_Wrapper" CLR 存储过程。在这个包装器中,我调用了一个存储过程,并将每条记录 "adapt" 到我预期的模式中。然后我 return 调整后的结果集。

然后我可以插入我的临时文件 table 知道模式是正确的。

现在,假设我调整了中间层的结果。我将不得不首先 return 结果到中间层。遍历它们,调整每条记录,然后单独插入或批量复制。

这似乎是正确的选择,但现在我必须部署这个 CLR 存储过程。我在这里真的收获很多吗?

using (var conn = new SqlConnection("context connection=true"))
{
        conn.Open();

        //load result table schema
        resultColumns = SqlSchema.getTempTableMeta(conn, resultTableName);

        //load parameter table schema - may not exist
        var hasParams = !String.IsNullOrEmpty(paramTableName);
        parameters = SqlSchema.getTempTableMeta(conn, paramTableName);

        SqlCommand command;
        SqlDataReader reader = null;

        ///Load Parameter Values
        if (hasParams)
        {
            command = conn.CreateCommand();
            command.CommandText = $@"if( object_id('tempdb..{paramTableName}') is not null) select top 1 * from {paramTableName};";
            command.CommandType = CommandType.Text;
            reader = command.ExecuteReader();

            using (reader)
            {
                while (reader.Read())
                {
                    foreach (var p in parameters)
                    {
                        var val = reader[p.Name];

                        if (!String.IsNullOrWhiteSpace(val?.ToString()))
                            parameter_values[p.Name] = val;
                    }
                }
            }
        }

        SqlDataRecord record = new SqlDataRecord(resultColumns.ToArray());

        //////mark the beginning of the result set
        SqlContext.Pipe.SendResultsStart(record);

        command = conn.CreateCommand();
        command.CommandType = CommandType.StoredProcedure;
        command.CommandText = spName;

        foreach (var p in parameters)
        {
            if (parameter_values.ContainsKey(p.Name))
                command.Parameters.Add(
                    new SqlParameter
                    {
                        ParameterName = p.Name,
                        SqlDbType = p.SqlDbType,
                        Value = parameter_values[p.Name]
                    }
                );
        }

        var cmdReader = command.ExecuteReader();

        using (cmdReader)
        {
            while (cmdReader.Read())
            {
                int sequence = 0;
                foreach (var resultColumn in resultColumns)
                {
                    var resultColumnValue = cmdReader[resultColumn.Name];

                    var t = resultColumn.SqlDbType;

                    resultColumnValue = SqlSchema.Convert(resultColumnValue, SqlSchema.sqlTypeMap[t]);

                    record.SetValue(sequence, resultColumnValue);
                    sequence++;
                }
                SqlContext.Pipe.SendResultsRow(record);
            }
        }

        // Mark the end of the result-set.
        SqlContext.Pipe.SendResultsEnd();

        conn.Close();
    }

原则上这个解决方案是有道理的。您正在使用 SQL CLR 作为转换为已知模式的适配器。您编写的代码看起来也很高效。

缺点是 SQL CLR 代码比普通代码更难编写、更难测试和更难部署。

这种权衡是否适合您取决于您​​的性能需求和开发人员生产力需求。这种数据复制真的会消耗那么多时间,以至于值得触摸 SQL CLR 吗?!可能会也可能不会。

另一种更快的解决方案是为您必须调用的每个过程生成 SQL 代码。不要手写。相反,让工具确定该过程的确切模式并输出完美的 T-SQL,直接将正确格式的数据通过管道传输到正确的目的地。

这个工具确实可以是一个 SQL CLR 过程,它生成代码然后执行它。或者,它可以是基于 C# 的代码生成器。

我会说这取决于:

  1. 存储过程返回的数据在做什么?

    问题中发布的代码缺少一些部分,因此您是否获取全部或部分返回的列并不完全清楚。当前方法的一个好处是,在将结果转储到临时 table(s) 时,您可以忽略不感兴趣的列。生成 T-SQL 代码以在纯 T-SQL 中执行 INSERT...EXEC 将不允许您过滤掉整个列;您必须将所有列插入目标 table 无论您是否愿意。

  2. 对于这些搜索过程,此界面是否还有其他潜在用途?

    SQLCLR 方法的一个好处是它更普遍可用。如果此功能包含在应用程序代码中,那么只有应用程序代码可以使用它。您将无法在 SQL 代理作业中使用它(不调用应用程序代码,这可能需要编写指向同一库的控制台应用程序,或使该库成为 PowerShell 模块)。您将无法在作为通过数据库邮件发送的自动报告来源的过程中使用它。您将无法轻松地将它的使用扩展到其他尚未被请求的领域。如果这些用例中的任何一个似乎可行,就需要考虑一下。

  3. 我不是 100% 确定这一点,但您当前的方法 可能 允许您回避对 INSERT...EXEC 的限制,如果您正在执行的过程(或其子过程之一,如果有的话)具有 INSERT...EXEC.

    (我现在没有时间测试这个,但是当我测试时,如果我发现它没有回避限制,我会删除这个点。)

虽然从部署/CI 的角度来看,SQLCLR 不像 T-SQL 那样直接,但这也并非不可能。当然,Visual Studio / SSDT 在需要正确处理安全性时(即使用基于签名的登录而不是启用 TRUSTWORTHY)确实不会让自动化部署变得容易,如果使用 SQL 服务器 2017 或更新版本。为了解决这个问题,我演示了两种与 Visual Studio / SSDT 一起使用或独立使用的类似方法,在我的以下两篇博客文章中进行了讨论:

  1. SQLCLR vs. SQL Server 2017, Part 2: “CLR strict security” – Solution 1 — more steps than Part 3, Solution 2 (below), but a good fit for existing projects as it requires almost no changes to the existing solution or even deployment process (and in fact, this is effectively the route that I went for my SQL# 项目所做的只是在安装脚本的开头添加 3 个简单的步骤)。此解决方案使用非对称密钥进行签名。
  2. SQLCLR vs. SQL Server 2017, Part 3: “CLR strict security” – Solution 2。此解决方案使用证书进行签名。

这两个解决方案的目标不仅是与 Visual Studio / SSDT 一起工作,而且还生成一个独立的 T-SQL 脚本。 T-SQL 脚本没有外部引用:不指向任何 DLL 文件,也不指向任何 .snk / .cer / .pfx 文件。这使得脚本完全 portable,因此更容易在任何持续集成设置中工作:-)。

有关使用 SQLCLR 的更多信息,请访问:SQLCLR Info