【问题标题】:Execute Stored Procedure or Dynamic SQL in a Function在函数中执行存储过程或动态 SQL
【发布时间】:2016-01-07 13:47:01
【问题描述】:

我正在使用 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)时,我收到以下错误消息:

只能执行函数和一些扩展存储过程 从函数内部。

我从其他文章中知道我无法从函数中调用 Dynamic SQL,那么还有其他方法可以完成我想要做的事情吗?

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

  • select replace('123-45-6789' '-', '')
  • select PlanCode from CodeTable where Plan = @Plan'

【问题讨论】:

  • @srutzky 查看@sql 示例的编辑。

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


【解决方案1】:

由于您只需要执行一些动态 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,但没有“对象未设置为引用...”错误。这要归功于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.
    

【讨论】:

    猜你喜欢
    • 2023-04-06
    • 1970-01-01
    • 2020-01-15
    • 1970-01-01
    • 2014-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多