一、XML 型別成為一等公民:TDS 協議層面的變革
SQL Server 2005 引入原生 `xml` 資料型別,TDS (Tabular Data Stream) 協議新增專屬型別標識符 `0xF1` (XML_F52E2B61-18A1-11d1-B105-000805F49916B)。查詢分析器顯示為單欄單列實為 XML 串流,客戶端必須使用 SqlCommand.ExecuteXmlReader 正確讀取。
二、FOR XML 增強:從串流到型別化 XML
| 功能 | SQL 2000 | SQL 2005 新增 |
| 模式 | RAW, AUTO, EXPLICIT | PATH (簡化屬性/元素控制,替代 EXPLICIT) |
| 回傳型別 | 僅 XML 串流 (特殊 TDS 型別) | TYPE 指令 直接回傳 `xml` 型別 |
| 巢狀查詢 | 不支援 | 巢狀 FOR XML 子查詢產生階層結構 |
| 根元素 | ROOT 指令僅支援片段 | ROOT 可產生完整 XML 文件 |
| 結構描述 | 無 | XMLSCHEMA 指令內嵌 XSD |
範例:PATH 模式 + TYPE 回傳 xml 型別
SELECT
OrderID AS '@OrderID',
OrderDate AS '@Date',
(SELECT ProductID, Qty, Price
FROM OrderDetails od
WHERE od.OrderID = o.OrderID
FOR XML PATH('Item'), TYPE) AS Items
FROM Orders o
WHERE CustomerID = 'ALFKI'
FOR XML PATH('Order'), ROOT('Orders'), TYPE;
三、ADO.NET 2.0 SqlClient 增強
1. 新型別枚舉值
// System.Data.DbType
DbType.Xml
// System.Data.SqlDbType
SqlDbType.Xml
2. SqlXml 類別:XML 值的標準載體
namespace System.Data.SqlTypes
{
public sealed class SqlXml : INullable
{
public static SqlXml Null { get; }
public bool IsNull { get; }
public XmlReader CreateReader(); // 核心方法:取得唯讀、前進式 XmlReader
public void WriteTo(XmlWriter writer);
// 隱式轉換:string → SqlXml, XmlReader → SqlXml
}
}
3. DataSet 支援 Xml 型別資料行
DataTable dt = new DataTable();
dt.Columns.Add("DocId", typeof(int));
dt.Columns.Add("Content", typeof(SqlXml)); // 或 typeof(string) 配合 DbType.Xml
// Fill 時自動將 xml 型別欄位映射為 SqlXml 實例
四、客戶端完整存取模式
模式 A:ExecuteXmlReader — 針對 FOR XML ... TYPE 查詢
using (SqlConnection conn = new SqlConnection(connStr))
using (SqlCommand cmd = new SqlCommand(
"SELECT Content FROM Docs WHERE DocId = @id FOR XML PATH('Doc'), TYPE", conn))
{
cmd.Parameters.Add("@id", SqlDbType.Int).Value = 1;
conn.Open();
using (XmlReader reader = cmd.ExecuteXmlReader())
{
// 處理單一 XML 文件 (可能包含多個根元素)
while (reader.Read())
{
// 邏輯處理...
}
}
}
模式 B:SqlParameter + SqlDbType.Xml — 參數化傳入/輸出 XML
// 輸入 XML 參數
string xmlInput = @" ";
using (SqlCommand cmd = new SqlCommand("usp_ProcessOrder", conn))
{
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@OrderXml", SqlDbType.Xml).Value = new SqlXml(new XmlTextReader(new StringReader(xmlInput)));
// 或直接 .Value = xmlInput (隱式轉換)
cmd.ExecuteNonQuery();
}
// 輸出 XML 參數
SqlParameter outParam = cmd.Parameters.Add("@ResultXml", SqlDbType.Xml);
outParam.Direction = ParameterDirection.Output;
cmd.ExecuteNonQuery();
SqlXml resultXml = (SqlXml)outParam.Value;
using (XmlReader r = resultXml.CreateReader()) { /* ... */ }
模式 C:DataAdapter Fill — 將 xml 欄位載入 DataTable
DataTable dt = new DataTable();
using (SqlDataAdapter da = new SqlDataAdapter("SELECT DocId, Content FROM Docs", conn))
{
da.Fill(dt);
foreach (DataRow row in dt.Rows)
{
SqlXml xml = (SqlXml)row["Content"];
if (!xml.IsNull)
{
XPathDocument doc = new XPathDocument(xml.CreateReader());
// XPath/XSLT 處理...
}
}
}
五、伺服器端 XML 方法與客戶端協同
| 伺服器方法 | 客戶端對應處理 | 典型用途 |
query() | `SqlXml` → `XmlReader` → `XDocument.Load` / `XPathNavigator` | 擷取 XML 片段 |
value() | 直接回傳 CLR 型別 (int, string, DateTime...) | 純量值投影,避免 XML 解析 |
exist() | 回傳 `bool` / `SqlBoolean` | WHERE 篩選,下推至伺服器 |
nodes() | 多資料列結果集,正常 `SqlDataReader` 讀取 | XML 切碎為關係列,客戶端無感知 |
modify() | 僅 `UPDATE` 陳述式,客戶端 `ExecuteNonQuery` | 部分 XML 更新 |
六、效能最佳實踐
- 優先使用 `value()` / `exist()`:將篩選/投影下推伺服器,減少網路傳輸與客戶端解析
- 大型 XML 分塊讀取:`ExecuteXmlReader` 配合 `XmlReader` 流式處理,避免載入完整 DOM
- 參數化 XML 輸入:使用 `SqlDbType.Xml` 參數,防止注入、利用計劃快取
- 避免 `SELECT *` 含 XML 欄位:明確列出所需欄位,或用 `value()` 只取純量
- 連線池與非同步:`BeginExecuteXmlReader` / `EndExecuteXmlReader` 實現非同步管線
七、常見陷阱與解決方案
| 問題 | 原因 | 解決 |
| XmlReader 讀取後無法重置 | 前進式唯讀流 | 需重複查詢或改用 `XDocument.Load(reader)` 緩存 |
| FOR XML 無 TYPE 回傳亂碼 | TDS 特殊型別被視為字串 | 必須加 `TYPE` 指令或用 `ExecuteXmlReader` |
| 大量 XML 參數導致 LOH 壓力 | 字串 > 85KB 進入 Large Object Heap | 使用 `SqlXml` + `XmlReader` 流式傳遞,避免大字串 |
| DataSet 序列化 XML 欄位失敗 | Remoting/WCF 不支援 SqlXml | 轉為 `string` 或 `byte[]` 傳輸,接收端重建 `SqlXml` |
八、完整範例:訂單系統 XML 工作流
// 1. 取得客戶訂單摘要 (伺服器端 FOR XML PATH + TYPE)
const string sql = @"
SELECT
CustomerID AS '@Id',
(SELECT OrderID, OrderDate, TotalAmt
FROM Sales.Orders
WHERE CustomerID = c.CustomerID
FOR XML PATH('Order'), TYPE) AS Orders
FROM Sales.Customers c
WHERE CustomerID = @CustId
FOR XML PATH('Customer'), ROOT('Data'), TYPE;";
using (SqlConnection conn = new SqlConnection(connStr))
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.Add("@CustId", SqlDbType.NVarChar, 5).Value = "ALFKI";
conn.Open();
using (XmlReader reader = cmd.ExecuteXmlReader())
{
XDocument doc = XDocument.Load(reader);
// 2. 客戶端 LINQ to XML 轉換為 HTML 報表
var html = new XElement("table",
doc.Descendants("Order").Select(o =>
new XElement("tr",
new XElement("td", (string)o.Attribute("OrderID")),
new XElement("td", (DateTime)o.Attribute("OrderDate")),
new XElement("td", (decimal)o.Attribute("TotalAmt"))
)));
File.WriteAllText("OrderReport.html", html.ToString());
}
}
下篇將深入:`SqlXml` 在 Entity Framework Core / Dapper 中的型別映射、XML 結構描述集合驗證的客戶端/伺服器端協同、大型 XML 串流的非同步管線設計、以及雲端遷移 (Azure SQL / SQL MI) 的 XML 型別相容性測試。