sqlc для Go: генерация type-safe кода из SQL
sqlc — это компилятор, который генерирует Go-код на основе ваших SQL-запросов. Вместо того чтобы писать вручную функции для работы с базой данных, вы пишете чистый SQL, а sqlc создаёт за вас типизированные, безопасные и производительные функции. Это принципиально иной подход по сравнению с ORM: вы сохраняете полный контроль над SQL, но избавляетесь от рутинного написания boilerplate-кода.
Зачем это нужно? Во-первых, sqlc гарантирует type-safety на уровне компиляции. Если вы измените схему таблицы или запрос, sqlc перегенерирует код, и компилятор Go сразу укажет на все места, где нужно внести правки. Во-вторых, производительность: сгенерированный код не использует рефлексию и не делает лишних аллокаций, что делает его практически таким же быстрым, как ручное написание запросов через database/sql. В-третьих, это упрощает ревью кода: SQL-запросы явно видны в файлах .sql, а не спрятаны в методах ORM.
sqlc идеально подходит для проектов, где важна производительность и контроль над запросами: высоконагруженные API, финансовые системы, аналитические платформы. Он также отлично сочетается с миграциями (например, golang-migrate) и не привязывает вас к конкретной СУБД — поддерживаются PostgreSQL, MySQL, SQLite.
Установка
sqlc — это не библиотека, а утилита командной строки. Установить её можно несколькими способами:
# Через go install (рекомендуется)
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# Через Homebrew (macOS/Linux)
brew install sqlc
# Скачать бинарник с GitHub
# https://github.com/sqlc-dev/sqlc/releases
После установки проверьте версию:
sqlc version
Быстрый старт
Создадим минимальный проект, который демонстрирует работу sqlc.
1. Структура проекта
mkdir sqlc-demo && cd sqlc-demo
go mod init sqlc-demo
2. Конфигурация sqlc
Создайте файл sqlc.yaml в корне проекта:
version: "2"
sql:
- engine: "postgresql"
queries: "queries.sql"
schema: "schema.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"
3. Схема базы данных
Файл schema.sql:
CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
bio text
);
4. SQL-запросы
Файл queries.sql:
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :one
INSERT INTO authors (name, bio)
VALUES ($1, $2)
RETURNING *;
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;
5. Генерация кода
sqlc generate
После выполнения в папке db/ появятся сгенерированные файлы. Теперь можно использовать их в коде:
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
"sqlc-demo/db"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost:5432/testdb")
if err != nil {
log.Fatal(err)
}
defer pool.Close()
queries := db.New(pool)
// Создаём автора
author, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{
Name: "Brian Kernighan",
Bio: "Co-author of The C Programming Language",
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created author: %+v\n", author)
// Получаем автора по ID
fetched, err := queries.GetAuthor(ctx, author.ID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Fetched author: %+v\n", fetched)
// Список всех авторов
authors, err := queries.ListAuthors(ctx)
if err != nil {
log.Fatal(err)
}
for _, a := range authors {
fmt.Printf("Author: %s\n", a.Name)
}
}
Основные методы и функции
sqlc генерирует код на основе аннотаций в SQL-комментариях. Рассмотрим все возможные аннотации и сгенерированные методы.
1. :one — получение одной записи
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (Model, error)
Что делает: Выполняет запрос и возвращает ровно одну запись. Если запрос не вернул строк, возвращает ошибку sql.ErrNoRows.
-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;
author, err := queries.GetAuthor(ctx, 1)
// author.ID, author.Name, author.Bio
2. :many — получение множества записей
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) ([]Model, error)
Что делает: Выполняет запрос и возвращает срез записей. Если запрос не вернул строк, возвращается пустой срез (не nil).
-- name: ListAuthors :many
SELECT * FROM authors ORDER BY name;
authors, err := queries.ListAuthors(ctx)
for _, a := range authors {
fmt.Println(a.Name)
}
3. :exec — выполнение без возврата строк
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) error
Что делает: Выполняет запрос, не возвращая строки. Используется для INSERT, UPDATE, DELETE без RETURNING.
-- name: DeleteAuthor :exec
DELETE FROM authors WHERE id = $1;
err := queries.DeleteAuthor(ctx, 1)
4. :execrows — выполнение с возвратом количества затронутых строк
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (int64, error)
Что делает: Выполняет запрос и возвращает количество строк, которые были изменены/удалены.
-- name: UpdateAuthorBio :execrows
UPDATE authors SET bio = $2 WHERE id = $1;
rowsAffected, err := queries.UpdateAuthorBio(ctx, db.UpdateAuthorBioParams{
ID: 1,
Bio: "New bio",
})
fmt.Printf("Updated %d rows\n", rowsAffected)
5. :execresult — выполнение с возвратом результата
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg Params) (pgconn.CommandTag, error)
Что делает: Выполняет запрос и возвращает pgconn.CommandTag, который содержит метаданные о выполненной команде (например, количество затронутых строк, OID).
-- name: DeleteAllAuthors :execresult
DELETE FROM authors;
result, err := queries.DeleteAllAuthors(ctx)
fmt.Printf("Rows deleted: %d\n", result.RowsAffected())
6. :copyfrom — массовая вставка через COPY
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg []Params) (int64, error)
Что делает: Использует PostgreSQL COPY для массовой вставки данных. Значительно быстрее, чем множественные INSERT.
-- name: CreateAuthors :copyfrom
INSERT INTO authors (name, bio) VALUES ($1, $2);
authors := []db.CreateAuthorsParams{
{Name: "Alice", Bio: "Writer"},
{Name: "Bob", Bio: "Engineer"},
}
count, err := queries.CreateAuthors(ctx, authors)
fmt.Printf("Inserted %d authors\n", count)
7. :batchmany и :batchone — пакетные запросы
Сигнатура: func (q *Queries) MethodName(ctx context.Context, arg []Params) *pgx.BatchResults
Что делает: Выполняет несколько одинаковых запросов в одном пакете. :batchmany для запросов, возвращающих множество строк, :batchone — для одной строки.
-- name: GetAuthorsBatch :batchmany
SELECT * FROM authors WHERE id = $1;
ids := []int32{1, 2, 3}
batch := queries.GetAuthorsBatch(ctx, ids)
for batch.Next() {
author, err := batch.Values()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Author: %+v\n", author)
}
8. New() — создание экземпляра Queries
Сигнатура: func New(db DBTX) *Queries
Что делает: Создаёт новый экземпляр структуры Queries, которая содержит все сгенерированные методы. Принимает интерфейс DBTX, который реализуют *sql.DB, *sql.Tx, *pgxpool.Pool и другие.
// С pgxpool
pool, _ := pgxpool.New(ctx, connString)
queries := db.New(pool)
// С database/sql
sqlDB, _ := sql.Open("postgres", connString)
queries := db.New(sqlDB)
// С транзакцией
tx, _ := pool.Begin(ctx)
queries := db.New(tx)
9. WithTx() — создание Queries с транзакцией
Сигнатура: func (q *Queries) WithTx(tx *sql.Tx) *Queries (для database/sql) или func (q *Queries) WithTx(tx pgx.Tx) *Queries (для pgx)
Что делает: Создаёт новый экземпляр Queries, привязанный к переданной транзакции. Позволяет выполнять запросы в рамках одной транзакции.
tx, err := pool.Begin(ctx)
if err != nil {
log.Fatal(err)
}
defer tx.Rollback(ctx)
qtx := queries.WithTx(tx)
qtx.CreateAuthor(ctx, db.CreateAuthorParams{Name: "Test"})
qtx.DeleteAuthor(ctx, 1)
tx.Commit(ctx)
Реальный практический пример
Создадим полноценный REST API для управления блогом с авторами и постами.
Схема базы данных
-- schema.sql
CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
author_id BIGINT NOT NULL REFERENCES authors(id),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
SQL-запросы
-- queries.sql
-- Authors
-- name: CreateAuthor :one
INSERT INTO authors (name, email, bio)
VALUES ($1, $2, $3)
RETURNING *;
-- name: GetAuthor :one
SELECT * FROM authors WHERE id = $1;
-- name: ListAuthors :many
SELECT * FROM authors ORDER BY created_at DESC;
-- name: UpdateAuthor :one
UPDATE authors
SET name = COALESCE($2, name),
email = COALESCE($3, email),
bio = COALESCE($4, bio)
WHERE id = $1
RETURNING *;
-- name: DeleteAuthor :exec
DELETE FROM authors WHERE id = $1;
-- Posts
-- name: CreatePost :one
INSERT INTO posts (author_id, title, content, published)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetPost :one
SELECT * FROM posts WHERE id = $1;
-- name: ListPostsByAuthor :many
SELECT * FROM posts
WHERE author_id = $1
ORDER BY created_at DESC;
-- name: ListPublishedPosts :many
SELECT p.*, a.name as author_name
FROM posts p
JOIN authors a ON a.id = p.author_id
WHERE p.published = TRUE
ORDER BY p.created_at DESC;
-- name: UpdatePost :one
UPDATE posts
SET title = COALESCE($2, title),
content = COALESCE($3, content),
published = COALESCE($4, published),
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: DeletePost :exec
DELETE FROM posts WHERE id = $1;
Конфигурация sqlc
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "queries.sql"
schema: "schema.sql"
gen:
go:
package: "db"
out: "db"
sql_package: "pgx/v5"