【问题标题】:SQLBulkCopy with CLR UDT gives "Could not find method 'Read' for type 'MyNamespace.MyType' in assembly 'MyType'"带有 CLR UDT 的 SQLBulkCopy 给出“在程序集 'MyType' 中找不到类型 'MyNamespace.MyType' 的方法 'Read'”
【发布时间】:2017-01-15 10:32:22
【问题描述】:

我在 SQL Server 2012 中编写了一个 SQL Server CLR 用户定义类型 (UDT)。我已经能够通过 SQL 测试脚本访问它,并将它用作局部变量,在表中定义它,并且通过 Visual Studio 和 SQL Server Management Studio 对其进行了测试。

我们有一个服务,它以一种相当通用的方式使用 SQLBulkCopy 来拾取放置在目录中的文件,然后将它们的内容插入到适当的表中。当我将我的 UDT 作为其中一个表中的列添加时,我收到来自 WriteToServer( DataTable ) 调用的错误。

UDT 列作为 System.String 传递,希望在 SQL Server 中调用 UDT 的 Parse() 方法以将其转换为内部类型。我还尝试在此客户端程序中声明 UDT 类,并将数据直接作为 UDT 类型传递。

在任何一种情况下,我都会收到此错误消息(经过编辑以删除我的专有名称)

在程序集“MyType”中找不到类型“MyNamespace.MyType”的方法“Read”

我已经查看了尽可能多的关于此错误消息的类似问题,它们通常指的是 CREATE 语句的格式。此外,它们通常指的是 CLR 函数,而不是 CLR 类型,它们略有不同。这是我的:

创建类型 [dbo].[MyType]
外部名称 [MyType].[MyNamespace.MyType]

我怀疑这可能不是问题,而是与 SQLBulkCopy 如何与 SQLCLR UDT 交互有关。对于这种特殊的组合,很难找到任何深入的解释。

编辑 #1 - 这是自定义序列化。

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

编辑 #2 - 授予执行权限

GRANT EXECUTE 
ON TYPE :: MyType
TO PUBLIC 

编辑 #3 - 调整测试代码

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

更新者

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

这给出了与上面显示的相同的错误消息。

编辑 #4 - 从问题中消除 SqlBulkCopy
我使用参数化的 INSERT 重新创建了该问题。我将它设置为将 UDT 对象从客户端传递到服务器作为直接使用 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();

给予

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'

【问题讨论】:

  • 您的 UDT 是使用原生序列化还是自定义序列化?
  • 是自定义序列化。 [Serializable] [Microsoft.SqlServer.Server.SqlUserDefinedType(Format.UserDefined, MaxByteSize = -1)] public struct MyType: INullable, IBinarySerialize
  • 抱歉,我不知道如何使它看起来像注释中的代码块。我已经尝试了所有可以在帮助页面中找到的方法。
  • 没问题;-)。在 cmets 中,只允许降价的一个子集。您可以对代码使用反引号。看这里:Markdown Help。您可以通过单击编辑器右上角的 ? 进入该帮助页面,然后单击“高级帮助”。今晚或明天晚些时候我会看看这个。此外,如果您使用某人的 @ 姓名以便他们得到通知,这也会有所帮助。通知总是发送到评论所在的问题或答案的发布者。
  • 当您说您尝试“在此客户端程序中声明UDT类,并直接将数据作为UDT类型传递”时,您的意思是您设置了列输入DataTable 作为您的类型,然后加载数据?不确定是否需要直接调用 Parse 方法。但是看看这个answer。此外,要通过 BCP / SqlBulkCopy 直接发送数据,它需要采用二进制格式,如 source code。我现在正在测试。

标签: sql-server sql-server-2012 sqlclr sqlbulkcopy user-defined-types


【解决方案1】:

SqlBulkCopy 应该能够很好地处理 SQLCLR UDT(用户定义类型)。我已经成功使用了DbDataReaderDataTable 方法。

这对我有用:

C# 代码(我将“客户端”设为 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();
        }
    }
}

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

