SQLBulkCopy avec CLR UDT indique "Impossible de trouver la méthode" Lecture "pour le type" MyNamespace.MyType "dans l'assembly" Mon type "

sqlbulkcopy sqlclr sql-server sql-server-2012 user-defined-types

Question

J'ai écrit un type défini par l'utilisateur (UDT) SQL Server CLR dans SQL Server 2012. J'ai pu y accéder via des scripts de test SQL, je l'ai utilisé comme variable locale, je l'ai défini dans une table et testé Visual Studio et SQL Server Management Studio.

Nous avons un service qui utilise SQLBulkCopy de manière assez générale pour récupérer les fichiers placés dans un répertoire, puis insérer leur contenu dans la table appropriée. Lorsque j'ajoute mon type défini par l'utilisateur en tant que colonne dans l'une de ces tables, l'erreur de l'invocation WriteToServer (DataTable) s'affiche.

La colonne UDT est transmise en tant que System.String, dans l'espoir que la méthode Parse () de l'UDT sera appelée dans SQL Server pour la convertir en type interne. J'ai également essayé de déclarer la classe UDT dans ce programme client et de transmettre directement les données en tant que type UDT.

Dans les deux cas, je reçois ce message d'erreur (modifié pour supprimer mes noms propriétaires)

Impossible de trouver la méthode 'Lecture' pour le type 'MyNamespace.MyType' dans l'assembly 'MyType'

J'ai examiné autant de questions similaires que je peux trouver à propos de ce message d'erreur et elles font généralement référence au format de l'instruction CREATE. En outre, ils font généralement référence aux fonctions CLR, pas aux types CLR, qui sont légèrement différentes. C'est à moi:

CREATE TYPE [dbo]. [MyType]
NOM EXTERNE [MyType]. [MyNamespace.MyType]

Je soupçonne que cela pourrait ne pas être le problème, et que, plutôt, cela a à voir avec la façon dont SQLBulkCopy interagit avec un UDT SQLCLR. Pour cette combinaison particulière, il est difficile de trouver une explication détaillée.

Edit # 1 - Il s’agit d’une sérialisation personnalisée.

[Serializable]  
[Microsoft.SqlServer.Server.SqlUserDefinedType( Format.UserDefined, MaxByteSize = -1 )]  
public struct MyType: INullable, IBinarySerialize  

Edit # 2 - l'autorisation d'exécution est accordée

GRANT EXECUTE 
ON TYPE :: MyType
TO PUBLIC 

Edit # 3 - code de test adapté

CREATE TABLE [dbo].[TestMyType]
(
    [SourceMachine]       [varchar](32)  NULL,
    [Output]              MyType NULL
)

et mis à jour par

try
{
    DataTable dataTable = new DataTable( "[TestMyType]" );
    dataTable.Columns.Add( "SourceMachine", typeof( System.String ) );
    dataTable.Columns.Add( "Output", typeof( MyNamespace.MyType ) );

    dataTable.Rows.Add( "Ron1", MyNamespace.MyType.Parse( "This is string 1" ) );
    dataTable.Rows.Add( "Ron2", MyNamespace.MyType.Parse( "This is string 2" ) );
    dataTable.Rows.Add( "Ron3", MyNamespace.MyType.Parse( "This is string 3" ) );

    SqlBulkCopy sqlBulkCopy = new SqlBulkCopy( conn );
    sqlBulkCopy.DestinationTableName = "[TestMyType]";
    sqlBulkCopy.WriteToServer( dataTable );
}
catch ( Exception ex)
{
    System.Diagnostics.Debug.WriteLine(ex.Message);                            
    throw;
}

Cela a donné le même message d'erreur que ci-dessus.

Éditer # 4 - Éliminer SqlBulkCopy du problème
J'ai recréé le problème en utilisant un INSERT paramétré. Je l'ai configuré pour transmettre l'objet UDT du client au serveur en tant que paramètre utilisant directement une instance de l'UDT.

string sInsert = "INSERT INTO TestMyType VALUES (?, ?)";
SqlCommand command = new SqlCommand(sInsert, conn);
SqlParameter parm1 = new SqlParameter("SourceMachine", "This is Machine 01");
SqlParameter parm2 = new SqlParameter("Output", MyNamespace.MyType.Parse( "This is INSERT 01" ) );
parm2.UdtTypeName = "MyType";
command.Parameters.Add(parm1);
command.Parameters.Add(parm2);
int nResult = command.ExecuteNonQuery();

donnant

A first chance exception of type 'System.Data.SqlClient.SqlException'
    occurred in System.Data.dll
Additional information: Could not find method 'Read' for 
    type 'MyNamespace.MyType' in assembly 'MyType'

Réponse acceptée

