sql-server - 在函数中执行存储过程或动态 SQL

标签 sql-server function sql-server-2012 dynamic-sql sqlclr

我正在使用 SQL Server 2012 并尝试将一个临时表中的数据转换为另一个临时表,在我将信息从一个表复制到另一个表时转换值。这是输入表的示例:

#IMPORT_DATA
SSN             Plan                          StartDate
123-45-6789     Basic Life Insurance          1/1/2015
123-45-6789     Vision                        1/1/2015
123-45-6789     Dental                        1/1/2015

我需要将这些数据转换成如下形式:

#STAGE_DATA
SSN             Plan                          StartDate
123456789       BLI                           20150101
123456789       VIS                           20150101
123456789       DTL                           20150101

我有一个转换表,它定义了一个将转换数据的 SQL 脚本,但在从函数内执行动态 SQL 时遇到了问题。

我正在使用表值函数加载 #STAGE_DATA,将 #IMPORT_DATA 作为 XML 传递。表函数为:

create function dcBPI.TranslateBenefitsStagingTable_FN
(
      @XmlData xml
    , @TranslationType varchar(50)
    , @Company varchar(3) = null
)
returns @ReturnTable table (
      [RowID] [int]
    , [SSN] [varchar](9) NULL
    , [Plan] [varchar](3) NULL
    , [StartDate] [varchar](8) NULL
)
as

begin

    insert into @ReturnTable ([RowID], [EmpSSN]) 
    select [Table].[Column].value('ID[1]', '[int]') as 'RowID'   -- no translation for RowID
            , dcBPI.TranslateSourceValue_FN('Benefit', 'SSN', [Table].[Column].value('SSN[1]', '[varchar](11)'), @Company) as 'SSN'
            , dcBPI.TranslateSourceValue_FN('Benefit', 'Plan', [Table].[Column].value('Plan[1]', ' [varchar](3)'), @Company) as 'Plan'
            , dcBPI.TranslateSourceValue_FN('Benefit', 'StartDate', [Table].[Column].value('StartDate[1]', ' [varchar](10)'), @Company) as 'StartDate'

    from @XmlData.nodes('//row') as [Table]([Column])

    return 
end
go

翻译函数是:

create function dcBPI.TranslateSourceValue_FN
(
      @TranslationType varchar(50)
    , @DestinationHeader varchar(255)
    , @SourceValue varchar(255)
    , @Company varchar(3) = null
)
returns varchar(255)
as
begin
    declare   @DestinationValue varchar(255) = 'Missing'
            , @sql nvarchar(max) = ''

    if len(rtrim(@Company)) = 0 set @Company = null

    select  @sql = 
            select
                    distinct td.DestinationValue
            from    dcBPI.TranslationData td    
            where   (
                        td.Company = @Company 
                        or td.Company = 'All'
                    )
                    and td.TranslationType = @TranslationType
                    and td.DestinationHeader = @DestinationHeader
                    and td.SourceValue = @SourceValue

    )

    exec sp_executesql @sql, N'@OutputValue nvarchar(max) OUTPUT', @OutputValue = @DestinationValue OUTPUT

    return @DestinationValue

end
go

当我执行表值函数(它依次执行 TranslateSourceValue_FN)时,我收到一条错误消息:

Only functions and some extended stored procedures can be executed from within a function.

我从其他文章中了解到我无法从函数调用动态 SQL,那么是否有其他方法可以完成我正在尝试做的事情?

编辑(添加@sql示例)
要使用的 SQL 是简单的选择语句,例如:

  • 选择替换('123-45-6789' '-', '')
  • 从代码表中选择 PlanCode,其中 Plan = @Plan'

最佳答案

由于您只需要执行一些动态 SQL,而动态 SQL 是简单的 SELECT 语句,您可以使用以下 SQLCLR 代码将对 sp_excutesql 的调用替换为:

SET @DestinationValue = dbo.SimpleSelect(@sql);

这个简单函数的 C# 代码是:

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

public class TheFunction
{
    [return: SqlFacet(MaxSize = 4000)]
    [Microsoft.SqlServer.Server.SqlFunction(DataAccess = DataAccessKind.Read,
        SystemDataAccess = SystemDataAccessKind.Read)]
    public static SqlString SimpleSelect(
        [SqlFacet(MaxSize = 4000)] SqlString TheQuery)
    {
        string _Output = null;

        if (TheQuery.Value.Trim() == String.Empty)
        {
            return new SqlString(_Output);
        }

        using (SqlConnection _Connection =
            new SqlConnection("Context Connection = true;"))
        {
            using (SqlCommand _Command = _Connection.CreateCommand())
            {
                _Command.CommandType = CommandType.Text;
                _Command.CommandText = TheQuery.Value;

                _Connection.Open();
                _Output = _Command.ExecuteScalar().ToString();
            }
        }

        return new SqlString(_Output);
    }
}

