database/sql

Лекция 9

Максим Иванов

План лекции

Основы database/sql
- Подключение к базам данных
- Основные сущности
- Выполнение запросов
- Именованные аргументы и работа с NULL

Продвинутые возможности
- Работа с транзакциями
- Prepared Statements
- Connection pool: настройка и статистика
- Работа с конкретным соединением (Conn)

Подводные камни
- Утечки ресурсов
- Deadlock'и
- Context и таймауты

Расширения и инструменты
- sqlx, hasql
- Тестирование с sqlmock
- ORM и альтернативные драйверы

2

Способы работы с базами данных

3

database/sql

type Driver interface {
    Open(name string) (driver.Conn, error)
}
type QueryerContext interface {
    QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error)
}
4

Подключение - database/sql

package open

import (
    "database/sql"
    "log"

    _ "github.com/jackc/pgx/v5/stdlib"
)

func SQLOpen() {
    db, err := sql.Open("pgx", "postgres://pgx_md5:secret@localhost:5432/pgx_test")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}
5

Подключение - на прямую

package open

import (
    "context"
    "log"

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

func PGXOpen() {
    ctx := context.Background()
    conn, err := pgx.Connect(ctx, "postgres://pgx_md5:secret@localhost:5432/pgx_test")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close(ctx)
}
6

Основные сущности

7

Проверка доступности

func IsItAliveQuestionMark(ctx context.Context) {
    db, err := sql.Open("pgx", "postgres://pgx_md5:secret@localhost:5432/pgx_test")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    if err := db.PingContext(ctx); err != nil {
        log.Fatal(err)
    }
}
8

Запрос с получением результатов

func Query(ctx context.Context, db *sql.DB) {
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        if err := rows.Scan(&id, &name); err != nil {
            log.Fatal(err)
        }

        log.Println(id, name)
    }

    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
9

Запрос с получением одного результата

func QueryRow(ctx context.Context, db *sql.DB) {
    var id int
    var name string
    err := db.QueryRowContext(ctx, "SELECT id, name FROM users WHERE id = $1", 1).Scan(&id, &name)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            log.Println("nothing found")
            return
        }

        log.Fatal(err)
    }

    log.Println(name)
}
10

Запрос без результатов

func Exec(ctx context.Context, db *sql.DB) {
    res, err := db.ExecContext(
        ctx,
        "UPDATE users SET name = $1 WHERE id = $2",
        "William Mandella", 1,
    )
    if err != nil {
        log.Fatal(err)
    }

    lastID, _ := res.LastInsertId()
    rowsAffected, _ := res.RowsAffected()

    log.Println(lastID, rowsAffected)
}

Возвращаемый тип

type Result interface {
    LastInsertId() (int64, error)
    RowsAffected() (int64, error)
}
11

Именованые аргументы

type NamedArg struct {
    Name  string
    Value interface{}
}
12

Именованые аргументы - использование

func Insert(ctx context.Context, db *sql.DB) {
    _, err := db.ExecContext(
        ctx,
        "INSERT INTO users(name) VALUES(@name)",
        sql.Named("name", "Amos Burton"),
    )
    if err != nil {
        log.Fatal(err)
    }
}
13

Nulls

type NullString struct {
    String string
    Valid  bool
}
14

Null в аргументах

func Insert(ctx context.Context, db *sql.DB, name interface{}) {
    _, err := db.ExecContext(
        ctx,
        "INSERT INTO users(name) VALUES(@name)",
        sql.Named("name", name),
    )
    if err != nil {
        log.Fatal(err)
    }
}

Передаём значения

func DoStuff(ctx context.Context, db *sql.DB) {
    // Nulls
    Insert(ctx, db, nil)
    Insert(ctx, db, sql.NullString{})

    // Values
    Insert(ctx, db, "The Shrike")
    Insert(ctx, db, sql.NullString{String: "The Shrike", Valid: true})
}
15

Null в результатах

func Results(ctx context.Context, db *sql.DB) {
    rows, err := db.QueryContext(ctx, "SELECT name FROM users WHERE id = $1", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var s sql.NullString
        if err := rows.Scan(&s); err != nil {
            log.Fatal(err)
        }

        if s.Valid {
            //
        } else {
            //
        }
    }
}
16

Работа с конкретным подключением к БД

func Conn(ctx context.Context, db *sql.DB) {
    c, err := db.Conn(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    _ = c.PingContext(ctx)
}
17

Работа с транзакциями

func Begin(ctx context.Context, db *sql.DB) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer tx.Rollback()

    _, err = tx.ExecContext(ctx, `UPDATE users SET name = "Tyador Borlú" WHERE id = 1`)
    if err != nil {
        log.Println(err)
        return
    }

    if err = tx.Commit(); err != nil {
        log.Println(err)
        return
    }
}
18

Что такое Prepared Statement?

