sp_executesql avec le type de table défini par l'utilisateur ne se comporte pas correctement

11

Problème : existe-t-il un problème connu avec les types de table définis par l'utilisateur en tant que paramètres de sp_executesql ? Réponse - non, je suis un idiot.

Configurer le script

Ce script crée une table, une procédure et un type de table défini par l'utilisateur (SQL Server 2008+ uniquement).

  • Le but du tas est de fournir un audit qui, oui, les données l'ont fait dans la procédure. Il n'y a pas de contraintes, rien pour empêcher l'insertion de données.

  • La procédure prend en paramètre un type de table défini par l'utilisateur. Tout ce que le proc fait, c'est l'insérer dans le tableau.

  • Le type de table défini par l'utilisateur est également simple, juste une seule colonne

J'ai exécuté le contre suivant 11.0.1750.32 (X64) et 10.0.4064.0 (X64)oui, je sais que cette boîte pourrait être corrigée, je ne contrôle pas cela.

-- 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

Problème de reproduction

Ce script illustre le problème et devrait prendre cinq secondes pour s'exécuter. Je crée deux instances de mes types de table définis par l'utilisateur, puis j'essaie deux moyens différents de les transmettre à sp_executesql. Le mappage des paramètres dans le premier appel imite ce que j'ai capturé à partir de SQL Profiler. J'appelle ensuite la procédure sans le sp_executesqlwrapper.

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

Résultats : voici mes résultats. La table est initialement vide. J'émets l'heure actuelle et fais les deux appels vers le sp_executesql. J'attends 5 secondes pour passer et émettre l'heure actuelle, puis j'appelle la procédure stockée elle-même et finalement jette la table d'audit.

Comme vous pouvez le voir sur l'horodatage, l'enregistrement pour l'enregistrement B correspond à l'appel de procédure stockée directe. De plus, il n'y a pas d'enregistrement 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 démontage

Ce script supprime les objets dans le bon ordre (vous ne pouvez pas supprimer le type avant la suppression de la référence de la procédure)

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

Pourquoi c'est important

Le code semble idiot mais je n'ai pas beaucoup de contrôle sur l'utilisation de sp_execute vs un appel "direct" au proc. Plus haut dans la chaîne d'appels, j'utilise la bibliothèque ADO.NET et je passe un TVP comme je l' ai fait précédemment . Le type du paramètre est correctement défini sur System.Data.SqlDbType.Structured et le CommandType est défini sur System.Data.CommandType.StoredProcedure .

Pourquoi je suis un noob (modifier)

Rob et Martin Smith ont vu ce que je ne voyais pas - l'instruction transmise à sp_executesql n'a pas utilisé le @MetricDataparamètre. Un paramètre non passé / mappé ne va pas être utilisé.

Je traduisais mon code de paramètre de valeur de table C # fonctionnel vers PowerShell et puisqu'il a été compilé, il ne pouvait pas être le code en faute; sauf qu'elle l'était. Tout en écrivant cette question, je me suis aperçu que je ne l' avais pas mis l' CommandTypeà StoredProceduredonc j'ajouté cette ligne et re-couru le code , mais la trace dans profileur n'a pas changé , donc je suppose que la question n'a pas été.

Histoire drôle - si vous n'enregistrez pas le fichier mis à jour, son exécution ne fera aucune différence. Je suis arrivé ce matin et l'ai réexécuté (après avoir enregistré) et ADO.NET l'a traduit, ce EXEC dbo.Repro @MetricData=@p3qui fonctionne très bien.

billinkc
la source

Réponses:

10

sp_executesqlsert à exécuter T-SQL ad hoc. Vous devriez donc essayer:

EXECUTE sp_executesql N'exec dbo.Repro @MetricData',N'@MetricData dbo.UDTT READONLY',@MetricData=@p3
Rob Farley
la source
1
+1 Actuellement, le script de l'OP n'a que l'instruction dbo.Reproet n'utilise pas du tout les paramètres passés.
Martin Smith
Oui, j'ai vu le commentaire de Rob et même s'il n'avait pas besoin de la partie EXEC, il était logique que la raison pour laquelle le paramètre n'était pas utilisé avec l'appel était que le paramètre n'était pas référencé dans le script. Mise à jour du ticket pour refléter ma folie
billinkc
@billinkc - Ah je viens d'ajouter une réponse avec une explication plus détaillée puis j'ai vu votre commentaire. Je supprimerai cette réponse dans un instant.
Martin Smith
Droite. Vous n'aviez pas mentionné que vous faisiez cela sur ado.net. sp_executesql est équivalent à un .CommandText, pas .StoredProcedure.
Rob Farley
3

J'ai été tenté de supprimer cette question, mais j'ai pensé que quelqu'un d'autre pourrait se retrouver dans une situation similaire.

La cause profonde était que mon $updateCommandn'avait pas défini le CommandType. La valeur par défaut est Texte, ma procédure stockée était donc appelée correctement. Mes paramètres ont été créés et transmis, mais comme indiqué dans les autres réponses, ce n'est pas parce que les paramètres sont disponibles qu'ils sont utilisés .

Code au cas où quelqu'un serait intéressé par mon erreur

    $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()

L'exécution avec une valeur de CommandType of Text nécessiterait la solution de @ rob_farley de changer la valeur de $updateQuery"EXEC dbo.Repro @MetricData". À ce stade, le sp_executesql mappera les valeurs correctement et la vie est bonne.

L'exécution avec un CommandTypede TableDirectn'est pas prise en charge par le fournisseur de données SqlClient, comme l'indique la documentation.

L'utilisation CommandTypede a se StoredProceduretraduit par une invocation directe de la procédure stockée sans le sp_executesqlwrapper et fonctionne très bien.

billinkc
la source