因为它使用进程内上下文连接,程序集可以保持标记为 SAFE 并且数据库不需要需要设置为 TRUSTWORTHY ON。但这也意味着您受到 T-SQL 函数所具有的所有其他限制的约束,例如:不能使用 NEWID(),不能更改数据库的状态,可以不要使用 RAISERRORSET 语句等。

以下代码是引用上面显示的 C# 方法的 T-SQL 包装器对象。请注意第二行末尾的 RETURNS NULL ON NULL INPUT。这不是 Visual Studio/SSDT 允许您设置的,因此必须通过运行以下代码手动完成。

CREATE FUNCTION [dbo].[SimpleSelect](@TheQuery [nvarchar](4000))
RETURNS [nvarchar](4000) WITH EXECUTE AS CALLER, RETURNS NULL ON NULL INPUT
AS EXTERNAL NAME [DynamicSqlForFunctions].[TheFunction].[SimpleSelect];

示例

  1. 下面两个例子都返回NULL。请注意,显示的第一个示例传入了一个 NULL,并且在 C# 方法中没有对 TheQuery.IsNull 的处理,但也没有“Object not set to a引用...”错误。这要归功于 CREATE FUNCTION 中指定的 RETURNS NULL ON NULL INPUT 选项的魔力。

    SELECT dbo.SimpleSelect(NULL);
    -- NULL
    
    SELECT dbo.SimpleSelect('');
    -- NULL
    
  2. 以下示例表明您几乎可以选择任何数据类型,并且无需将其转换为 NVARCHAR 即可正常工作。

    SELECT dbo.SimpleSelect('SELECT GETDATE();');
    -- 10/12/2015 10:25:33 AM
    
  3. 下面显示了传递多语句查询:

    DECLARE @SQL NVARCHAR(4000);
    SET @SQL = N'
    DECLARE @TempSum INT;
    SET @TempSum = 0;
    SELECT TOP(80) @TempSum = so.[object_id]
    FROM   [master].[sys].[objects] so
    SELECT @TempSum;
    ';
    
    SELECT dbo.SimpleSelect(@SQL);
    -- 133575514
    
  4. 以下示例显示我们可以通过此函数执行一个简单的只读存储过程。这不是可以在 T-SQL 函数中完成的事情。但是,与 T-SQL 函数一样,它不能运行有副作用的语句,这就是存储过程中没有 SET NOCOUNT ON; 的原因。

    所以首先运行这个:

    CREATE PROCEDURE #SimpleProcTest
    AS
    SELECT TOP(5) so.[name], so.[type_desc]
    FROM   [master].[sys].[objects] so
    ORDER BY so.[name] ASC;
    GO
    

    然后,如果你想测试它,运行这个:

    EXEC #SimpleProcTest;
    

    最后,我们在这里进行完整的测试,包括创建一个表变量,将存储过程的结果转储到该表变量中。

    DECLARE @TestSQL NVARCHAR(4000) = N'
    DECLARE @Bob TABLE (
    [name] sysname,
    [type_desc] NVARCHAR(60)
    );
    
    INSERT INTO @Bob
       EXEC #SimpleProcTest;
    
    SELECT COUNT(*)
    FROM @Bob
    WHERE PATINDEX(N''%[0-9]%'', [name]) > 0;
    ';
    
    SELECT dbo.SimpleSelect(@TestSQL);
    -- 2
    
  5. 对于最后一个示例,我们再次看到我们受到 T-SQL 函数对它们施加的大多数相同限制的约束。

    SELECT dbo.SimpleSelect('SELECT NEWID();');
    -- Msg 6522, Level 16, State 1, Line 1
    -- Invalid use of a side-effecting operator 'SELECT WITHOUT QUERY' within a function.
    

关于sql-server - 在函数中执行存储过程或动态 SQL,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33041179/

相关文章:

sql - 表中具有特定列总和的行

sql-server - 如何组织凌乱的数据库

PHP 函数调用 MySQL

c++ - 函数总是返回 1

c - 在树中查找函数

sql-server - 删除 SQL 集群节点

sql-server - 用vb.net执行存储过程

sql-server - 如何修复 SQL Server 查询中的排序规则冲突?

sql - 如何添加排名栏?

SQL Server : create a foreign key with a condition