JSON и XML-данные с помощью go-mssqldb

SQL Server поддерживает встроенную поддержку JSON и XML-данных. В этой статье показано, как использовать драйвер go-mssqldb для запроса, вставки и трансформации данных JSON и XML между структурами Go и SQL Server.

Примеры в этой статье выполняются в образце базы данных AdventureWorks2025. Примеры, ориентированные на чтение, запрашивают встроенные объекты, такие Sales.vSalesPerson как и Sales.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

Результаты запроса в формате JSON с FOR 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)

Замечание

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 JSON вместе с encoding/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}

Парс JSON в SQL Server с OPENJSON

Используйте 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)

Tip

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-данные

Результаты запроса в формате XML с FOR 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
}

Разбор результатов XML в Go

Используйте пакет 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)
}

Замечание

Параметр @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 OPENJSON, JSON_VALUE, JSON_QUERY, FOR JSON (SQL Server 2016 и более поздние версии) nodes(), value(), query(), FOR XML (все версии)
Performance В целом парсинг выполняется быстрее. Менее многословный формат проводов. Поддерживает схемы и валидацию. Более многословно.
Проверка схемы Нет встроенной проверки схемы в SQL Server. Поддерживает коллекции схем XML для серверной валидации.
Индексирование Вычисленные столбцы с индексом JSON_VALUE + XML-индексы (первичные и вторичные).
Моделирование данных Массивы и вложенные объекты. Хорошо подходит для срезов и отображений в Go. Иерархические документы с атрибутами и пространствами имён.

Tip

Для новых разработок JSON обычно лучший выбор. Это требует меньших накладных расходов на парсинг, обеспечивает меньший объём передаваемых данных и естественным образом сопоставляется со структурами Go. Используйте XML, когда нужна проверка схемы или при интеграции с системами, требующими XML.