Страницы

среда, 28 мая 2014 г.

Техника запросов к хранимых процедурам. Преодоление ошибки "Инструкция INSERT EXEC не может быть вложенной". Поиск объектов во всех базах данных одним запросом.

Всем известно ограничение Transact-SQL, состоящее в следующем. Есть хранимая процедура, которая возвращает таблицу. Если требуется, чтобы эта таблица участвовала в каком-либо запросе, то приходится сохранять результаты работы процедуры во временной таблице с помощью инструкции "insert into <Table Name> exec <ProcName>" Это неудобно, требуется больше кода.
К тому же инструкция "insert into <Table Name> exec <ProcName>" имеет ограничение. Многие разработчики сталкивались с такой проблемой: в коде процедуры <ProcName> уже имеется инструкция "insert into ... exec ...". Тогда попытка сохранить результаты <ProcName> приводит к ошибке. В сообщении говорится, что конструкция "insert into ... exec ..." не может быть вложенной. Можно пытаться обойти эту проблему, используя функцию openquery, которая использует в качестве связанного сервера линк, который смотрит на исходный сервер. Однако такой метод не всегда сработает: функция может завершиться с ошибкой, если в процедуре используются временные таблицы, к тому же openquery не поддерживает параметры.


Один из подходов к решению проблемы состоит в следующем. Напишем табличную функцию, которая получает в качестве параметра текстовое sql-выражение и возвращает набор записей, который возщвращает sql-код. Как известно функции имеют массу ограничений по сравнению с хранимыми процедурами. Однако в них можно применять расширенные хранимые процедуры sp_OA...


План функции таков. Через sp_OACreate мы создадим ADO-соединение и ADO-команду. Для команды инициализируем свойство CommandText нашим sql-кодом. Далее применим метод Execute, инициализировав ADO-набор записей. После этого пройдемся в цикле по набору записей, наполнив выходную таблицу.


Предварительно создадим табличную функцию, которая будет логировать возможные ошибки OLE:
if sessionproperty ( N'quoted_identifier' ) = 0
    set quoted_identifier on
go
if sessionproperty ( N'ansi_nulls' ) = 0
    set ansi_nulls on
go
if object_id ( N'dbo.ProcessOleError', N'TF' ) is null
    exec ( N'create function dbo.ProcessOleError () returns @tab table ( i int ) as begin return end' )
go
alter function dbo.ProcessOleError ( @step varchar ( 100 ), @obj int )
    returns @tab table ( src varchar ( 300 ), descr varchar ( 300 ) )
as
begin

    declare @src varchar ( 300 ), @desc varchar ( 300 )
    exec sp_OAGetErrorInfo @obj, @src out, @desc out
    insert into @tab ( src, descr )
        values ( N'статус: ' + @step + N'; источник: ' + isnull ( @src, 'неизвестно' ), N'описание: ' + isnull ( @desc, 'неизвестно' ) )
    return
end
go

Ниже код табличной функции dbo.GetQueryResult:
if sessionproperty ( N'quoted_identifier' ) = 0
    set quoted_identifier on
go
if sessionproperty ( N'ansi_nulls' ) = 0
    set ansi_nulls on
go
if object_id ( N'dbo.GetQueryResult', N'TF' ) is null
    exec ( N'create function dbo.GetQueryResult () returns @tab table ( i int ) as begin return end' )
go
alter function dbo.GetQueryResult ( @txt nvarchar ( max ) )
    returns @tab table
    (
        col0 varchar ( 300 ),
        col1 varchar ( 300 )
    )
