mssql-pythonでXMLデータを使う

Microsoft SQLは、サーバー側の処理機能を備えたネイティブxmlデータ型を提供します:

  • XMLコンテンツのクエリにはXQueryがあります。
  • 抽出および修正のためのXMLメソッド(value()query()exist()nodes()modify())。
  • 任意のXMLスキーマ検証。
  • パフォーマンスのためのXMLインデックス。

mssql-pythonドライバーはXMLデータを文字列として送受信します。 すべてのXML処理(XQuery、XMLメソッド、スキーマ検証)はMicrosoft SQL側で実行されます。 クライアント側のXML解析にはPythonのxml.etree.ElementTreeや類似のライブラリを使いましょう。

XMLとJSONのどちらを使うべきか: スキーマ検証、名前空間のサポート、またはテキストと要素を交互に組み合わせるコンテンツが必要な場合はXMLを使いましょう。 データがキーバリュー指向であったり、ウェブAPIで消費されている場合、スキーマの強制が不要な場合にはJSON(nvarchar列とMicrosoft SQLのJSON関数付き)を使いましょう。 ほとんどの新しいアプリケーションは、データ自体がドキュメント構造化でない限りJSONを好みます。

XMLデータを挿入する

XMLをPython文字列として渡します。ドライバーはそれをMicrosoft SQLのネイティブXML列タイプに送信します。

文字列として挿入

パラメータ化されたクエリを使って、XMLコンテンツを文字列としてXML列に直接挿入します。

import mssql_python
from xml.etree import ElementTree as ET

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes"
)
cursor = conn.cursor()

# Create temp table for XML storage
cursor.execute("CREATE TABLE #XMLOrders (OrderID INT IDENTITY(1,1) PRIMARY KEY, OrderXML XML)")

# Insert XML as string
xml_data = """
<Order OrderID="1001">
    <Customer Name="John Doe" Email="john@example.com"/>
    <Items>
        <Item ProductID="A1" Quantity="2" Price="29.99"/>
        <Item ProductID="B2" Quantity="1" Price="49.99"/>
    </Items>
</Order>
"""

cursor.execute("""
    INSERT INTO #XMLOrders (OrderXML) VALUES (%(xml)s)
""", {"xml": xml_data})
conn.commit()

ElementTreeでXMLを構築する

PythonのElementTreeライブラリを使ってXMLドキュメントをプログラム的に構築し、挿入用の文字列に変換します。

from xml.etree import ElementTree as ET

# Build XML document
order = ET.Element("Order", OrderID="1002")
customer = ET.SubElement(order, "Customer", Name="Jane Smith", Email="jane@example.com")
items = ET.SubElement(order, "Items")
ET.SubElement(items, "Item", ProductID="C3", Quantity="3", Price="19.99")
ET.SubElement(items, "Item", ProductID="D4", Quantity="2", Price="39.99")

# Convert to string
xml_string = ET.tostring(order, encoding="unicode")

cursor.execute("""
    INSERT INTO #XMLOrders (OrderXML) VALUES (%(xml)s)
""", {"xml": xml_string})
conn.commit()

挿入前にXMLを検証してください

サーバー上でSQLのTRY/CATCHおよびXML型キャスティングを用いてXML構文を検証し、保存前に誤った文書を拒否します。

# Validate XML with TRY/CATCH
cursor.execute("""
    BEGIN TRY
        DECLARE @xml XML = CAST(%(xml)s AS XML);
        SELECT @xml.value('(/Order/@OrderID)[1]', 'INT') AS Validated;
    END TRY
    BEGIN CATCH
        THROW;
    END CATCH
""", {"xml": xml_data})

XMLデータのクエリ

サーバー上でXMLメソッドを使って、クライアントに完全なドキュメントを取得せずに特定の値を抽出できます。

XML value() メソッド

XPath式を用いた value() メソッドを用いてXMLから単一のスカラー値や属性を抽出します。

XMLからスカラー値を抽出する:

cursor.execute("""
    SELECT 
        OrderID,
        OrderXML.value('(/Order/@OrderID)[1]', 'INT') AS XmlOrderID,
        OrderXML.value('(/Order/Customer/@Name)[1]', 'NVARCHAR(100)') AS CustomerName,
        OrderXML.value('(/Order/Customer/@Email)[1]', 'NVARCHAR(100)') AS Email
    FROM #XMLOrders
""")

