Динамически перебирать передаваемые значения-параметры в процедуре T-SQL - PullRequest
1 голос
/ 27 июня 2019

В настоящее время я пытаюсь написать шаблон процедуры по умолчанию для отчетов из T-SQL Datawarehouse.

Идея состоит в том, чтобы обернуть каждый запрос в процедуру, чтобы можно было легко управлять разрешениями и ведением журнала.

Поскольку это будут делать администраторы баз данных, я бы хотел, чтобы это решение работало, только вставляя некоторый стандартный код до и после основного запроса. Я бы предпочел, чтобы администратору БД не приходилось изменять какую-либо часть кода регистрации.

Я решил эту проблему для большинства частей, однако мне нужно записать, какие параметры пользователь отправил в процедуру. Очевидным решением будет жесткое кодирование параметров в журнале. Тем не менее, процедуры могут иметь различное количество параметров, и поэтому я бы хотел универсальное решение.

Насколько я понимаю, не существует простого способа перебора всех параметров. Однако я могу получить доступ к именам параметров из таблицы 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 ]

2 голосов
/ 27 июня 2019

Значения параметров недоступны при общем подходе.Вы можете создать генератор кода, который будет использовать sys.parameters для создания фрагмента кода, который вам нужно будет скопировать в каждый из ваших SP, или вы можете прочитать это или это о трассировке и XEvents.SQL-Server-Profiler работает таким образом, чтобы показывать вам операторы вместе со значениями параметров ...

Если вы не хотите заниматься трассировкой или XEvents, вы можете попробовать что-то подобное:

- Создать фиктивный процесс

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

Подсказка: нам нужен пустой элемент (,'') в конце каждой строки, чтобы разрешить несколько элементов с одинаковым именем.

- Теперь мы можем вызвать 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 голос
/ 27 июня 2019

Сфера - проблема убийцы для этого подхода. Я не думаю, что есть способ ссылаться на значения параметров с помощью чего-либо, кроме имен их переменных. Если бы был способ получить значения переменных из коллекции или по объявленной порядковой позиции, это могло бы работать на лету.

Я понимаю, что хочу снизить накладные расходы для администраторов баз данных и устранить вероятность ошибок, но я думаю, что лучшее решение - это сгенерировать необходимый код и предоставить его администраторам баз данных или предоставить им инструмент, который генерирует необходимые блоки кода. , Это примерно так же легко, как мы можем сделать это для администратора баз данных, но я думаю, что это дает дополнительное преимущество, заключающееся в устранении нагрузки обработки в процедуре, превращая ее в статический оператор с некоторой условной проверкой на корректность и конкатенацию. Курсоры и зацикленные объекты следует избегать, насколько это возможно.

Напишите скрипт SQL, который генерирует ваши блоки до и после запроса. Создайте их массово с комментариями вверху каждого набора блоков с именем хранимой процедуры и передайте его администраторам баз данных для копирования / вставки в соответствующие процедуры. В качестве альтернативы, дайте им сценарий и дайте им выполнить его по мере необходимости, чтобы сгенерировать пре- и постблоки самостоятельно.

Я бы включил некоторые проверки в сгенерированный скрипт, чтобы убедиться, что он работает во время исполнения. Это позволит обнаружить несоответствия в сгенерированном коде из-за последующих модификаций самой процедуры. Мы могли бы пройти лишнюю милю и включить имена параметров при генерации кода и проверить их по 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 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...