migrate для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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


                
    
    

Рекомендаций