SqlBulkCopy devrait être capable de gérer parfaitement les UDT (types définis par l'utilisateur) SQLCLR. J'ai réussi à utiliser les méthodes DbDataReader et DataTable .

Voici ce qui a fonctionné pour moi:

Code C # (j'ai fait du "client" une procédure stockée SQLCLR)

using System;
using System.Data.SqlTypes;
using System.Data.SqlClient;
using Microsoft.SqlServer.Server;

public class xtra
{

    [SqlProcedure]
    public static void BcpTest(SqlInt32 TheID, SqlString TheConnectionString)
    {
        System.Data.DataTable _DataTable = new System.Data.DataTable();
        _DataTable.Columns.Add("ID", typeof(Int32));
        _DataTable.Columns.Add("SomeDate", typeof(DateTime));
        _DataTable.Columns.Add("SomeData", typeof(Type_HashTable));

        Type_HashTable _Bob = Type_HashTable.Parse(@"testKey=testVal");
        _DataTable.Rows.Add(TheID.Value, DateTime.Now, _Bob);

        _DataTable.Rows.Add(TheID.Value + 1, DateTime.Now,
           Type_HashTable.Parse(@"testKey2=testVal2"));

        SqlBulkCopy _BulkCopy = new SqlBulkCopy(TheConnectionString.Value);
        _BulkCopy.DestinationTableName = "dbo.BulkCopyUDT";

        try
        {
            _BulkCopy.WriteToServer(_DataTable);
        }
        finally
        {
            _BulkCopy.Close();
        }
    }
}

Code T-SQL

-- DROP TABLE dbo.BulkCopyUDT;
CREATE TABLE dbo.BulkCopyUDT
(
  ID INT NOT NULL CONSTRAINT [PK_BulkCopyUDT] PRIMARY KEY,
  SomeDate DATETIME,
  SomeData [SQL#].[Type_HashTable]
);
GO

GRANT INSERT, SELECT ON dbo.BulkCopyUDT TO [Public];
GRANT EXECUTE ON TYPE::SQL#.Type_HashTable TO [Public];
GO

CREATE PROCEDURE dbo.SqlBulkCopy_Test
(
    @TheID INT,
    @TheConnectionString NVARCHAR(4000) = 
        N'Data Source=(local); Integrated Security=true; Initial Catalog=my_database;'
)
AS EXTERNAL NAME [my_assembly].[xtra].[BcpTest];
GO

ALTER ASSEMBLY [my_assembly] WITH PERMISSION_SET = EXTERNAL_ACCESS;
GO

Le test

EXEC dbo.SqlBulkCopy_Test 1;

SELECT *, SomeData.ToString() FROM dbo.BulkCopyUDT;

EXEC dbo.SqlBulkCopy_Test 3,
       N'Data Source=(local); User=test; Password=test; Initial Catalog=my_database;';

SELECT *, SomeData.ToString() FROM dbo.BulkCopyUDT;

Je l'ai également obtenu depuis une application console, en utilisant à la fois SqlBulkCopy et une requête ad hoc paramétrée:

using System;
using System.Data;
using System.Data.SqlClient;

namespace SqlBulkCopyUDT
{
    class Program
    {
        static void Main(string[] args)
        {
            int _TheID = Int32.Parse(args[0]);

            string _TheConnectionString = 
              @"Data Source=(local); Integrated Security=true; Initial Catalog=my_database;";
            if (args.Length > 1)
            {
                _TheConnectionString = args[1];
            }

            //DataTable _DataTable = new DataTable();
            //_DataTable.Columns.Add("ID", typeof(Int32));
            //_DataTable.Columns.Add("SomeDate", typeof(DateTime));
            //_DataTable.Columns.Add("SomeData", typeof(Type_HashTable));

            //Type_HashTable _Bob = Type_HashTable.Parse(@"testKey=testVal");
            //_DataTable.Rows.Add(_TheID, DateTime.Now, _Bob);

            //_DataTable.Rows.Add(_TheID + 1, DateTime.Now,
            //   Type_HashTable.Parse(@"testKey2=testVal2"));

            //SqlBulkCopy _BulkCopy = new SqlBulkCopy(_TheConnectionString);
            //_BulkCopy.DestinationTableName = "dbo.BulkCopyUDT";

            //try
            //{
            //    _BulkCopy.WriteToServer(_DataTable);
            //}
            //finally
            //{
            //    _BulkCopy.Close();
            //}

            using (SqlConnection _Connection = new SqlConnection(_TheConnectionString))
            {
                using (SqlCommand _Command = _Connection.CreateCommand())
                {
                    _Command.CommandType = CommandType.Text;
                    _Command.CommandText =
                        @"INSERT INTO dbo.BulkCopyUDT (ID, SomeDate, SomeData)
                         VALUES (@MyID, GETDATE(), @MyData);";

                    SqlParameter _ParamMyID = new SqlParameter("@MyID", SqlDbType.Int);
                    _ParamMyID.Value = _TheID;
                    _Command.Parameters.Add(_ParamMyID);

                    SqlParameter _ParamMyData = new SqlParameter("@MyData", SqlDbType.Udt);
                    _ParamMyData.UdtTypeName = "SQL#.Type_HashTable";
                    _ParamMyData.Value = Type_HashTable.Parse(@"testKey3=testVal3");
                    _Command.Parameters.Add(_ParamMyData);

                    _Connection.Open();
                    _Command.ExecuteNonQuery();
                }
            }
        }
    }
}

PS Si vous envoyez les données directement à une colonne UDT, celle-ci doit être sous forme binaire, car c'est le seul moyen par lequel SqlBulkCopy transporte, conformément au code source .


Réponse populaire

J'ai utilisé une notation d'interface explicite sur deux méthodes dans l'UDT, comme ceci.

void IBinarySerialize.Read( BinaryReader r )
{
}

void IBinarySerialize.Write( BinaryWriter w )
{
}

Mais ils devaient être définis comme suit:

public void Read( BinaryReader r )
{
}

public void Write( BinaryWriter w )
{
}

La différence était suffisante pour empêcher SQL Server d'identifier la méthode correcte à utiliser sur l'UDT pendant SqlBulkCopy et INSERT paramétré lors de la transmission de l'objet MyType complet.

Le problème a commencé lorsque j'ai utilisé Visual Studio pour ajouter les routines de stub qui implémentaient l'interface IBinarySerialize . J'ai cliqué avec le bouton droit sur le nom de l'interface en haut de la définition de la struct et choisi "Implémenter explicitement l'interface". J'aurais dû sélectionner "Implement Interface" pour générer les stubs de méthode sans les qualificateurs.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi