将XML插入SQL Server时如何解决“无法切换编码”错误

时间:2010-09-21 13:32:18

标签: .net sql-server xml utf-8 utf-16

我正在尝试插入XML列(SQL SERVER 2008 R2),但服务器抱怨:

  

System.Data.SqlClient.SqlException(0x80131904):
  XML解析:第1行,第39个字符,无法切换编码

我发现XML列必须是UTF-16才能使插入成功。

我正在使用的代码是:

 XmlSerializer serializer = new XmlSerializer(typeof(MyMessage));
 StringWriter str = new StringWriter();
 serializer.Serialize(str, message);
 string messageToLog = str.ToString();

如何将对象序列化为UTF-8字符串?

编辑:好的,抱歉混淆 - 字符串需要是UTF-8。你是对的 - 默认情况下它是UTF-16,如果我尝试以UTF-8插入它就会通过。所以问题是如何序列化为UTF-8。

示例

这会在尝试插入SQL Server时导致错误:

    <?xml version="1.0" encoding="utf-16"?>
    <MyMessage>Teno</MyMessage>

这不是:

    <?xml version="1.0" encoding="utf-8"?>
    <MyMessage>Teno</MyMessage>

更新

我想出SQL Server 2008的Xml列类型需要utf-8,以及当你尝试插入的xml规范的encoding属性中的utf-16时:

如果要添加utf-8,请将参数添加到SQL命令,如下所示:

 sqlcmd.Parameters.Add("ParamName", SqlDbType.VarChar).Value = xmlValueToAdd;

如果您尝试在前一行添加带有encoding=utf-16的xmlValueToAdd,则会在插入中产生错误。此外,VarChar表示不识别国家字符(它们最终显示为问号)。

要将utf-16添加到db,请在前面的示例中使用SqlDbType.NVarCharSqlDbType.Xml,或者根本不指定类型:

 sqlcmd.Parameters.Add(new SqlParameter("ParamName", xmlValueToAdd));

8 个答案:

答案 0 :(得分:34)

这个问题几乎与其他2个问题重复,令人惊讶的是 - 虽然这个问题是最新的 - 但我相信它缺少最佳答案。

重复,以及我认为最佳答案,是:

最后,只要XmlReader可以在应用程序服务器中本地解析它,无论声明或使用什么编码都无关紧要。

正如Most efficient way to read XML in ADO.net from XML type column in SQL server?中所确认的那样,SQL Server以高效的二进制格式存储XML。通过使用SqlXml类,ADO.net可以以此二进制格式与SQL Server通信,而不需要数据库服务器对XML进行任何序列化或反序列化。这对于通过网络传输也应该更有效。

通过使用SqlXml,XML将被预先解析到数据库,然后DB不需要知道有关字符编码的任何信息 - UTF-16或其他。特别要注意的是,XML声明甚至不会与数据库中的数据一起保留,无论使用哪种方法插入它。

请参阅上面链接的答案,了解与此非常相似的方法,但这个例子是我的:

using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.IO;
using System.Xml;

static class XmlDemo {
    static void Main(string[] args) {
        using(SqlConnection conn = new SqlConnection()) {
            conn.ConnectionString = "...";
            conn.Open();

            using(SqlCommand cmd = new SqlCommand("Insert Into TestData(Xml) Values (@Xml)", conn)) {

                cmd.Parameters.Add(new SqlParameter("@Xml", SqlDbType.Xml) {
                    // Works.
                    // Value = "<Test/>"

                    // Works.  XML Declaration is not persisted!
                    // Value = "<?xml version=\"1.0\"?><Test/>"

                    // Works.  XML Declaration is not persisted!
                    // Value = "<?xml version=\"1.0\" encoding=\"UTF-16\"?><Test/>"

                    // Error ("unable to switch the encoding" SqlException).
                    // Value = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Test/>"

                    // Works.  XML Declaration is not persisted!
                    Value = new SqlXml(XmlReader.Create(new StringReader("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Test/>")))
                });

                cmd.ExecuteNonQuery();
            }
        }
    }
}

请注意,我不会将最后一个(未注释的)示例视为“生产就绪”,而是将其保留为简洁易读。如果操作正确,StringReader和已创建的XmlReader都应在using语句中初始化,以确保在完成时调用其Close()方法。

从我所看到的,使用XML列时,XML声明永远不会持久化。例如,即使不使用.NET并仅使用此直接SQL插入语句,XML声明也不会使用XML保存到数据库中:

Insert Into TestData(Xml) Values ('<?xml version="1.0" encoding="UTF-8"?><Test/>');

