在T-SQL过程中动态遍历传入的参数值

时间:2019-06-27 16:46:29

标签: sql sql-server tsql

我目前正在尝试编写一个默认的过程模板,用于从T-SQL数据仓库进行报告。

该想法是将每个查询包装在一个过程中,以便可以轻松管理权限和日志记录。

由于这将由DBA完成,因此我希望通过仅在主查询之前和之后粘贴一些标准代码来使该解决方案起作用。我希望DBA不必修改日志记录代码的任何部分。

我已经解决了大部分问题,但是,我需要记录用户已向过程提交了哪些参数。 显而易见的解决方案是将参数硬编码到日志中。但是,这些过程可以具有不同数量的参数,因此,我希望采用一种万能的解决方案。

我的理解是,没有简单的方法可以遍历所有参数。 但是,我可以从表sys.parameters中访问参数名称。

这个最接近我的解决方案的例子是最小的例子:

CREATE TABLE #loggingTable (
  [ProcedureID] INT
, [paramName] NVARCHAR(128)
, [paramValue] NVARCHAR(128)
)
;
go

CREATE PROCEDURE dbo.[ThisIsMyTestProc] (
        @param1 TINYINT = NULL
      , @Param2 NVARCHAR(64) = null
)
AS
BEGIN
   -- Do some logging here

   DECLARE @query NVARCHAR(128)
   DECLARE @paramName NVARCHAR(128)
   DECLARE @paramValue nvarchar(128)

   DECLARE db_cursor CURSOR FOR 
   SELECT [name] FROM [sys].[parameters] WHERE object_id = @@PROCID

   OPEN db_cursor 
   FETCH NEXT FROM db_cursor INTO @paramName

   WHILE @@FETCH_STATUS = 0
   BEGIN
      SET @query = 'SELECT @paramValue = cast(' + @paramName + ' as nvarchar(128))';
      SELECT @query;
      -- Following line doesn't work due to scope out of bounds, and is prone to SQL-Injections.
      --EXEC SP_EXECUTESQL @query; -- Uncomment for error
      insert into #loggingTable(ProcedureID, paramName, paramValue)
      values(@@PROCID, @paramName, @paramValue)
      FETCH NEXT FROM db_cursor INTO @paramName
   END

   CLOSE db_cursor
   DEALLOCATE db_cursor

   -- Run the main query here (Dummy statement)
   SELECT @param1 AS [column1], @Param2 AS [column2]

   -- Do more logging after statement has run

END
GO

-- test
EXEC dbo.[ThisIsMyTestProc] 1, 'val 2';
select * from #loggingTable;

-- Cleanup 
DROP PROCEDURE dbo.[ThisIsMyTestProc];
DROP table #loggingTable;

但是,这确实具有主要缺点。

  1. 由于范围可变,它不起作用
  2. 很容易发生SQL注入,这是不可接受的

有什么办法解决这个问题?

2 个答案:

答案 0 :(得分:2)

在通用方法中,参数值不可用。您可以创建一些代码生成器,该生成器将使用sys.parameters创建必须复制到每个SP中的代码块,或者您可能会read thisthis关于跟踪和XEvent 。 SQL-Server-Profiler以这种方式向您显示语句以及参数值...

如果您不想进入跟踪或XEvent,则可以尝试以下方法:

-创建一个虚拟proc

CREATE PROCEDURE dbo.[ThisIsMyTestProc] (
        @param1 TINYINT = NULL
      , @Param2 NVARCHAR(64) = null
)
AS
BEGIN
    SELECT @@PROCID; 
END
GO

-调用它以查看@@PROCID的值

EXEC dbo.ThisIsMyTestProc; --See the proc-id
GO

