使用 go-mssqldb 的 JSON 與 XML 資料

SQL Server 內建對 JSON 和 XML 資料的支援。 本文說明如何利用go-mssqldb驅動程式查詢、插入及轉換 Go 結構與 SQL Server 之間的 JSON 與 XML 資料。

本文範例與 AdventureWorks2025 範例資料庫對比。 以讀取為導向的範例查詢內建物件如 Sales.vSalesPersonSales.SalesOrderHeader。 以寫入為導向的範例使用 OPENJSON 和 XML .nodes() 將資料列插入到 HumanResources.Department 中。

大多數片段 ctx 假設且 db 已初始化。 你可以使用以下設定模式:

import (
    "context"
    "database/sql"
    "log"
    "time"

    _ "github.com/microsoft/go-mssqldb"
)

db, err := sql.Open("sqlserver", "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&encrypt=true&TrustServerCertificate=true")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()

JSON 資料

使用 FOR JSON 將查詢結果轉換為 JSON

FOR JSON PATH 來以 JSON 字串回傳查詢結果:

var jsonResult string
err := db.QueryRowContext(ctx,
    "SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson WHERE CountryRegionName = @dept FOR JSON PATH",
    sql.Named("dept", "United States")).Scan(&jsonResult)
if err != nil {
    log.Fatal(err)
}
fmt.Println(jsonResult)

Note

對於較大的承載資料,SQL Server 可以將 FOR JSON 輸出分成多個資料列傳回。 如果你需要可靠地支援較大的結果,請閱讀所有回傳的列並將它們串接起來。 請參見 處理大型 JSON 結果

處理大型 JSON 結果

當 SQL Server 回傳多列的 JSON 輸出時,先將區塊串接起來,再解封組或列印結果:

import "strings"

func queryJSON(ctx context.Context, db *sql.DB, query string, args ...any) (string, error) {
    rows, err := db.QueryContext(ctx, query, args...)
    if err != nil {
        return "", err
    }
    defer rows.Close()

    var sb strings.Builder
    for rows.Next() {
        var chunk string
        if err := rows.Scan(&chunk); err != nil {
            return "", err
        }
        sb.WriteString(chunk)
    }
    if err := rows.Err(); err != nil {
        return "", err
    }
    return sb.String(), nil
}

使用方式:

jsonStr, err := queryJSON(ctx, db,
    "SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson FOR JSON PATH")
if err != nil {
    log.Fatal(err)
}

將 JSON 結果反序列化為 Go 結構體

FOR JSONencoding/json 結合,可直接將 SQL Server 結果反序列化為 Go 型別:

import "encoding/json"

type Employee struct {
    ID         int    `json:"Id"`
    Name       string `json:"Name"`
    Department string `json:"Department"`
}

func getEmployees(ctx context.Context, db *sql.DB, dept string) ([]Employee, error) {
    var jsonStr string
    err := db.QueryRowContext(ctx,
        "SELECT BusinessEntityID AS Id, FirstName + ' ' + LastName AS Name, CountryRegionName AS Department FROM Sales.vSalesPerson WHERE CountryRegionName = @dept FOR JSON PATH",
        sql.Named("dept", dept)).Scan(&jsonStr)
    if err != nil {
        return nil, err
    }

    var employees []Employee
    if err := json.Unmarshal([]byte(jsonStr), &employees); err != nil {
        return nil, err
    }
    return employees, nil
}

嵌套 JSON 與 FOR JSON PATH

透過欄位別名中的點符號來建立巢狀 JSON 結構:

var jsonResult string
err := db.QueryRowContext(ctx, `
    SELECT
        soh.SalesOrderID AS [Id],
        soh.OrderDate AS [OrderDate],
        CONCAT(pp.FirstName, ' ', pp.LastName) AS [Customer.Name],
        ea.EmailAddress AS [Customer.Email],
        soh.TotalDue AS [Total]
    FROM Sales.SalesOrderHeader AS soh
    JOIN Sales.Customer AS c ON soh.CustomerID = c.CustomerID
    LEFT JOIN Person.Person AS pp ON c.PersonID = pp.BusinessEntityID
    LEFT JOIN Person.EmailAddress AS ea ON pp.BusinessEntityID = ea.BusinessEntityID
    WHERE soh.SalesOrderID = @id
    FOR JSON PATH, WITHOUT_ARRAY_WRAPPER`,
    sql.Named("id", 43659)).Scan(&jsonResult)