Prepared Statement - предварительно скомпилированный SQL запрос:

Как работает:
- Парсинг и планирование (один раз при PrepareContext)
- Выполнение с параметрами (много раз через QueryContext/ExecContext)
- Закрытие (освобождение ресурсов на сервере БД)

19

Prepare

func Prepare(ctx context.Context, db *sql.DB) {
    stmt, err := db.PrepareContext(ctx, "SELECT name FROM users WHERE id = $1")
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()

    for i := 1; ; i++ {
        var name string
        if err = stmt.QueryRowContext(ctx, i).Scan(&name); err != nil {
            log.Fatal(err)
        }

        log.Println(name)
    }
}
20

Prepared Statements

Преимущества:

Недостатки:

21

Когда использовать Prepared Statements?

Используйте:
- Один запрос выполняется много раз с разными параметрами
- Сложные запросы (экономия на парсинге)

Не используйте:
- Каждый запрос уникален
- Работаете через внешние пулеры соединений
- План запроса сильно зависит от параметров

Проблема производительности:
План создаётся без знания конкретных значений → может быть неоптимальным
Пример: WHERE status = $1 (если 99% записей active, 1% deleted)

22

Настройка *sql.DB

23

Статистика *sql.DB

type DBStats struct {
    MaxOpenConnections int // Maximum number of open connections to the database; added in Go 1.11

    // Pool Status
    OpenConnections int // The number of established connections both in use and idle.
    InUse           int // The number of connections currently in use; added in Go 1.11
    Idle            int // The number of idle connections; added in Go 1.11

    // Counters
    WaitCount         int64         // The total number of connections waited for; added in Go 1.11
    WaitDuration      time.Duration // The total time blocked waiting for a new connection; added in Go 1.11
    MaxIdleClosed     int64         // The total number of connections closed due to SetMaxIdleConns; added in Go 1.11
    MaxIdleTimeClosed int64         // The total number of connections closed due to SetConnMaxIdleTime; added in Go 1.15
    MaxLifetimeClosed int64         // The total number of connections closed due to SetConnMaxLifetime; added in Go 1.11
}
24

Подводные камни при работе с database/sql

25

Основные типы граблей

26

Context - вечные запросы

func NoContext(ctx context.Context, db *sql.DB) {
    // У Conn() нет версии без контекста
    c, err := db.Conn(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer c.Close()

    // Потенциально вечный Ping
    _ = db.Ping()
}
27

Запросы - неосвобождение ресурсов

func RowsExhaust(ctx context.Context, db *sql.DB) {
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
    if err != nil {
        log.Fatal(err)
    }

    if rows.Next() {
        var id int
        var name string
        if err := rows.Scan(&id, &name); err != nil {
            log.Fatal(err)
        }

        log.Println(id, name)
    }
}
28

Коннекты - неосвобождение ресурсов

func ConnExhaust(ctx context.Context, db *sql.DB) {
    c, err := db.Conn(ctx)
    if err != nil {
        log.Fatal(err)
    }

    _ = c.PingContext(ctx)
}
29

Транзакции - неосвобождение ресурсов

func TxExhaust(ctx context.Context, db *sql.DB) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        log.Println(err)
        return
    }

    _, err = tx.ExecContext(ctx, `UPDATE users SET name = "Surl/Tesh-echer" WHERE id = 1`)
    if err != nil {
        log.Println(err)
        return
    }

    if err = tx.Commit(); err != nil {
        log.Println(err)
    }
}
30

Транзакции - исчерпание ресурсов приводящее к deadlock

func TxDeadlock(ctx context.Context, db *sql.DB) {
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer tx.Rollback()

    _, _ = db.QueryContext(ctx, "SELECT id, name FROM users")
    _, _ = db.QueryContext(ctx, "SELECT id, name FROM users")
}
31

Запросы - исчерпание ресурсов приводящее к deadlock

func QueryDeadlock(ctx context.Context, db *sql.DB) {
    rows, _ := db.QueryContext(ctx, "SELECT id, name FROM users")
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        _ = rows.Scan(&id, &name)

        rowsAddrs, _ := db.QueryContext(
            ctx,
            "SELECT address FROM addresses WHERE user_id = $1",
            id,
        )
        defer rowsAddrs.Close()

        var addr string
        _ = rowsAddrs.Scan(&addr)

        log.Println(id, name, addr)
    }
}
32

Запросы - deadlock - fix 1

func QueryDeadlockFixOne(ctx context.Context, db *sql.DB) {
    type Res struct {
        ID   int
        Name string
        Addr string
    }
    var values []Res
    rows, _ := db.QueryContext(ctx, "SELECT id, name FROM users")

    for rows.Next() {
        var res Res
        _ = rows.Scan(&res.ID, &res.Name)
        values = append(values, res)
    }
    rows.Close()

    for _, v := range values {
        _ = db.QueryRowContext(
            ctx, "SELECT address FROM addresses WHERE user_id = $1", v.ID,
        ).Scan(&v.Addr)
        log.Println(v)
    }
}
33

