migrate для Go: полное руководство с примерами
Библиотека migrate — это мощный и гибкий инструмент для управления миграциями баз данных в экосистеме Go. Она позволяет разработчикам версионировать изменения схемы базы данных, автоматически применять и откатывать миграции, а также интегрироваться с различными СУБД, включая PostgreSQL, MySQL, SQLite, MongoDB, CockroachDB и другие. В этой статье мы подробно разберем, как использовать migrate в Go, начиная с установки и заканчивая продвинутыми техниками.
Что это и зачем нужна
В процессе разработки любого приложения, работающего с базой данных, рано или поздно возникает необходимость изменить структуру таблиц: добавить новую колонку, создать индекс, изменить тип данных или переименовать таблицу. Если вы работаете в команде или разворачиваете приложение на нескольких окружениях (разработка, стейджинг, продакшн), ручное выполнение SQL-скриптов становится источником ошибок и несоответствий.
Библиотека migrate решает эту проблему, предоставляя простой и декларативный способ описания изменений схемы. Каждая миграция — это пара файлов: один для применения изменений (up), другой для отката (down). Библиотека отслеживает, какие миграции уже были применены, и позволяет безопасно двигаться вперед или назад по истории изменений.
Основные преимущества использования migrate:
- Версионирование: каждая миграция имеет уникальный номер или метку времени, что позволяет точно знать, какая версия схемы используется в данный момент.
- Воспроизводимость: вы можете развернуть базу данных с нуля, применив все миграции последовательно.
- Безопасность: наличие down-миграций позволяет откатить изменения в случае ошибки.
- Независимость от языка: миграции пишутся на чистом SQL, что делает их переносимыми между проектами на разных языках.
Установка
Для установки библиотеки migrate в Go используйте стандартную команду go get. Рекомендуется устанавливать её как зависимость вашего проекта:
go get -u github.com/golang-migrate/migrate/v4
Также вам потребуется установить драйвер для вашей базы данных. Например, для PostgreSQL:
go get -u github.com/golang-migrate/migrate/v4/database/postgres
Для MySQL:
go get -u github.com/golang-migrate/migrate/v4/database/mysql
Для SQLite:
go get -u github.com/golang-migrate/migrate/v4/database/sqlite3
Кроме того, вам понадобится драйвер для источника миграций. Чаще всего используется файловая система:
go get -u github.com/golang-migrate/migrate/v4/source/file
Также доступны драйверы для io/fs, github, gitlab, s3 и других источников.
Быстрый старт — минимальный рабочий пример
Давайте создадим простое приложение, которое подключается к SQLite базе данных и применяет миграции.
Сначала создадим структуру папок для миграций:
mkdir -p migrations
cd migrations
Создадим первую миграцию. Имя файла должно соответствовать шаблону: {версия}_{название}.{направление}.sql. Версия обычно представляет собой timestamp или последовательный номер.
Файл: migrations/000001_create_users_table.up.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Файл: migrations/000001_create_users_table.down.sql
DROP TABLE IF EXISTS users;
Теперь напишем Go-код:
package main
import (
"database/sql"
"fmt"
"log"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/mattn/go-sqlite3"
)
func main() {
// Открываем соединение с БД
db, err := sql.Open("sqlite3", "./app.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Создаем драйвер базы данных для migrate
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
log.Fatal(err)
}
// Создаем экземпляр мигратора
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"sqlite3",
driver,
)
if err != nil {
log.Fatal(err)
}
// Применяем все миграции
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal(err)
}
fmt.Println("Миграции успешно применены!")
}
Запустите код:
go run main.go
После выполнения в директории появится файл app.db с таблицей users.
Основные методы/функции
Рассмотрим ключевые методы библиотеки migrate, которые понадобятся вам в повседневной работе.
1. New()
Сигнатура:
func New(sourceURL string, databaseURL string) (*Migrate, error)
Что делает: Создает новый экземпляр мигратора на основе URL источника миграций и URL базы данных. Это самый простой способ инициализации.
Пример:
m, err := migrate.New(
"file://migrations",
"postgres://postgres:password@localhost:5432/mydb?sslmode=disable",
)
if err != nil {
log.Fatal(err)
}
defer m.Close()
2. NewWithDatabaseInstance()
Сигнатура:
func NewWithDatabaseInstance(sourceURL string, databaseName string, databaseInstance database.Driver) (*Migrate, error)
Что делает: Создает мигратор с использованием уже существующего экземпляра драйвера базы данных. Полезно, когда вы уже открыли соединение с БД и хотите использовать его.
Пример:
db, _ := sql.Open("postgres", "postgres://...")
driver, _ := postgres.WithInstance(db, &postgres.Config{})
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
"postgres",
driver,
)
3. Up()
Сигнатура:
func (m *Migrate) Up() error
Что делает: Применяет все ожидающие (непримененные) миграции. Если миграций нет, возвращает ErrNoChange.
Пример:
if err := m.Up(); err != nil {
if err == migrate.ErrNoChange {
fmt.Println("Нет новых миграций")
} else {
log.Fatal(err)
}
}
4. Down()
Сигнатура:
func (m *Migrate) Down() error
Что делает: Откатывает все примененные миграции (обычно до нулевого состояния). Будьте осторожны в продакшене!
Пример:
if err := m.Down(); err != nil {
log.Fatal(err)
}
fmt.Println("Все миграции откачены")
5. Steps()
Сигнатура:
func (m *Migrate) Steps(n int) error
Что делает: Применяет (если n > 0) или откатывает (если n < 0) ровно n миграций. Позволяет двигаться по истории миграций пошагово.
Пример:
// Применить следующие 2 миграции
if err := m.Steps(2); err != nil {
log.Fatal(err)
}
// Откатить 1 миграцию
if err := m.Steps(-1); err != nil {
log.Fatal(err)
}
6. Migrate()
Сигнатура:
func (m *Migrate) Migrate(version uint) error
Что делает: Мигрирует базу данных до указанной версии. Если текущая версия ниже целевой — применяет миграции, если выше — откатывает.
Пример:
// Перейти к версии 3
if err := m.Migrate(3); err != nil {
log.Fatal(err)
}
fmt.Println("База данных на версии 3")
7. Force()
Сигнатура:
func (m *Migrate) Force(version uint) error
Что делает: Принудительно устанавливает версию базы данных без применения или отката миграций. Используется для исправления состояния, когда таблица миграций повреждена.
Пример:
// Принудительно установить версию 5 (например, если миграция 5 уже была применена вручную)
if err := m.Force(5); err != nil {
log.Fatal(err)
}
8. Version()
Сигнатура:
func (m *Migrate) Version() (version uint, dirty bool, err error)
Что делает: Возвращает текущую версию миграций и флаг "грязной" миграции (если предыдущая миграция не завершилась успешно).
Пример:
version, dirty, err := m.Version()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Текущая версия: %d, грязная: %t\n", version, dirty)
9. Drop()
Сигнатура:
func (m *Migrate) Drop() error
Что делает: Удаляет все таблицы в базе данных. Крайне опасная операция, используйте только в тестовых средах.
Пример:
if err := m.Drop(); err != nil {
log.Fatal(err)
}
fmt.Println("Все таблицы удалены")
10. Close()
Сигнатура:
func (m *Migrate) Close() (source error, database error)
Что делает: Закрывает соединения с источником миграций и базой данных. Всегда вызывайте этот метод после завершения работы с мигратором.
Пример:
defer func() {
sourceErr, dbErr := m.Close()
if sourceErr != nil {
log.Printf("Ошибка закрытия источника: %v", sourceErr)
}
if dbErr != nil {
log.Printf("Ошибка закрытия БД: %v", dbErr)
}
}()
Реальный практический пример
Давайте создадим полноценное приложение на Go, которое использует migrate для управления схемой PostgreSQL. Мы реализуем REST API для управления пользователями и заказами, а миграции будут создавать и изменять таблицы.
Структура проекта:
project/
├── main.go
├── migrations/
│ ├── 000001_create_users.up.sql
│ ├── 000001_create_users.down.sql
│ ├── 000002_create_orders.up.sql
│ ├── 000002_create_orders.down.sql
│ ├── 000003_add_status_to_orders.up.sql
│ └── 000003_add_status_to_orders.down.sql
├── go.mod
└── go.sum
Файл: migrations/000001_create_users.up.sql
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
Файл: migrations/000001_create_users.down.sql
DROP TABLE IF EXISTS users;
Файл: migrations/000002_create_orders.up.sql
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
Файл: migrations/000002_create_orders.down.sql
DROP INDEX IF EXISTS idx_orders_status;
DROP INDEX IF EXISTS idx_orders_user_id;
DROP TABLE IF EXISTS orders;
Файл: migrations/000003_add_status_to_orders.up.sql
Также в библиотеке
Рекомендаций