测试

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;

我还通过控制台应用程序使用SqlBulkCopy 和参数化的临时查询来完成这项工作:

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();
                }
            }
        }
    }
}

附:如果将数据直接发送到 UDT 列,则它需要采用二进制形式,因为这是 SqlBulkCopy 传输它的唯一方式,根据 source code

【讨论】:

  • 如果您的存储过程在 SQL Server 进程中执行而不是像我的测试中那样从单独的进程中执行,则可能会有所不同。 SQL Server 内部的调用可能不需要调用与 INSERT 或 SqlBulkCopy 从客户端应用程序传递同一对象的二进制图像时相同的处理路径。
  • @RonSanderson 我希望这是一个潜在的问题 ;-)。但遗憾的是,您不能使用内部Context Connection=true; 作为SqlBulkCopy 的目标连接字符串。这是一个完整的外部连接,我作为特权用户和“测试”用户都尝试过,除了我授予Public 的权限外,我没有其他权限。事实上你得到一个关于它的错误 finding Read 方法显然很奇怪,这就是为什么我要求查看 StackTrace,如果权限或版本控制可能是一个问题。
  • 产生 SQLException 的 SQL Server 端没有返回堆栈跟踪,客户端上也没有 InnerException 或服务器端堆栈跟踪。
  • 通过不同的处理路径,我并不是要暗示连接或权限可能是一个问题。相反,我更担心存储过程代码实际上是在与 UDT 相同的 CLRSQL 环境中执行的,这可能允许 SQL Server 以某种方式缩短 MyType UDT 的解析,从而允许解析 Read 方法。对于来自外部进程的调用,CLR 会显示一个二进制图像,该图像声称是 MyType 的一个实例,并且可能将其转换为正确的对象并调用 Read() 以某种方式通过不同的分辨率。
  • 下一步是将我的测试代码作为存储过程或函数移动到 SQL Server 中,看看是否可以用字符串调用它,然后转换一个 MyType 对象并成功插入。
【解决方案2】:

我在 UDT 中的两个方法上使用了显式接口表示法,就像这样。

void IBinarySerialize.Read( BinaryReader r )
{
}

void IBinarySerialize.Write( BinaryWriter w )
{
}

但它们必须像这样定义:

public void Read( BinaryReader r )
{
}

public void Write( BinaryWriter w )
{
}

差异足以阻止 SQL Server 在 SqlBulkCopy 期间识别在 UDT 上使用的正确方法,并在传递完整的 MyType 对象时进行参数化 INSERT。

当我使用 Visual Studio 添加实现 IBinarySerialize 接口的存根例程时,问题就开始了。我右键单击struct 定义顶部的接口名称并选择“显式实现接口”。我应该选择“实现接口”来生成没有限定符的方法存根。

【讨论】:

  • 我不知道您可以使用其他语法。显式实现接口有什么好处?您可以将 public 关键字添加到显式实现中吗?为什么要明确地实施它来绕过它公开的要求?我昨天尝试编译它,没有明确地编译它,没有public 并得到一个错误,所以我认为如果没有ReadWrite 方法是public,就不可能编译UDT,这就是我说的原因在一两条评论中,Read 方法必须存在并且可见。
  • 我真的不知道定义显式接口的含义。但是一旦我有了它,我确实尝试添加 public 并且它给出了编译错误。我需要更好地学习 C# 的这个微妙领域,以了解在什么情况下显式接口实现是合适的。我以前使用过它,但这只是因为我喜欢通过让接口名称限定方法名称来使接口的方法自我记录。似乎还有一些超出我所知道的含义。
  • 好的。是的,我对那些“暗示”以及为什么选择使用显式声明感到好奇。感谢您提到在显式声明中添加 public 不是一种选择。
猜你喜欢
  • 2012-10-21
  • 2020-04-04
  • 2012-01-11
  • 2020-06-28
  • 1970-01-01
  • 2021-01-21
  • 1970-01-01
  • 2020-06-27
  • 1970-01-01
相关资源
最近更新 更多