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

Posted

技术标签:

【中文标题】将 XML 插入 SQL Server 时如何解决“无法切换编码”错误【英文标题】:How to solve "unable to switch the encoding" error when inserting XML into SQL Server 【发布时间】:2011-04-15 05:13:47 【问题描述】:

我正在尝试插入 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));

【问题讨论】:

你能不能不将所有内容都保存为 XML,而是在应用程序中将其转换为字符串,而只是让 SQL Server 尝试将其转换回 XML? 我收到了对象 - 我还没有 XML,这就是我需要的 仅供读者参考 - 几乎重复:***.com/questions/1564718/… 和 ***.com/questions/384974/… @Damien_The_Unbeliever - 是的,你可以!请看我刚刚提供的答案。 【参考方案1】:

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

重复的,以及我认为他们最好的答案是:

Using StringWriter for XML Serialization (2009-10-14) https://***.com/a/1566154/751158 Trying to store XML content into SQL Server 2005 fails (encoding problem) (2008-12-21) https://***.com/a/1091209/751158

最后,声明或使用什么编码都没有关系,只要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 将被预先解析发送到数据库,然后数据库不需要知道任何关于字符编码的信息 - 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。然而,在最坏的情况下,消息不是序列化为字符串,而是序列化为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 开发和测试的。

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

【讨论】:

事件我在将 xml 内容插入数据库时​​遇到了类似的问题。例如:插入 TestData(Xml) 值('')。这种陈述曾经失败,我得到“无法切换..”错误。后来我简单地将 N 预置为 xml 字符串,如下所示:插入 TestData(Xml) 值 (N'')。在此之后它开始工作!!! 您好,ziesemer,是否可以将此SqlXml 方法用作实体框架的一部分? ***.com/questions/32443571/… “数据库不需要知道任何关于字符编码的知识”。逻辑含义是比特和字节以外的东西以某种方式被传输。最好改写或省略它 - 关键是“二进制 XML”格式更有效。 优秀的不仅处理编码切换的问题,而且性能提升。从某种意义上说,我们在不使用它时仍然会出现错误,提醒我们应该以不同的方式进行操作。很好,你用更多的例子重复了这个答案:-) +1,请参阅我的答案,这是这个好答案的附录:***.com/a/53620185/577765【参考方案2】:

虽然 .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"?>

【讨论】:

就是这样。这是最灵活的 嗯,如果我在这里错了,请纠正我,但所有这些代码所做的只是在 XML 数据的顶部设置encoding="utf-16"。无论您对XmlTextWriter 使用什么编码,content 字符串都是 UTF-16。 是的。字符串是 UTF-8 还是 UTF-16 不是问题,正如您之前所说,它始终是 UTF-16。问题是设置 encoding="utf-16" 或 "utf-8"。 恕我直言,更好的选择是使用序列化程序设置,以便省略 XML 声明(或者,如果设置允许,仅省略 encoding 属性)。除非存储在文件中,否则在文档中声明编码是没有意义的,因为当您处理文本或某些 DOM 模型时,字节早就被解释为文本,这是所有编码信息都适用的。 【参考方案3】:

告诉序列化程序不要输出 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();

【讨论】:

【参考方案4】:

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

我在 SQL Server 中执行INSERT 语句,如下所示:

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

这给出了错误:

消息 9402,第 16 级,状态 1,第 2 行 XML解析:第1行,第39个字符,无法切换编码

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

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

区别在于 Unicode 字符串的前缀是 N

N'Teno'

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

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

N''

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

【讨论】:

【参考方案5】:

@ziesemer's answer(上图)是该问题的唯一完全正确答案以及该问题的链接副本。但是,它仍然可以使用更多的解释和一些澄清。将此视为@ziesemer 答案的扩展。


即使他们产生了预期的结果,这个问题的大多数答案(包括重复的问题)都是令人费解的,并且要经过许多不必要的步骤。这里的主要问题是总体上缺乏对XML 数据类型在 SQL Server 中的实际工作方式的理解(这并不奇怪,因为它没有很好的文档记录)。 XML 类型:

    是一种高度优化的(用于存储)类型,可将传入的 XML 转换为二进制格式(在 msdn 站点的某处记录)。优化包括:
      将数字和日期从字符串(就像它们在 XML 中一样)转换为二进制表示IF元素或属性用类型信息标记(这可能需要指定 XML 模式集合)。也就是说,数字“1234567”存储为 4 字节的“int”,而不是 7 位的 14 字节 UTF-16 字符串。 元素和属性名称存储在字典中并给出数字 ID。该数字 ID 用于 XML 树结构。意思是,“&lt;ElementName&gt;...&lt;/ElementName&gt;”以字符串形式占用 27 个字符(即 54 个字节),但存储在 XML 类型中时只占用 11 个字符(即 22 个字节)。那是它的一个实例。多个实例占用 54 个字节的额外倍数。但是在 XML 类型中,每个实例只占用那个数字 ID 的空间,很可能是一个 4 字节的 int。
    将字符串存储为 UTF-16 Little Endian,始终。这很可能是未存储 XML 声明的原因:它完全没有必要,因为它始终相同,因为“编码”属性永远不会改变。 没有 XML 声明假定编码是 UTF-16,不是 UTF-8。

    可以传入 8 位/非 UTF-16 数据。在这种情况下,您需要确保字符串不是NVARCHAR 字符串(即不以文字的大写“N”,在处理 T-SQL 变量时未声明为 NVARCHAR,在 .NET 中未声明为 SqlDbType.NVarChar)。并且,您需要确保您确实拥有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”。

    当输入字符串为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】:

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

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

【讨论】:

你是对的 - 似乎 Sql 被配置为在 xml 列中只接受 UTF-8。 +1 @veljkoz - SQL Server 不能接受 UTF-8 编码的 XML 值。对我来说,解决方案是去掉 XML 声明,因为它无论如何都不会与 XML 数据一起存储。见***.com/a/9002485/895218。 @NightShovel(和其他):是的,SQL Server XML 数据类型可以接受 UTF-8 编码值(甚至其他 8 位编码),就像只要 a) 您将值发送为 VarCharVarBinary(不是 NVarcharXml),并且 b) 字符串真正使用您声称它在 XML 声明中的编码进行编码。有关详细信息和示例,请参阅my answer(在此页面上):-)【参考方案7】:

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

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

编辑

根据您的进一步信息,当字符串被传递到数据库时,它听起来像 自动 转换为 utf-8,但是由于 XML 声明说它是 utf-16,所以数据库阻塞了。

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

【讨论】:

【参考方案8】:

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

【讨论】:

以上是关于将 XML 插入 SQL Server 时如何解决“无法切换编码”错误的主要内容,如果未能解决你的问题,请参考以下文章

如何从XML *向SQL Server DATE字段*中插入NULL

如何在 SQL Server 2005 中插入值数组?

如何设置“以属性的形式将xml数据插入到SQL Server2008数据库的表中”的存储过程

将 xml 字符串参数传递给 SQL Server 存储过程

如何将随机值插入 SQL Server 表?

将 XML 文档从 SQL SERVER 插入到 Oracle