結果如下:

{"Id":43659,"OrderDate":"2022-05-30T00:00:00","Customer":{"Name":"James Hendergart","Email":"james9@adventure-works.com"},"Total":23153.2339}

用 OPENJSON 解析 SQL Server 中的 JSON

OPENJSON 來將 JSON 陣列切碎成伺服器端的列:

jsonData := `[
    {"Name": "Alice", "Department": "Engineering"},
    {"Name": "Bob", "Department": "Marketing"}
]`

rows, err := db.QueryContext(ctx, `
    SELECT Name, Department
    FROM OPENJSON(@json)
    WITH (
        Name NVARCHAR(100) '$.Name',
        Department NVARCHAR(100) '$.Department'
    )`, sql.Named("json", jsonData))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var name, dept string
    if err := rows.Scan(&name, &dept); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s - %s\n", name, dept)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

將 JSON 資料插入為參數

將 Go 結構體以 JSON 字串形式傳送給儲存程序,或 OPENJSON

type OrderItem struct {
    ProductID int     `json:"productId"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

func sendJSONPayload(ctx context.Context, db *sql.DB, metadata map[string]any) error {
    jsonBytes, err := json.Marshal(metadata)
    if err != nil {
        return err
    }

    // Pass JSON to a stored procedure for server-side processing.
    _, err = db.ExecContext(ctx,
        "EXECUTE dbo.ProcessDepartmentMetadata @payload",
        sql.Named("payload", string(jsonBytes)))
    return err
}

使用方式:

metadata := map[string]any{
    "source":  "docs-sample",
    "batchId": "b-20260709-01",
    "departments": []map[string]any{
        {"name": "Data Science", "groupName": "Research and Development"},
        {"name": "Cloud Ops", "groupName": "Information Technology"},
    },
}

if err := sendJSONPayload(ctx, db, metadata); err != nil {
    log.Fatal(err)
}

從 JSON 進行批次插入,使用 OPENJSON

在單一陳述式中從 JSON 陣列插入多筆資料列:

func bulkInsertFromJSON(ctx context.Context, db *sql.DB, jsonData string) (int64, error) {
    result, err := db.ExecContext(ctx, `
        INSERT INTO HumanResources.Department (Name, GroupName)
        SELECT Name, GroupName
        FROM OPENJSON(@json)
        WITH (
            Name NVARCHAR(50) '$.name',
            GroupName NVARCHAR(50) '$.groupName'
        )`, sql.Named("json", jsonData))
    if err != nil {
        return 0, err
    }
    return result.RowsAffected()
}

使用方式:

jsonData := `[
    {"name": "Data Science", "groupName": "Research and Development"},
    {"name": "Cloud Ops", "groupName": "Information Technology"},
    {"name": "Developer Relations", "groupName": "Sales and Marketing"}
]`

rowsInserted, err := bulkInsertFromJSON(ctx, db, jsonData)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Inserted %d rows\n", rowsInserted)

JSON_VALUE 與 JSON_QUERY

從 JSON 欄位擷取數值,而不傳送整個 JSON 文件:

// Extract a scalar value.
var productName string
err := db.QueryRowContext(ctx, `
    SELECT Name
    FROM Production.Product
    WHERE ProductID = @id`,
    sql.Named("id", 1)).Scan(&productName)
if err != nil {
    log.Fatal(err)
}
fmt.Println(productName)

// Extract a JSON array from a FOR JSON subquery.
var itemsJSON string
err = db.QueryRowContext(ctx, `
    SELECT (
        SELECT ProductID, Name
        FROM Production.Product
        WHERE ProductSubcategoryID = @subId
        FOR JSON PATH
    ) AS Items`,
    sql.Named("subId", 1)).Scan(&itemsJSON)
if err != nil {
    log.Fatal(err)
}
fmt.Println(itemsJSON)

小提示

JSON_VALUE 回傳一個純量值(字串、數字)。 JSON_QUERY 回傳一個物件或陣列。 使用你所需的資料型態的正確函式。

JSON 計算欄位與索引

對於經常被查詢的 JSON 屬性,請建立帶有索引的計算欄位以提升效能:

-- Demo table with JSON-only rows.
IF OBJECT_ID('dbo.ProductJsonDemo', 'U') IS NOT NULL
    DROP TABLE dbo.ProductJsonDemo;

CREATE TABLE dbo.ProductJsonDemo (
    ProductDescriptionID INT IDENTITY(1,1) PRIMARY KEY,
    Metadata NVARCHAR(MAX) NOT NULL,
    CONSTRAINT CK_ProductJsonDemo_Metadata_IsJson CHECK (ISJSON(Metadata) = 1),
    -- Cast to a bounded length so the index key stays under SQL Server limits.
    LocaleName AS CAST(JSON_VALUE(Metadata, '$.locale') AS NVARCHAR(10)) PERSISTED
);

INSERT INTO dbo.ProductJsonDemo (Metadata)
VALUES
    (N'{"locale":"en","title":"Road helmet"}'),
    (N'{"locale":"fr","title":"Casque de route"}');

CREATE INDEX IX_ProductJsonDemo_LocaleName ON dbo.ProductJsonDemo(LocaleName);

接著直接從 Go 查詢計算出的欄位:

rows, err := db.QueryContext(ctx,
    "SELECT ProductDescriptionID, LocaleName FROM dbo.ProductJsonDemo WHERE LocaleName = @name",
    sql.Named("name", "en"))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var id int
    var localeName string
    if err := rows.Scan(&id, &localeName); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%d %s\n", id, localeName)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

XML 資料

使用 FOR XML 將查詢結果轉換為 XML

可以 FOR XML PATH XML 格式回傳查詢結果:

var xmlResult string
err := db.QueryRowContext(ctx, `
    SELECT BusinessEntityID AS [@id], FirstName + ' ' + LastName AS Name, CountryRegionName AS Department
    FROM Sales.vSalesPerson
    WHERE CountryRegionName = @dept
    FOR XML PATH('Employee'), ROOT('Employees')`,
    sql.Named("dept", "United States")).Scan(&xmlResult)
if err != nil {
    log.Fatal(err)
}
fmt.Println(xmlResult)

結果如下:

<Employees>
    <Employee id="274"><Name>Stephen Jiang</Name><Department>United States</Department></Employee>
    <Employee id="275"><Name>Michael Blythe</Name><Department>United States</Department></Employee>
    <Employee id="276"><Name>Linda Mitchell</Name><Department>United States</Department></Employee>
    <Employee id="277"><Name>Jillian Carson</Name><Department>United States</Department></Employee>
    <Employee id="279"><Name>Tsvi Reiter</Name><Department>United States</Department></Employee>
    <Employee id="280"><Name>Pamela Ansman-Wolfe</Name><Department>United States</Department></Employee>
    <Employee id="281"><Name>Shu Ito</Name><Department>United States</Department></Employee>
    <Employee id="283"><Name>David Campbell</Name><Department>United States</Department></Employee>
    <Employee id="284"><Name>Tete Mensa-Annan</Name><Department>United States</Department></Employee>
    <Employee id="285"><Name>Syed Abbas</Name><Department>United States</Department></Employee>
    <Employee id="287"><Name>Amy Alberts</Name><Department>United States</Department></Employee>
</Employees>

處理大型 XML 結果

像 JSON 一樣,大型 XML 結果會分散在多列之間。

func queryXML(ctx context.Context, db *sql.DB, query string, args ...any) (string, error) {
    rows, err := db.QueryContext(ctx, query, args...)
    if err != nil {
        return "", err
    }
    defer rows.Close()

    var sb strings.Builder
    for rows.Next() {
        var chunk string
        if err := rows.Scan(&chunk); err != nil {
            return "", err
        }
        sb.WriteString(chunk)
    }
    if err := rows.Err(); err != nil {
        return "", err
    }
    return sb.String(), nil
}

在 Go 中解析 XML 結果

使用 encoding/xml 套件來將 XML 結果還原。

import "encoding/xml"

type EmployeeList struct {
    XMLName   xml.Name   `xml:"Employees"`
    Employees []Employee `xml:"Employee"`
}

type Employee struct {
    ID         int    `xml:"id,attr"`
    Name       string `xml:"Name"`
    Department string `xml:"Department"`
}

func getEmployeesXML(ctx context.Context, db *sql.DB, dept string) (*EmployeeList, error) {
    xmlStr, err := queryXML(ctx, db, `
        SELECT BusinessEntityID AS [@id], FirstName + ' ' + LastName AS Name, CountryRegionName AS Department
        FROM Sales.vSalesPerson
        WHERE CountryRegionName = @dept
        FOR XML PATH('Employee'), ROOT('Employees')`,
        sql.Named("dept", dept))
    if err != nil {
        return nil, err
    }

    var result EmployeeList
    if err := xml.Unmarshal([]byte(xmlStr), &result); err != nil {
        return nil, err
    }
    return &result, nil
}

將 XML 參數傳給 SQL Server

將 XML 文件傳送到儲存程序或查詢:

xmlData := `<Employees>
    <Employee><Name>Alice</Name><Department>Engineering</Department></Employee>
    <Employee><Name>Bob</Name><Department>Marketing</Department></Employee>
</Employees>`

rows, err := db.QueryContext(ctx, `
    DECLARE @xmlDoc XML = CAST(@xml AS XML);
    SELECT
        e.value('(Name)[1]', 'NVARCHAR(100)') AS Name,
        e.value('(Department)[1]', 'NVARCHAR(100)') AS Department
    FROM @xmlDoc.nodes('/Employees/Employee') AS t(e)`,
    sql.Named("xml", xmlData))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var name, dept string
    if err := rows.Scan(&name, &dept); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s - %s\n", name, dept)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

Note

@xml參數預設為 nvarchar(max) 傳送。 此 .nodes() 方法需要 xml 資料類型,因此請使用 CAST(@xml AS XML) 明確轉換該參數。

從 XML 插入資料列

.nodes() 這個方法搭配 INSERT...SELECT 一個陳述式,將 XML 切碎成資料表列。

func bulkInsertFromXML(ctx context.Context, db *sql.DB, xmlData string) (int64, error) {
    result, err := db.ExecContext(ctx, `
        DECLARE @xmlDoc XML = CAST(@xml AS XML);
        INSERT INTO HumanResources.Department (Name, GroupName)
        SELECT
            e.value('(Name)[1]', 'NVARCHAR(50)'),
            e.value('(GroupName)[1]', 'NVARCHAR(50)')
        FROM @xmlDoc.nodes('/Departments/Department') AS t(e)`,
        sql.Named("xml", xmlData))
    if err != nil {
        return 0, err
    }
    return result.RowsAffected()
}

在 JSON 和 XML 之間選擇

考慮事項 JSON XML
Go 生態系統支援 標準 encoding/json。 結構標籤用於映射。 標準 encoding/xml。 更多冗長的結構標籤。
SQL Server 支援 OPENJSONJSON_VALUEJSON_QUERYFOR JSON(SQL Server 2016 和更新版本) nodes()value()query()FOR XML(所有版本)
Performance 通常解析速度更快。 更精簡的線路格式。 支援結構與驗證。 更冗長。
模式驗證 SQL Server 沒有內建的結構驗證功能。 支援用於伺服器端驗證的 XML 結構集合。
索引 計算出具有 JSON_VALUE + 索引的欄位。 XML 索引(主要與次要)。
資料模型 陣列與巢狀物件。 非常適合用於 Go 的切片和映射。 具有屬性與命名空間的階層文件。

小提示

對於新開發,JSON 通常是更好的選擇。 它所需的解析開銷較少,產生的有效載荷較小,且自然映射到 Go 結構。 當你需要結構驗證或與需要 XML 的系統整合時,請使用 XML。