Запросы - deadlock - fix 2

func QueryDeadlockFixTwo(ctx context.Context, db *sql.DB) {
    rows, _ := db.QueryContext(
        ctx,
        "SELECT u.id, u.name, a.address FROM users AS u, addresses as a WHERE u.id == a.user_id",
    )
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        var addr string
        _ = rows.Scan(&id, &name, &addr)
        log.Println(id, name, addr)
    }
}
34

Удобства и расширения

35

github.com/jmoiron/sqlx

36

sqlx - подключение

package sqlx

import (
    "log"

    _ "github.com/jackc/pgx/v5/stdlib"
    "github.com/jmoiron/sqlx"
)

func Open() {
    db, err := sqlx.Open("pgx", "postgres://pgx_md5:secret@localhost:5432/pgx_test")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
}
37

sqlx - StructScan

func (r *Rows) StructScan(dest interface{}) error

func (r *Row) StructScan(dest interface{}) error

func StructScan(rows rowsi, dest interface{}) error

type rowsi interface {
    Close() error
    Columns() ([]string, error)
    Err() error
    Next() bool
    Scan(...interface{}) error
}
38

sqlx - StructScan на практике

func Example(ctx context.Context, db *sqlx.DB) {
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE id = $1", 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var value struct {
            ID   int    `db:"id"`
            Name string `db:"name"`
        }
        if err := sqlx.StructScan(rows, &value); err != nil {
            log.Fatal(err)
        }

        log.Println(value)
    }

    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}
39

sqlx - именованые аргументы

func Insert(ctx context.Context, db *sqlx.DB) {
    _, err := db.NamedExecContext(
        ctx,
        "INSERT INTO users(name) VALUES(:name)",
        map[string]interface{}{
            "name": "Jukka Sarasti",
        },
    )
    if err != nil {
        log.Fatal(err)
    }
}
40

golang.yandex/hasql

41

hasql - подключение и использование

func Open() {
    dbFoo, _ := sql.Open("pgx", "host=foo")
    dbBar, _ := sql.Open("pgx", "host=bar")
    cluster, err := hasql.NewCluster(
        []hasql.Node{hasql.NewNode("foo", dbFoo), hasql.NewNode("bar", dbBar)},
        checkers.PostgreSQL,
    )
    if err != nil {
        log.Fatal(err)
    }

    node := cluster.Primary()
    if err == nil {
        log.Fatal(err)
    }

    log.Println("Node address", node.Addr())

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    if err = node.DB().PingContext(ctx); err != nil {
        log.Fatal(err)
    }
}
42

Нетипичные драйверы

43

Нетипичные драйверы - github.com/DATA-DOG/go-sqlmock

package main

import (
	"database/sql"
	"log"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/require"
)

func TestSelect(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("SELECT name FROM users WHERE id = ?").
        WithArgs(1).
        WillReturnError(sql.ErrNoRows)
    mock.ExpectCommit()

    tx, err := db.Begin()
    require.NoError(t, err)

    _, err = db.Exec("SELECT name FROM users WHERE id = ?", 1)
    require.NotNil(t, err)
    require.Equal(t, err, sql.ErrNoRows)

    require.NoError(t, tx.Commit())

    require.NoError(t, mock.ExpectationsWereMet())
}
44

Нетипичные драйверы - github.com/ClickHouse/clickhouse-go

func Example(ctx context.Context) {
    db, _ := sql.Open("clickhouse", "tcp://127.0.0.1:9000?debug=true")
    defer db.Close()

    // Начало батча
    tx, _ := db.BeginTx(ctx, nil)
    defer tx.Rollback()

    // Описание батча
    stmt, _ := tx.PrepareContext(ctx, "INSERT INTO example (id) VALUES (?)")
    defer stmt.Close()

    // Добавление данных
    for i := 0; i < 100; i++ {
        _, _ = stmt.ExecContext(ctx, i)
    }

    // Отправка батча в ClickHouse
    _ = tx.Commit()
}
45

Не-SQL драйверы - Redis

func Example(ctx context.Context) {
    rdb := redis.NewUniversalClient(&redis.UniversalOptions{
        MasterName: "master",
        Addrs:      []string{":26379"},
    })
    defer rdb.Close()

    if err := rdb.Ping(ctx); err != nil {
        log.Fatal(err)
    }

    if err := rdb.Set(ctx, "key", "value", time.Hour).Err(); err != nil {
        log.Fatal(err)
    }

    value, err := rdb.Get(ctx, "key").Result()
    if err != nil {
        log.Fatal(err)
    }

    log.Println(value)
}
46

ORM

47

Популярные базы данных и их драйверы

Совместимые с database/sql:

Другие:

48

Thank you

Максим Иванов

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)