for row in cursor:
    print(f"Order {row.XmlOrderID}: {row.CustomerName} ({row.Email})")

XML query() メソッド

XPath式を使って文書からXML断片(スカラー値ではなく)を返します。

XML断片を抽出する:

cursor.execute("""
    SELECT 
        OrderID,
        OrderXML.query('/Order/Items') AS ItemsXML
    FROM #XMLOrders
    WHERE OrderID = %(id)s
""", {"id": 1})

row = cursor.fetchone()
# row.ItemsXML is an XML string
items_xml = row.ItemsXML
print(items_xml)

XML exist() メソッド

XPathの式がドキュメント内の任意のノードと一致し、trueの場合は1、falseなら0を返すかどうかをテストします。

XPathが一致しているか確認してください:

cursor.execute("""
    SELECT OrderID, OrderXML
    FROM #XMLOrders
    WHERE OrderXML.exist('/Order/Items/Item[@ProductID="A1"]') = 1
""")

for row in cursor:
    print(f"Order {row.OrderID} contains product A1")

XML nodes() メソッド(行へのシュレッド)

CROSS APPLY演算子とnodes()メソッドを使って、ネストされたXML要素をリレーショナル行セットに変換します。

XMLをリレーショナル形式に変換する:

cursor.execute("""
    SELECT 
        o.OrderID,
        Items.Item.value('@ProductID', 'VARCHAR(10)') AS ProductID,
        Items.Item.value('@Quantity', 'INT') AS Quantity,
        Items.Item.value('@Price', 'DECIMAL(10,2)') AS Price
    FROM #XMLOrders o
    CROSS APPLY o.OrderXML.nodes('/Order/Items/Item') AS Items(Item)
    WHERE o.OrderID = %(id)s
""", {"id": 1})

for row in cursor:
    print(f"Product {row.ProductID}: {row.Quantity} x ${row.Price}")

XMLデータを修正する

XQuery DML式で使う XML.modify() を使い、XMLコンテンツをその場で更新してください。

XML modify() と挿入

XQueryのmodify()操作を使ってinsertメソッドを使ってXML文書に新しい要素を追加します。

cursor.execute("""
    UPDATE #XMLOrders
    SET OrderXML.modify('
        insert <Item ProductID="E5" Quantity="1" Price="59.99"/>
        into (/Order/Items)[1]
    ')
    WHERE OrderID = %(id)s
""", {"id": 1})
conn.commit()

XML modify() と delete

XQueryのmodify()操作を用いて、deleteメソッドを使ってXML文書から要素やノードを削除できます。

cursor.execute("""
    UPDATE #XMLOrders
    SET OrderXML.modify('
        delete /Order/Items/Item[@ProductID="A1"]
    ')
    WHERE OrderID = %(id)s
""", {"id": 1})
conn.commit()

XML modify() を置き換える

XQueryのmodify()操作を使って、replace value ofメソッドを使ってXMLドキュメント内の属性値や要素テキストを更新できます。

cursor.execute("""
    UPDATE #XMLOrders
    SET OrderXML.modify('
        replace value of (/Order/Items/Item[@ProductID="B2"]/@Price)[1]
        with 54.99
    ')
    WHERE OrderID = %(id)s
""", {"id": 1})
conn.commit()

FOR XML クエリ

クエリに FOR XML を追加して結果を単一のXML文字列として返します。

FOR XML RAW

各行が属性として列を付けた単純な要素となるXMLを生成します。

cursor.execute("""
    SELECT ProductID, Name, ListPrice
    FROM Production.Product
    WHERE ProductSubcategoryID = %(cat)s
    FOR XML RAW('Product'), ROOT('Products')
""", {"cat": 1})

xml_result = cursor.fetchval()
print(xml_result)
# <Products><Product ProductID="1" Name="..." ListPrice="..."/></Products>

XML AUTO用

クエリのジョイン階層を自動的に反映するネスト構造を持つXMLを生成してください。

cursor.execute("""
    SELECT sc.Name AS SubcategoryName, p.Name AS ProductName, p.ListPrice
    FROM Production.ProductSubcategory sc
    JOIN Production.Product p ON sc.ProductSubcategoryID = p.ProductSubcategoryID
    WHERE sc.ProductSubcategoryID = %(cat)s
    FOR XML AUTO, ROOT('Catalog')
""", {"cat": 1})

xml_result = cursor.fetchval()
# Nested XML structure based on join hierarchy