-现在这是魔术部分。它将创建一个命令,您可以将其复制并粘贴到您的SP中:

  SELECT CONCAT('INSERT INTO YourLoggingTable(LogType,ObjectName,ObjectId,Parameters) SELECT ''ProcedureCall'', ''',o.[name],''',',o.object_id,','
         ,'(SELECT'
         ,STUFF((
            SELECT CONCAT(',''',p.[name],''' AS [parameter/@name],',p.[name],' AS [parameter/@value],''''')
            FROM sys.parameters p 
            WHERE p.object_id=o.object_id
            FOR XML PATH('')
          ),1,1,'')
          ,' FOR XML PATH(''''),ROOT(''parameters''),TYPE)'
          ) 
   FROM [sys].[objects] o 
   WHERE o.object_id = 525244926; --<-- Use the proc-id here

-现在我们可以将字符串复制到过程中了
-我评论了INSERT部分,SELECT足以显示效果

ALTER PROCEDURE dbo.[ThisIsMyTestProc] (
        @param1 TINYINT = NULL
      , @Param2 NVARCHAR(64) = null
)
AS
BEGIN
    --The generated code comes in one single line
    --INSERT INTO YourLoggingTable(LogType,ObjectName,ObjectId,Parameters) 
    SELECT 'ProcedureCall'
          ,'ThisIsMyTestProc'
          ,525244926
          ,(SELECT'@param1' AS [parameter/@name],@param1 AS [parameter/@value],''
                 ,'@Param2' AS [parameter/@name],@Param2 AS [parameter/@value],'' 
            FOR XML PATH(''),ROOT('parameters'),TYPE)
END
GO

提示:我们需要在每行末尾添加 empty元素(,'',以允许多个具有相同名称的元素。

-现在我们可以使用一些参数值调用SP

EXEC dbo.ThisIsMyTestProc 1,'hello'; 
GO

结果,您的日志表将获得一个类似这样的条目

ProcedureCall   ThisIsMyTestProc    525244926   <parameters>
                                                  <parameter name="@param1" value="1" />
                                                  <parameter name="@Param2" value="hello" />
                                                </parameters>

只需添加典型的日志记录数据,例如UserID,DateTime,无论您需要什么...

答案 1 :(得分:1)

范围是这种方法的杀手级问题。我认为除了变量名之外,没有其他方法可以引用参数的值。如果有一种方法可以从集合中或按声明的顺序位置检索变量值,那么它可以即时运行。

我了解到希望将DBA的开销保持在较低水平并消除出错的机会,但是我认为最好的解决方案是生成所需的代码并将其提供给DBA或为他们提供生成所需代码块的工具。这与我们为DBA所做的一样轻巧,但是我认为它具有消除过程中的处理负载的额外好处,方法是将其转换为静态语句,并进行一些条件检查,以确保有效性和并发工作。应尽可能避免使用游标和循环的东西。

编写一个SQL脚本来生成您的查询前和查询后块。在每个带有存储过程名称的块的顶部带有注释的情况下,大量生成它们,并将其交给DBA以复制/粘贴到相应的proc中。或者,给他们脚本,让他们根据需要运行脚本,以自行生成pre-block和post-block。

我将在生成的脚本中包括一些检查,以帮助确保其在执行期间正常运行。由于对过程本身的后续修改,这将检测到所生成代码中的不匹配。我们可以加倍努力,在生成代码时包括参数名称,并针对sys.parameters进行验证,以确保自代码生成以来,硬编码到生成代码中的参数名称没有改变。

-- Log execution details pre-execution
IF object_name(@@PROCID) = 'ThisIsMyTestProc' AND (SELECT COUNT(*) FROM [sys].[parameters] WHERE object_id = @@PROCID) = 2
BEGIN
    EXEC LogProcPreExecution @Params = CONCAT('parm1: ', @param1, ' parm2: ', @Param2), @ProcName = 'ThisIsMyTestProc', @ExecutionTime = getdate() @ExecutionUser = system_user
END
ELSE 
BEGIN
    --Do error logging for proc name and parameter mismatch
END

--Log procedure would look like this
CREATE PROCEDURE
    LogProcPreExecution
    @Parameters varchar(max),
    @ProcName nvarchar(128),
    @ExecutionTime datetime, 
    @ExecutionUser nvarchar(50)
AS
BEGIN
    --Do the logging
END