sp_executesql com o tipo de tabela definido pelo usuário não se comportando corretamente

11

Problema : existe um problema conhecido com os tipos de tabela definidos pelo usuário como parâmetros para sp_executesql ? Resposta - não, eu sou um idiota.

Configurar script

Esse script cria uma tabela, procedimento e tipo de tabela definido pelo usuário (somente SQL Server 2008+ restrito).

  • O objetivo do heap é fornecer uma auditoria que sim, os dados entraram no procedimento. Não há restrições nem nada para impedir a inserção de dados.

  • O procedimento usa como parâmetro um tipo de tabela definido pelo usuário. Tudo o que o proc faz é inserir na tabela.

  • O tipo de tabela definido pelo usuário também é simples, apenas uma coluna

Eu corri contra 11.0.1750.32 (X64) e 10.0.4064.0 (X64)Sim, eu sei que a caixa pode ser remendada, eu não controlo isso.

-- this table record that something happened
CREATE TABLE dbo.UDTT_holder
(
    ServerName varchar(200)
,   insert_time datetime default(current_timestamp)
)
GO

-- user defined table type transport mechanism
CREATE TYPE dbo.UDTT
AS TABLE
(
    ServerName varchar(200)
)
GO

-- stored procedure to reproduce issue
CREATE PROCEDURE dbo.Repro
(
    @MetricData dbo.UDTT READONLY
)
AS
BEGIN
    SET NOCOUNT ON

    INSERT INTO dbo.UDTT_holder 
    (ServerName)
    SELECT MD.* FROM @MetricData MD
END
GO

Reprodução de problemas

Este script demonstra o problema e deve levar cinco segundos para ser executado. Crio duas instâncias dos meus tipos de tabela definidos pelo usuário e, em seguida, tento dois meios diferentes de transmiti-los para sp_executesql. O mapeamento de parâmetros na primeira chamada imita o que eu capturei do SQL Profiler. Em seguida, chamo o procedimento sem o sp_executesqlinvólucro.

SET NOCOUNT ON
DECLARE 
    @p3 dbo.UDTT
,   @MetricData dbo.UDTT
INSERT INTO @p3 VALUES(N'SQLB\SQLB')
INSERT INTO @MetricData VALUES(N'SQLC\SQLC')

-- nothing up my sleeve
SELECT * FROM dbo.UDTT_holder

SELECT CONVERT(varchar(24), current_timestamp, 121) + ' Firing sp_executesql' AS commentary
-- This does nothing
EXECUTE sp_executesql N'dbo.Repro',N'@MetricData dbo.UDTT READONLY',@MetricData=@p3
-- makes no matter if we're mapping variables
EXECUTE sp_executesql N'dbo.Repro',N'@MetricData dbo.UDTT READONLY',@MetricData

-- Five second delay
waitfor delay '00:00:05'
SELECT CONVERT(varchar(24), current_timestamp, 121) + ' Firing proc' AS commentary
-- this does
EXECUTE dbo.Repro @p3

-- Should only see the latter timestamp
SELECT * FROM dbo.UDTT_holder

GO

Resultados : abaixo estão os meus resultados. A tabela está inicialmente vazia. Emito a hora atual e faço as duas chamadas para o sp_executesql. Aguardo 5 segundos para passar e emito a hora atual seguida pela chamada do próprio procedimento armazenado e finalmente despejo da tabela de auditoria.

Como você pode ver no registro de data e hora, o registro para o registro B corresponde à invocação direta do procedimento armazenado. Além disso, não há registro SQLC.

ServerName                                       insert_time
------------------------------------------------------------------------

commentary
-------------------------------------------------
2012-02-02 13:09:05.973 Firing sp_executesql

commentary
-------------------------------------------------
2012-02-02 13:09:10.983 Firing proc

ServerName                                       insert_time
------------------------------------------------------------------------
SQLB\SQLB                                        2012-02-02 13:09:10.983

Script de derrubar

Este script remove os objetos na ordem correta (você não pode soltar o tipo antes que a referência do procedimento tenha sido removida)

-- cleanup
DROP TABLE dbo.UDTT_holder
DROP PROCEDURE dbo.Repro
DROP TYPE dbo.UDTT
GO

Por que isso importa