XMLパスについて

明示的な列のエイリアスやサブクエリを用いて、ネストや要素名を制御するカスタムXML構造を構築します。

XML構造に対する最も多くのコントロール:

cursor.execute("""
    SELECT 
        sc.ProductSubcategoryID AS '@ID',
        sc.Name AS 'Name',
        (
            SELECT p.ProductID AS '@ID',
                   p.Name AS 'Name',
                   p.ListPrice AS 'Price'
            FROM Production.Product p
            WHERE p.ProductSubcategoryID = sc.ProductSubcategoryID
            FOR XML PATH('Product'), TYPE
        ) AS 'Products'
    FROM Production.ProductSubcategory sc
    WHERE sc.ProductSubcategoryID = %(cat)s
    FOR XML PATH('Subcategory'), ROOT('Catalog')
""", {"cat": 1})

xml_result = cursor.fetchval()

PythonでのXML解析

XMLを文字列として取得し、 xml.etree.ElementTree または互換性のあるライブラリで解析します。

ElementTreeでクエリ結果を解析

データベースからXMLを取得し、ElementTreeを使ってPythonオブジェクトツリーに解析し、DOMを通じて要素や属性にアクセスします。

from xml.etree import ElementTree as ET

cursor.execute("""
    SELECT CatalogDescription FROM Production.ProductModel
    WHERE CatalogDescription IS NOT NULL AND ProductModelID = 19
""")

row = cursor.fetchone()
root = ET.fromstring(row.CatalogDescription)

# Navigate XML structure using namespace
ns = {'pd': 'http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/ProductModelDescription'}
summary = root.find('.//pd:Summary', ns)
if summary is not None:
    # Get all text content
    text = ''.join(summary.itertext()).strip()
    print(f"Summary: {text[:100]}")

XMLを辞書に変換する

XML木構造をネストされたPython辞書に変換し、ネストされたデータへのプログラム的アクセスを容易にします。

def xml_to_dict(element):
    """Convert XML element to dictionary."""
    result = {}
    
    # Add attributes
    if element.attrib:
        result['@attributes'] = element.attrib
    
    # Add children
    children = list(element)
    if children:
        child_dict = {}
        for child in children:
            child_result = xml_to_dict(child)
            if child.tag in child_dict:
                # Convert to list if multiple same-named children
                if not isinstance(child_dict[child.tag], list):
                    child_dict[child.tag] = [child_dict[child.tag]]
                child_dict[child.tag].append(child_result)
            else:
                child_dict[child.tag] = child_result
        result.update(child_dict)
    
    # Add text content
    if element.text and element.text.strip():
        result['#text'] = element.text.strip()
    
    return result

# Usage
cursor.execute("""
    SELECT CatalogDescription FROM Production.ProductModel
    WHERE CatalogDescription IS NOT NULL AND ProductModelID = 19
""")
xml_string = cursor.fetchval()
root = ET.fromstring(xml_string)
data = xml_to_dict(root)

大きなXML文書の処理

Microsoft SQL では、大きな FOR XML 結果が複数行に分割されることがあるため、解析する前に各部分を連結してください。

チャンクでXMLを取得する

FOR XMLが大きな結果セットを返す場合、Microsoft SQLはXMLを複数の行に分割し、すべての部分を1つのドキュメントに統合することがあります。

# FOR XML might split large results across rows
cursor.execute("""
    SELECT * FROM LargeTable FOR XML RAW
""")

xml_parts = []
for row in cursor:
    xml_parts.append(row[0])

full_xml = "".join(xml_parts)

ストリームXML解析

非常に大きなXMLドキュメントの場合は、反復解析を用いて、ツリー全体をメモリにロードせずに要素を一つずつ処理します。

from xml.etree import ElementTree as ET
import io

cursor.execute("""
    SELECT Instructions FROM Production.ProductModel
    WHERE Instructions IS NOT NULL AND ProductModelID = 7
""")
xml_string = cursor.fetchval()

# Use iterparse for memory-efficient parsing
xml_stream = io.StringIO(xml_string)
for event, elem in ET.iterparse(xml_stream, events=['end']):
    if elem.tag.endswith('step'):
        # Process step element
        text = ''.join(elem.itertext()).strip()
        if text:
            print(f"Step: {text[:60]}")
        # Free memory
        elem.clear()

XML インデックス

Microsoft SQLでインデックスを作成して、より高速なXMLクエリを行使します:

