Как в Go выполнить несколько запросов к базе данных в одной пачке (batch) и обработать результаты?

Ответ

Пакетная обработка запросов (batching) — это эффективный способ отправить несколько команд к базе данных за один сетевой вызов, что снижает задержки и повышает производительность.

В Go это можно реализовать с помощью драйверов, поддерживающих такую функциональность. Наиболее популярным и современным решением является pgx.

Пример с использованием pgx

pgx предоставляет удобный интерфейс pgx.Batch для работы с пакетами запросов.

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

func main() {
    ctx := context.Background()
    // Убедитесь, что у вас есть доступ к БД
    dbpool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to create connection pool: %vn", err)
        os.Exit(1)
    }
    defer dbpool.Close()

    batch := &pgx.Batch{}
    // Добавляем запросы в очередь
    batch.Queue("SELECT id, name FROM users WHERE id = $1", 1)
    batch.Queue("SELECT id, price FROM products WHERE id = $1", 42)

    // Отправляем пакет
    results := dbpool.SendBatch(ctx, batch)
    // Важно закрыть results, чтобы освободить соединение
    defer results.Close()

    // Читаем результаты в том же порядке, в котором отправляли запросы
    var userID int
    var userName string
    if err := results.QueryRow().Scan(&userID, &userName); err != nil {
        fmt.Fprintf(os.Stderr, "Failed to read user: %vn", err)
    }
    fmt.Printf("User: ID=%d, Name=%sn", userID, userName)

    var productID int
    var productPrice float64
    if err := results.QueryRow().Scan(&productID, &productPrice); err != nil {
        fmt.Fprintf(os.Stderr, "Failed to read product: %vn", err)
    }
    fmt.Printf("Product: ID=%d, Price=%.2fn", productID, productPrice)
}

Ключевые моменты:

  1. Порядок важен: Результаты нужно читать строго в том же порядке, в котором запросы были добавлены в batch.
  2. Освобождение ресурсов: Всегда используйте defer results.Close(), чтобы гарантировать, что соединение с базой данных вернется в пул, даже если при обработке результатов произойдет паника.
  3. Обработка ошибок: Проверяйте ошибку после каждого вызова .Scan(). Ошибка в одном из запросов не прерывает выполнение всего пакета, но результат для этого запроса будет содержать ошибку.

Ответ 18+ 🔞

А, ну ты глянь, пакетная обработка запросов! Это ж когда ты, блядь, не как дурак по одному запросу в базу шлёшь, а пачкой, как умный человек, сразу кучу. Сетевые задержки, производительность — всё это, ёпта, взлетает просто в небеса, как хуй с горы.

В Go, если не использовать всякое старьё, то самый адекватный вариант — это pgx. Драйвер, который не просто поддерживает батчинг, а делает это с таким удобством, что прям подозрение ебать чувствую — слишком хорошо.

Смотри, как это выглядит в коде, главное — техническую точность не теряем:

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/jackc/pgx/v5"
    "github.com/jackc/pgx/v5/pgxpool"
)

func main() {
    ctx := context.Background()
    // Убедитесь, что у вас есть доступ к БД
    dbpool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to create connection pool: %vn", err)
        os.Exit(1)
    }
    defer dbpool.Close()

    batch := &pgx.Batch{}
    // Добавляем запросы в очередь
    batch.Queue("SELECT id, name FROM users WHERE id = $1", 1)
    batch.Queue("SELECT id, price FROM products WHERE id = $1", 42)

    // Отправляем пакет
    results := dbpool.SendBatch(ctx, batch)
    // Важно закрыть results, чтобы освободить соединение
    defer results.Close()

    // Читаем результаты в том же порядке, в котором отправляли запросы
    var userID int
    var userName string
    if err := results.QueryRow().Scan(&userID, &userName); err != nil {
        fmt.Fprintf(os.Stderr, "Failed to read user: %vn", err)
    }
    fmt.Printf("User: ID=%d, Name=%sn", userID, userName)

    var productID int
    var productPrice float64
    if err := results.QueryRow().Scan(&productID, &productPrice); err != nil {
        fmt.Fprintf(os.Stderr, "Failed to read product: %vn", err)
    }
    fmt.Printf("Product: ID=%d, Price=%.2fn", productID, productPrice)
}

А теперь, блядь, самое важное, что ты должен запомнить, чтобы не обосраться на ровном месте:

  1. Порядок — это пиздец как важен. Ты читаешь результаты ровно в той же очерёдности, в которой закидывал запросы в батч. Перепутал — получишь в переменную userName цену на хуйвуху, и потом удивление пиздец, почему всё падает.
  2. Закрывай за собой, мудак. defer results.Close() — это святое. Если не закроешь, соединение в пул не вернётся, и рано или поздно твоё приложение накроется медным тазом с ошибкой "too many connections". А ты будешь чесать репу и думать, э бошка думай, кто ж тебя так подставил.
  3. Ошибки — они везде. Проверяй ошибку после каждого .Scan(). Если один запрос в пачке ебнулся, это не значит, что весь батч провалился. Просто на этом конкретном результате будет висеть ошибка, а остальные — нормально. Не проверил — получил кривые данные и волнение ебать на ровном месте.

Вот и вся магия, блядь. Вместо двадцати отдельных походов в базу — один, но с овердохуищей запросов внутри. Красота, ёпта.