O código parece bobo, mas não tenho muito controle sobre o uso do sp_execute vs uma chamada "direta" ao proc. No nível mais alto da cadeia de chamadas, estou usando a biblioteca ADO.NET e passando um TVP como fiz anteriormente . O tipo do parâmetro está definido corretamente como System.Data.SqlDbType.Structured e o CommandType está definido como System.Data.CommandType.StoredProcedure .

Por que eu sou um noob (editar)

Rob e Martin Smith viram o que eu não estava vendo - a declaração sendo passada para sp_executesql não usava o @MetricDataparâmetro Um parâmetro não passado / mapeado não será usado.

Eu estava traduzindo meu código de parâmetro com valor de tabela em C # para o PowerShell e, como ele foi compilado, não poderia ser o código com defeito; exceto que era. Ao escrever-se a esta pergunta, eu percebi que não tinha definido o CommandTypeque StoredProcedureentão eu adicionei essa linha e re-correu o código, mas o traço no profiler não mudou então eu assumi que não era o problema.

História engraçada - se você não salvar o arquivo atualizado, executá-lo não fará diferença. Cheguei nesta manhã e a executei novamente (após salvar) e o ADO.NET traduziu para o EXEC dbo.Repro @MetricData=@p3que funciona perfeitamente.

billinkc
fonte

Respostas:

10

sp_executesqlé para executar T-SQL ad-hoc. Então você deve tentar:

EXECUTE sp_executesql N'exec dbo.Repro @MetricData',N'@MetricData dbo.UDTT READONLY',@MetricData=@p3
Rob Farley
fonte
1
+1 Atualmente, o script no OP apenas possui a instrução dbo.Reproe não usa os parâmetros passados.
Martin Smith
Sim, vi o comentário de Rob e, embora não precisasse da parte EXEC, fazia sentido que o motivo pelo qual o parâmetro não estava sendo usado com a chamada fosse o parâmetro não ter sido referenciado no script. Atualizando tíquetes para refletir minhas tolices
billinkc 03/02/2012
@ billinkc - Ah, acabei de adicionar uma resposta com explicações mais detalhadas, em seguida, vi seu comentário. Vou excluir essa resposta daqui a pouco.
Martin Smith
Direita. Você não mencionou que estava fazendo isso no ado.net. sp_executesql é equivalente a um .CommandText, não .StoredProcedure.
22430 Rob Farley
3

Fiquei tentado a excluir esta pergunta, mas imaginei que outra pessoa poderia ter uma situação semelhante.

A causa raiz foi minha $updateCommandnão havia definido o CommandType. O valor padrão é Texto, portanto, meu procedimento armazenado estava sendo chamado corretamente. Meus parâmetros estavam sendo criados e transmitidos, mas conforme observado nas outras respostas, apenas porque os parâmetros estão disponíveis não significa que eles estão sendo usados .

Código no caso de alguém estar interessado no meu erro

    $updateConnection = New-Object System.Data.SqlClient.SqlConnection("Data Source=localhost\localsqla;Initial Catalog=baseline;Integrated Security=SSPI;")
    $updateConnection.Open()

    $updateCommand = New-Object System.Data.SqlClient.SqlCommand($updateQuery)
    $updateCommand.Connection = $updateConnection

    # This is what I was missing
    $updateCommand.CommandType = [System.Data.CommandType]::StoredProcedure
    #$updateCommand.CommandType = [System.Data.CommandType]::Text
    #$updateCommand.CommandType = [System.Data.CommandType]::TableDirect

    $DataTransferFormat = $updateCommand.Parameters.AddWithValue("@MetricData", $dataTable)
    $DataTransferFormat.SqlDbType = [System.Data.SqlDbType]::Structured
    $DataTransferFormat.TypeName = $udtt
    $results = $updateCommand.ExecuteNonQuery()

A execução com um valor CommandType of Text exigiria a solução de @ rob_farley de alterar o valor $updateQuerypara "EXEC dbo.Repro @MetricData". Nesse ponto, o sp_executesql mapeará os valores corretamente e a vida é boa.

A execução com um CommandTypedos TableDirectnão é suportada pelo SqlClient Data Provider, como indica a documentação.

Usar a CommandTypede StoredProceduretraduz para uma chamada direta do procedimento armazenado sem o sp_executesqlwrapper e funciona muito bem.

billinkc
fonte