-- Primary XML index (assumes a table with an XML column)
CREATE PRIMARY XML INDEX PIX_XMLOrders_OrderXML
ON #XMLOrders(OrderXML);

-- Secondary indexes for specific access patterns
CREATE XML INDEX SIX_XMLOrders_Path
ON #XMLOrders(OrderXML)
USING XML INDEX PIX_XMLOrders_OrderXML
FOR PATH;

CREATE XML INDEX SIX_XMLOrders_Value
ON #XMLOrders(OrderXML)
USING XML INDEX PIX_XMLOrders_OrderXML
FOR VALUE;

名前空間を扱う

XQuery式で declare namespace 構文を使って名前空間プレフィックスをインラインで宣言します。

名前空間付きのXMLクエリ

XPathの式に名前空間宣言を追加して、特定の名前空間内の要素にマッチさせます。

xml_with_ns = """
<Order xmlns="http://example.com/orders" 
       xmlns:c="http://example.com/customer">
    <c:Customer Name="John Doe"/>
    <Items>
        <Item ProductID="A1"/>
    </Items>
</Order>
"""

cursor.execute("""
    SELECT OrderXML.value('
        declare namespace o="http://example.com/orders";
        declare namespace c="http://example.com/customer";
        (/o:Order/c:Customer/@Name)[1]
    ', 'NVARCHAR(100)') AS CustomerName
    FROM #XMLOrders
    WHERE OrderID = %(id)s
""", {"id": 1})

Pythonでの名前空間XMLの解析

Pythonで名前空間とXMLを解析する際は、find()や他の要素アクセスコールで名前空間のマッピングを宣言してください。

from xml.etree import ElementTree as ET

cursor.execute("""
    SELECT TOP 1 
        (SELECT SalesOrderID AS [@OrderID],
                TotalDue AS [@Total],
                CustomerID AS [Customer/@ID]
         FROM Sales.SalesOrderHeader
         WHERE SalesOrderID = 43659
         FOR XML PATH('Order'))
""")
xml_string = cursor.fetchval()
root = ET.fromstring(xml_string)
order_id = root.get('OrderID')
total = root.get('Total')
print(f"Order {order_id}: ${total}")

ベスト プラクティス

これらのガイドラインを適用してXMLデータを効率的に扱いましょう。

JSONではなくXMLを選ぶ

データ構造や処理要件に応じて、Microsoft SQLのネイティブxml型を使うか、JSONをnvarcharに保存するかを決めてください。

この比較は、SQLのxml型を使うかMicrosoft JSONをnvarcharに保存するかを判断するのに役立ちます。

特徴 XML JSON
スキーマの検証 ネイティブ サポート ネイティブサポートなし
名前空間 完全なサポート サポートなし
属性 サポートされている 直接同等の機能はありません
混合コンテンツ サポートされている サポートしていません
ドキュメント処理 より良い より構造化されていない

SELECT* は避けてください

特定の値だけが必要な場合は、完全なXMLドキュメントを取得しないでください。 XMLメソッドを使ってサーバー上でデータを抽出します。 この方法はネットワークトラフィックを減らし、Pythonでの大規模なXML文書の解析を回避できます。

# Create sample XML table for performance demos
cursor.execute("""
    CREATE TABLE #XMLPerf (
        OrderID INT,
        OrderXML XML
    )
""")
cursor.execute("""
    INSERT INTO #XMLPerf (OrderID, OrderXML) VALUES
    (1, '<Order><Customer Name="Alice"/><Item ProductID="1" Qty="2" Price="29.99"/></Order>'),
    (2, '<Order><Customer Name="Bob"/><Item ProductID="3" Qty="1" Price="49.99"/></Order>')
""")

# Anti-pattern: Downloads entire XML per row
cursor.execute("SELECT * FROM #XMLPerf")

# Better: Extract only the values you need on the server
cursor.execute("""
    SELECT OrderID,
           OrderXML.value('(/Order/Customer/@Name)[1]', 'NVARCHAR(100)') AS Customer
    FROM #XMLPerf
""")

NULL XML の処理

XMLデータをクエリする際は、XMLメソッドがNULLを返す際にデフォルト値を指定するために COALESCE() を使用します。

cursor.execute("""
    SELECT OrderID,
           COALESCE(OrderXML.value('(/Order/@Total)[1]', 'DECIMAL(10,2)'), 0) AS Total
    FROM #XMLPerf
""")