现在就OP的问题而言,要序列化的对象仍然需要从MyMessage对象转换为XML结构,并且仍然需要XmlSerializer。但是,在最坏的情况下,而不是序列化为String,而是可以将消息序列化为XmlDocument - 然后可以通过新的XmlNodeReader将其传递给SqlXml - 避免使用序列化/序列化跳转到字符串。 (有关详细信息和示例,请参阅http://blogs.msdn.com/b/jongallant/archive/2007/01/30/how-to-convert-xmldocument-to-xmlreader-for-sqlxml-data-type.aspx。)

这里的所有内容都是针对.NET 4.0和SQL Server 2008 R2进行开发和测试的。

请不要浪费,方法是通过额外的转换运行XML(去反序列化和序列化 - 到DOM,字符串或其他方式),如此处和其他地方的其他答案所示。

答案 1 :(得分:21)

虽然.net字符串始终为UTF-16,但您需要使用UTF-16编码序列化对象。 那应该是这样的:

public static string ToString(object source, Type type, Encoding encoding)
{
    // The string to hold the object content
    String content;

    // Create a memoryStream into which the data can be written and readed
    using (var stream = new MemoryStream())
    {
        // Create the xml serializer, the serializer needs to know the type
        // of the object that will be serialized
        var xmlSerializer = new XmlSerializer(type);

        // Create a XmlTextWriter to write the xml object source, we are going
        // to define the encoding in the constructor
        using (var writer = new XmlTextWriter(stream, encoding))
        {
            // Save the state of the object into the stream
            xmlSerializer.Serialize(writer, source);

            // Flush the stream
            writer.Flush();

            // Read the stream into a string
            using (var reader = new StreamReader(stream, encoding))
            {
                // Set the stream position to the begin
                stream.Position = 0;

                // Read the stream into a string
                content = reader.ReadToEnd();
            }
        }
    }

    // Return the xml string with the object content
    return content;
}

通过将编码设置为Encoding.Unicode,不仅字符串为UTF-16,而且还应将xml字符串设为UTF-16

<?xml version="1.0" encoding="utf-16"?>

答案 2 :(得分:12)

告诉序列化程序不输出XML声明是不是最简单的解决方案? .NET和SQL应该对它们进行排序。

        XmlSerializer serializer = new XmlSerializer(typeof(MyMessage));
        StringWriter str = new StringWriter();
        using (XmlWriter writer = XmlWriter.Create(str, new XmlWriterSettings { OmitXmlDeclaration = true }))
        {
            serializer.Serialize(writer, message);
        }
        string messageToLog = str.ToString();

答案 3 :(得分:7)

我花了很长时间才重新解决这个问题。

我正在对SQL Server执行INSERT语句,如:

UPDATE Customers 
SET data = '<?xml version="1.0" encoding="utf-16"?><MyMessage>Teno</MyMessage>';

这会产生错误:

  

Msg 9402,Level 16,State 1,Line 2
  XML解析:第1行,第39个字符,无法切换编码

真正非常简单的解决办法是:

UPDATE Customers 
SET data = N'<?xml version="1.0" encoding="utf-16"?><MyMessage>Teno</MyMessage>';

区别在于Unicode字符串前缀为 N

  

N &#39;&lt;?xml version =&#34; 1.0&#34;编码=&#34; UTF-16&#34;&GT;特诺&LT; / MyMessage&GT;&#39;

在前一种情况下,假定未加前缀的字符串是varchar(例如,Windows-1252代码页)。当它遇到字符串中的encoding="utf-16"时,就会发生冲突(正确的是,因为字符串不是 utf-16)。

修复方法是将字符串作为 nvarchar (即UTF-16)传递给SQL服务器:

  

N &#39;&lt;?xml version =&#34; 1.0&#34;编码=&#34; UTF-16&#34;&GT;&#39;

这样字符串 UTF-16,它匹配XML所说的utf-16编码。可以这么说,地毯与窗帘相配。

答案 4 :(得分:5)

在.NET中字符串总是UTF-16,因此只要您留在托管应用程序中,就不必关心它是哪种编码。

问题更可能发生在与SQL服务器通信的地方。您的问题没有显示该代码,因此很难确定错误。我的建议是检查是否有可以在该代码上设置的属性或属性,用于指定发送到服务器的数据的编码。

答案 5 :(得分:4)

@ziesemer's answer(上方)是对该问题以及该问题的链接重复项的唯一完全正确的答案。但是,它仍然可以使用更多的解释和一些说明。将此视为@ziesemer答案的扩展。


即使产生期望的结果,该问题的大多数答案(包括重复的问题)也会令人费解,并经过许多不必要的步骤。这里的主要问题是对XML数据类型在SQL Server中的实际工作方式的总体了解不足(鉴于记录不足,不足为奇)。 XML类型:

  1. 是一种高度优化的(用于存储)类型,可将传入的XML转换为二进制格式(在msdn网站的某个位置进行了记录)。优化包括:
    1. 将数字和日期从字符串(如XML中的字符串)转换为二进制表示形式 IF 。元素或属性用类型info标记(这可能需要指定XML Schema Collection)。意思是,数字“ 1234567”存储为4字节的“ int”,而不是14字节的7位UTF-16字符串。
    2. 元素和属性名称存储在字典中,并具有数字ID。该数字ID在XML树结构中使用。意思是,“ <ElementName>...</ElementName>”以字符串形式占用27个字符(即54个字节),但以XML类型存储时仅占11个字符(即22个字节)。那是它的一个实例。多个实例占用了54个字节的其他倍数。但是在XML类型中,每个实例仅占用该数字ID的空间,很可能是4字节的整数。
  2. 将字符串存储为UTF-16 Little Endian,始终( )。这很可能是为什么不存储XML声明的原因:它完全没有必要,因为它始终是相同的,因为“ Encoding”属性无法更改。
  3. 没有XML声明假定编码为UTF-16,不是 UTF-8。
  4. 可以传入8位/非UTF-16数据。在这种情况下,您需要确保该字符串不是NVARCHAR字符串(<例如,在文字上没有前缀大写的“ N”,在处理T-SQL变量时未声明为NVARCHAR,在.NET中未声明为SqlDbType.NVarChar。并且,您需要确保 do 具有XML声明,并指定正确的编码。

    PRINT 'VARCHAR / UTF-8:';
    DECLARE @XML_VC_8 XML;
    SET @XML_VC_8 = '<?xml version="1.0" encoding="utf-8"?><test/>';
    PRINT 'Success!'
    -- Success!
    
    GO
    PRINT '';
    PRINT 'NVARCHAR / UTF-8:';
    DECLARE @XML_NVC_8 XML;
    SET @XML_NVC_8 = N'<?xml version="1.0" encoding="utf-8"?><test/>';
    PRINT 'Success!'
    /*
    Msg 9402, Level 16, State 1, Line XXXXX
    XML parsing: line 1, character 38, unable to switch the encoding
    */
    
    GO
    PRINT '';
    PRINT 'VARCHAR / UTF-16:';
    DECLARE @XML_VC_16 XML;
    SET @XML_VC_16 = '<?xml version="1.0" encoding="utf-16"?><test/>';
    PRINT 'Success!'
    /*
    Msg 9402, Level 16, State 1, Line XXXXX
    XML parsing: line 1, character 38, unable to switch the encoding
    */
    
    GO
    PRINT '';
    PRINT 'NVARCHAR / UTF-16:';
    DECLARE @XML_NVC_16 XML;
    SET @XML_NVC_16 = N'<?xml version="1.0" encoding="utf-16"?><test/>';
    PRINT 'Success!'
    -- Success!
    

    如您所见,当输入字符串为NVARCHAR时,可以包含XML声明 ,但它必须为“ UTF-16”。

    < / li>
  5. 当输入字符串为VARCHAR时,可以包含XML声明 ,但不能为“ UTF-16”。但是,它可以是任何有效的8位编码,在这种情况下,该编码的字节将转换为UTF-16,如下所示:

    DECLARE @XML XML;
    SET @XML = '<?xml version="1.0" encoding="utf-8"?><test attr="'
               + CHAR(0xF0) + CHAR(0x9F) + CHAR(0x98) + CHAR(0x8E) + '"/>';
    SELECT @XML;
    -- <test attr="" />
    
    
    SET @XML = '<?xml version="1.0" encoding="Windows-1255"?><test attr="'
               + CONVERT(VARCHAR(10), 0xF9ECE5ED) + '"/>';
    SELECT @XML AS [XML from Windows-1255],
           CONVERT(VARCHAR(10), 0xF9ECE5ED) AS [Latin1_General / Windows-1252];
    /*
    XML from Windows-1255    Latin1_General / Windows-1252
    <test attr="שלום" />     ùìåí
    */
    

    第一个示例为Smiling Face with Sunglasses指定了4字节的UTF-8序列,并且可以正确转换。
    第二个示例使用4个字节来表示组成单词“ Shalom”的4个希伯来字母,该单词正确转换并正确显示,前提是第一个字符“ F9”是ש字符,即在单词的右侧(因为希伯来语是从右到左的语言)。但是,由于当前数据库的默认归类为ùìåí,直接选择时,这些相同的4个字节将显示为Latin1_General_100_CS_AS_SC

答案 6 :(得分:1)

您正在序列化为字符串而不是字节数组,因此,此时尚未发生任何编码。

“messageToLog”的开头是什么样的? XML是否指定了一个后来证明是错误的编码(例如utf-8)?

修改

根据您的进一步信息,听起来字符串在传递给数据库时会自动转换为utf-8,但数据库会因为XML声明显示为utf-16而窒息。

在这种情况下,您需要序列化为utf-8。您需要使用XML中省略的“encoding =”进行序列化。 XmlFragmentWriter(不是.Net的标准部分,谷歌)可以让你这样做。

答案 7 :(得分:0)

xml序列化程序的默认编码应为UTF-16。只是为了确保你可以尝试 -

XmlSerializer serializer = new XmlSerializer(typeof(YourObject));

// create a MemoryStream here, we are just working
// exclusively in memory
System.IO.Stream stream = new System.IO.MemoryStream();

// The XmlTextWriter takes a stream and encoding
// as one of its constructors
System.Xml.XmlTextWriter xtWriter = new System.Xml.XmlTextWriter(stream, Encoding.UTF16);

serializer.Serialize(xtWriter, yourObjectInstance);

xtWriter.Flush();