as
begin
    declare @res int, @conn int, @cmd int, @prop int, @propInst int, @propSearch int, @propAsync int, @ADOrs int
    exec @res = sp_OACreate 'ADODB.Connection', @conn out
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'создание соединения', @conn )
        goto GarbageCollector
    end

    exec @res = sp_OASETProperty @conn, 'ConnectionString', 'Provider=SQLNCLI11; Server=EDYNAK\SHADOW; Database=master; Trusted_Connection=yes;'
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'строка подключения', @conn )
        goto GarbageCollector
    end

    exec @res = sp_OAMethod @conn, 'Open'
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'открытие соединения', @conn )
        goto GarbageCollector
    end

    exec @res = sp_OACreate 'ADODB.Command', @cmd out
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'создание команды', @cmd )
        goto GarbageCollector
    end

    exec @res = sp_OASETProperty @cmd, 'ActiveConnection', @conn
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'соединение для команды', @cmd )
        goto GarbageCollector
    end

    exec @res = sp_OASETProperty @cmd, 'CommandText', @txt
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'текст команды', @cmd )
        goto GarbageCollector
    end

    exec @res = sp_OACreate 'ADODB.Recordset', @ADOrs out
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'набор записей', @ADOrs )
        goto GarbageCollector
    end

    exec @res = sp_OAMethod @cmd, 'Execute', @ADOrs out
    if @res <> 0
    begin
        insert into @tab ( col0, col1 )
            select src, descr
            from dbo.ProcessOleError ( N'выполнение запроса', @cmd )
        goto GarbageCollector
    end

    declare @h int = 1, @data varchar ( 300 ), @obj varchar ( 300 )
    declare @src varchar ( 300 ), @desc varchar ( 300 )
    declare @eof bit = 0

    while @eof <> 1
    begin
        exec @res = sp_OAGetProperty @ADOrs, 'Fields(0).Value', @data out
        if @res <> 0
        begin
            insert into @tab ( col0, col1 )
                select src, descr
                from dbo.ProcessOleError ( N'первое поле', @ADOrs )
            goto GarbageCollector
        end


        exec @res = sp_OAGetProperty @ADOrs, 'Fields(1).Value', @obj out
        if @res <> 0
        begin
            insert into @tab ( col0, col1 )
                select src, descr
                from dbo.ProcessOleError ( N'второе поле', @ADOrs )
            goto GarbageCollector
        end

        insert into @tab ( col0, col1 )
            values ( @data, @obj )
        exec @res = sp_OAMethod @ADOrs, 'MoveNext', null
        if @res <> 0
        begin
            insert into @tab ( col0, col1 )
                select src, descr
                from dbo.ProcessOleError ( N'переход на строку', @ADOrs )
            goto GarbageCollector
        end

        exec @res = sp_OAGetProperty @ADOrs, 'EOF', @eof out
        if @res <> 0
        begin
            insert into @tab ( col0, col1 )
                select src, descr
                from dbo.ProcessOleError ( N'признак окончания набора', @ADOrs )
            goto GarbageCollector
        end
    end
GarbageCollector:
    exec sp_OADestroy @conn
    exec sp_OADestroy @cmd
    exec sp_OADestroy @prop
    exec sp_OADestroy @propInst
    exec sp_OADestroy @propSearch
    exec sp_OADestroy @propAsync
    exec sp_OADestroy @ADOrs

    return
end
go

Испытаем функцию. Например, для того чтобы получить список всех объектов всех баз данных одним запросом, можно использовать такой код:
select *
from dbo.GetQueryResult
(    '
    declare @sql nvarchar ( max ) = N'''', @name nvarchar ( max )
    declare cur cursor local static forward_only for
        select name
        from sys.databases
        where database_id > 4
    open cur
    fetch next from cur into @name
    while @@fetch_status = 0
    begin
        set @sql = @sql + case when @sql = N'''' then N'''' else N'' union all '' end + N''
        select '''''' + @name + '''''', name collate Cyrillic_General_CI_AI
        from '' + quotename ( @name ) + ''.sys.tables
        ''
        fetch next from cur into @name
    end
    close cur
    deallocate cur
    exec sp_executesql @sql
'
)
Теперь можно использовать этот запрос для соединения с другими таблицами, вставками в таблицы. Можно передать в паметр функции и sql-выражение, содержащее вызов любой процедуры с параметрами, возвращающей набор записей. В коде функции я для примера сделал возврат только двух столбцов. При необходимости можно добавить большее число столбцов. Для работы функции на серверном компьютере должна быть установлена библиотека Microsoft ActiveX Data Objects.

Комментариев нет:

Отправить комментарий