Data JSON dan XML dengan go-mssqldb

SQL Server memiliki dukungan bawaan untuk data JSON dan XML. Artikel ini menunjukkan cara menggunakan go-mssqldb driver untuk mengkueri, menyisipkan, dan mengubah data JSON dan XML antara struktur Go dan SQL Server.

Contoh dalam artikel ini menggunakan basis data sampel AdventureWorks2025. Contoh berorientasi baca mengkueri objek bawaan seperti Sales.vSalesPerson dan Sales.SalesOrderHeader. Contoh berorientasi tulis menggunakan OPENJSON dan XML .nodes() untuk menyisipkan baris ke HumanResources.Department.

Sebagian besar cuplikan mengasumsikan ctx dan db sudah diinisialisasi. Anda dapat menggunakan pola pengaturan ini:

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

Data JSON

Hasil kueri dalam format JSON dengan FOR JSON

Gunakan FOR JSON PATH untuk mengembalikan hasil kueri sebagai string 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 dapat mengembalikan FOR JSON output di beberapa baris untuk muatan yang lebih besar. Jika Anda perlu mendukung hasil yang lebih besar dengan andal, baca semua baris yang ditampilkan dan gabungkan. Lihat Menangani hasil JSON besar.

Menangani hasil JSON besar

Saat SQL Server mengembalikan keluaran JSON dalam beberapa baris, gabungkan bagian-bagiannya sebelum mengurai atau mencetak hasilnya:

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
}

Penggunaan:

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)
}

Unmarshal JSON menghasilkan menjadi Go structs

Gabungkan FOR JSON dengan encoding/json untuk mendeserialisasi hasil SQL Server langsung ke dalam jenis 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 bertingkat dengan FOR JSON PATH

Buat struktur JSON berlapis dengan menggunakan notasi titik dalam alias kolom:

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)

Hasilnya:

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

Mengurai JSON di SQL Server dengan OPENJSON

Gunakan OPENJSON untuk menguraikan array JSON menjadi baris di sisi server:

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)
}

Sisipkan data JSON sebagai parameter

Kirim struktur Go sebagai string JSON ke prosedur tersimpan atau 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
}

Penggunaan:

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)
}

Penyisipan batch dari JSON dengan OPENJSON

Sisipkan beberapa baris dari array JSON dalam satu pernyataan:

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

Penggunaan:

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 dan JSON_QUERY

Ekstrak nilai dari kolom JSON tanpa mentransfer seluruh dokumen 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 mengembalikan nilai skalar (string, number). JSON_QUERY mengembalikan objek atau array. Gunakan fungsi yang benar untuk jenis data yang Anda butuhkan.

Kolom dan indeks komputasi JSON

Untuk properti JSON yang sering dikueri, buat kolom komputasi dengan indeks untuk performa yang lebih baik:

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

Kemudian kueri kolom yang dihitung langsung dari 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)
}

Data XML

Kueri hasil sebagai XML dengan FOR XML

Gunakan FOR XML PATH untuk mengembalikan hasil kueri sebagai 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)

Hasilnya:

<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>

Menangani hasil XML besar

Seperti JSON, hasil XML berukuran besar dipecah ke beberapa baris.

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
}

Mengurai hasil XML di Go

Gunakan paket encoding/xml untuk mengurai serialisasi hasil 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
}

Meneruskan parameter XML ke SQL Server

Mengirim dokumen XML ke prosedur atau kueri tersimpan:

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

Parameter dikirim @xml sebagai nvarchar(max) secara default. Metode ini .nodes() memerlukan tipe data xml , jadi transmisikan parameter secara eksplisit dengan CAST(@xml AS XML).

Menyisipkan baris dari XML

Gunakan metode .nodes() dengan pernyataan INSERT...SELECT untuk memecah XML menjadi baris tabel.

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

Pilih antara JSON dan XML

Consideration JSON XML
Dukungan ekosistem Go Standar encoding/json. Tag struct untuk pemetaan. Standar encoding/xml. Tag struct yang lebih rinci.
Dukungan SQL Server OPENJSON, JSON_VALUE, , JSON_QUERY( FOR JSON SQL Server 2016 dan versi yang lebih baru) nodes(), value(), , query()( FOR XML semua versi)
Performance Penguraian umumnya lebih cepat. Format kawat yang kurang bertele-tele. Mendukung skema dan validasi. Lebih bertele-tele.
Skema validasi Tidak ada validasi skema bawaan di SQL Server. Mendukung Koleksi Skema XML untuk validasi sisi server.
Pengindeksan Kolom yang dihitung dengan indeks JSON_VALUE+. Indeks XML (primer dan sekunder).
Pemodelan data Array dan objek bersarang. Sangat cocok untuk slice dan map di Go. Dokumen hierarkis dengan atribut dan namespace.

Tip

Untuk pengembangan baru, JSON biasanya merupakan pilihan yang lebih baik. Ini membutuhkan lebih sedikit penguraian overhead, menghasilkan muatan yang lebih kecil, dan memetakan secara alami ke struktur Go. Gunakan XML saat Anda memerlukan validasi skema atau saat mengintegrasikan dengan sistem yang memerlukan XML.