Содержание урока · 11 разделов
- Схема — код, база — отдельный мир
- append-only: что автомиграция делает и чего не делает
- Границы automigrate: почему прод требует большего
- atlas: diff между желаемым и применённым
- Конвейер: apply, status, lint
- Чеклист миграций перед продом
- Авто-миграция против версионных: карта выбора
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Маршрут урока — от первого CREATE TABLE до взрослого конвейера миграций. Сначала — проблема: схемы из второго урока — это код, а база о коде ничего не знает, и кто-то обязан перевести одно в другое. Затем — авто-миграция: механика Schema.Create, её append-only семантика и опции вроде WithDropColumn; здесь же — самая коварная операция, переименование поля, на которой ловятся все, кто путает «добавить колонку» с «переименовать». Третья часть — границы: почему automigrate отлично живёт в разработке, но не выдерживает прод-требований к истории, ревью и откату. Четвёртая — версионные миграции atlas: diff, каталог SQL-файлов, atlas.sum, apply, status и lint как ворота CI. Финиш — таблица выбора между двумя формами и чеклист деплоя. Если вы шли сюда по запросу «ent schema migration» — это тот урок: обе стратегии, честно, с ценой каждой.
Схема — код, база — отдельный мир#
Восемь уроков мы работали так, будто таблицы уже есть: создавали пользователей, строили рёбра, двигали счётчики. На самом деле у ent за это отвечает отдельный слой, и механика у него та же, что у всего фреймворка, — кодогенерация. Седьмой урок показал, где она живёт: файл ent/migrate/migrate.go содержит исполнение миграций, а ent/migrate/schema.go — описание вашего графа в терминах таблиц: users и groups, их колонки, индексы, внешние ключи и служебная join-таблица group_users с двумя каскадными FK. Это описание — единственный источник правды: и авто-миграция, и atlas читают одни и те же схемы, поэтому миграции не могут разойтись с билдерами, из которых вы вызываете SetX. ent миграции предлагает в двух формах. Первая — автоматическая: приложение само подводит базу к текущей схеме на старте. Вторая — версионная: инструмент atlas генерирует SQL-файлы, они лежат в репозитории, проходят ревью и применяются отдельным шагом. Формы не конкурируют, а делятся по зрелости проекта: пока вы в разработке — первая, когда появился прод и команда — вторая. Разница между ними не в силе, а в свойствах: у одной цена — слепота, у другой — церемония.
Авто-миграция: один вызов на старте
GO// main.go — поднять схему при запуске приложения:
client, err := ent.Open("postgres", dsn)
if err != nil {
log.Fatalf("драйвер не открылся: %v", err)
}
defer client.Close()
// сверить схемы с базой и создать недостающее:
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("миграция не прошла: %v", err)
}Schema.Create приходит из сгенерированного пакета ent/migrate и вызывается на клиенте. На пустой базе она исполняет CREATE TABLE IF NOT EXISTS для users, groups, posts и join-таблицы, ALTER TABLE ADD COLUMN для недостающих колонок, CREATE INDEX и ADD CONSTRAINT для внешних ключей. На уже размеченной базе — сверяет желаемое с фактическим и исполняет только недостающее: вызов идемпотентен, сколько раз ни запускай.
Проверка: второй запуск Schema.Create
Вы добавили в схему User поле bio и запустили приложение второй раз. Что делает Schema.Create?
append-only: что автомиграция делает и чего не делает#
Главная черта ent автомиграции — append-only семантика: она создаёт таблицы, добавляет колонки и индексы, расширяет внешние ключи — и ни при каких обстоятельствах не удаляет колонки и не сносит таблицы, даже если они исчезли из схем. Это защита данных: исчезновение поля из Go-файла не обязано означать «убить колонку с живыми данными». Из той же философии растёт самая коварная операция — переименование. Вы переименовали bio в about в схеме; automigrate не знает про «переименовал» — для неё это новая колонка about, которую надо добавить, и всё. Старая bio останется с данными, новая about добавится пустой, и никакой ошибки вы не увидите — просто тихая потеря, обнаруженная пользователями, а не компилятором. Если удаления нужны сознательно, их включают опциями: WithDropColumn(true) разрешает DROP COLUMN, WithDropIndex(true) — DROP INDEX. Ещё две опции стоит знать в лицо: WithGlobalUniqueID(true) переводит первичные ключи на общую таблицу счётчиков — сквозные уникальные id по всем таблицам, нужные полиморфным связям; WithForeignKeys(false) запрещает ent создавать внешние ключи — граф целостности вы обслуживаете сами, уровень приложения. Слово automigrate — общепринятое имя этой стратегии: миграция без человека, автоматически на каждом старте.
Опции Schema.Create
GOimport "ваш-модуль/ent/migrate"
// осторожный прогон с сознательными включениями:
if err := client.Schema.Create(
ctx,
migrate.WithDropIndex(true), // уметь удалять индексы
migrate.WithDropColumn(true), // уметь удалять колонки
migrate.WithGlobalUniqueID(true), // сквозные id по таблицам
); err != nil {
log.Fatalf("миграция не прошла: %v", err)
}Каждая опция — осознанный риск. Drop-режимы включают, когда точно знают, что колонка мертва: обычно на короткий период, чтобы убрать хвост после переименования. WithGlobalUniqueID меняет формат первичных ключей — включайте до первой продовой вставки, не после. В каноне курса мы этих флагов не касались: стандартный Schema.Create — append-only и безопасный по умолчанию.
Переименование: что окажется в базе
Прочитайте код и выберите, что он выведет:
// было: field.String("bio").Optional().Nillable()
// стало: field.String("about").Optional().Nillable()
err := client.Schema.Create(ctx) // без drop-опций
// что увидит SELECT * FROM users?Границы automigrate: почему прод требует большего#
Авто-миграция — стратегия доверия: код, исполняющийся на старте каждого инстанса, сам решает, какой станет база. У доверия три дыры. Первая — история: Schema.Create не оставляет следов того, что и когда исполняла; вопрос «когда у нас появилась колонка about» решается археологией, а не логом миграций. Вторая — откат: down-миграции не существует; если новая колонка с NOT NULL без дефолта разломала старую версию приложения, единственный путь назад — руками. Третья — ревью: SQL не видит ни один человек; каждая операция из этого урока — CREATE TABLE, ALTER TABLE, DROP — исполняется молча, и опасное изменение ничем не остановлено до момента, когда оно уже случилось. Четвёртая — деплой: при multi-instance-раскатке несколько подов стартуют одновременно, и каждый пытается мигрировать; на одних базах это окончится дублирующими ALTER, на других — взаимными блокировками. Отсюда правило: automigrate — для разработки, прототипов и пет-проектов; прод с командой, историями и SLA — территория версионных миграций, где каждая версия — файл, каждый файл — ревью, каждый apply — событие.
atlas: diff между желаемым и применённым#
Версионные миграции ent строятся на atlas — инструменте от той же команды, что развивает ent; связка atlas ent официальная, и это не случайность: atlas вырос из потребностей самого ent. Идея — та же, что у automigrate, только человек возвращается в цикл. atlas тоже сравнивает желаемое состояние (ваши Go-схемы) с фактическим, но не исполняет diff сразу, а записывает его SQL-файлом в каталог миграций — ent/migrate/migrations. Файлы нумеруются, складываются в историю и никогда не редактируются задним числом; рядом живёт atlas.sum — файл чексумм, гарантирующий, что уже применённые версии не подменят. Отдельный концепт — dev-база: чтобы вычислить diff, atlas поднимает временную базу (конфиг описывает её docker-строкой), применяет к ней весь каталог миграций, затем вашу новую схему — и снимает разницу. Поэтому diff не трогает прод: расчёт идёт на одноразовой базе. Правка схемы превращается в конвейер: go generate из седьмого урока, затем atlas migrate diff — и SQL ждёт вас в файле, готовый к чтению глазом.
Конфиг и генерация версии
GO// atlas.hcl — конфиг провайдера ent:
env "ent" {
src = "ent/schema"
dev = "docker://postgres/15/dev?search_path=public"
migration {
dir = "file://ent/migrate/migrations"
}
}
# первый снимок схемы — вся база одним файлом:
atlas migrate diff init --env ent
# после правки схемы — точечная версия:
atlas migrate diff add_bio --env entenv ent описывает вход: src — каталог схем, dev — временная база для расчёта diff, migration.dir — каталог версий. diff генерирует SQL-файл с именем вида <версия>_add_bio.sql и обновляет atlas.sum. Второй вызов после правки схем снимет только разницу: для поля bio это ALTER TABLE users ADD COLUMN bio — тот же результат, что у automigrate, но в файле, который вы можете прочитать и отклонить.
Проверка: зачем atlas.sum
В каталоге миграций рядом с SQL-файлами лежит atlas.sum. Что произойдёт, если вы отредактируете уже применённую версию миграции?
Конвейер: apply, status, lint#
Генерация — половина истории; вторая — исполнение и контроль. atlas migrate apply применяет отложенные версии к базе по порядку: каждая — отдельным шагом, отметка о применении пишется в служебную таблицу ревизий, и повторный apply честно пропустит то, что уже применено. atlas migrate status отвечает на главный вопрос дежурного: совпадает ли каталог с базой, какие версии ждут, нет ли расхождений чексумм. atlas migrate lint — статический анализ новых версий: инструмент читает SQL и подсвечивает разрушительные операции — DROP COLUMN, изменения типов, которые могут потерять данные, — до того, как их увидит ревьюер. Именно lint превращает ревью миграции из «поверю на слово» в «посмотрю отчёт». Из этих трёх команд складываются ворота CI: на pull request прогоняется diff + lint, на деплой — apply отдельным шагом, и только один раз — не на старте каждого пода. Отдельная тема для существующих баз — baseline: точка, с которой вы начинаете вести историю, объявляя текущее состояние базы уже применённым; atlas умеет генерировать её отдельной командой, чтобы не переигрывать всё с нуля.
Деплой: статус и применение
GO# что база думает о каталоге миграций:
atlas migrate status --env ent --url "$DSN"
# применить отложенные версии — один шаг деплоя:
atlas migrate apply --env ent --url "$DSN"status читает таблицу ревизий и сравнивает с каталогом: текущая версия, число отложенных, целостность atlas.sum. apply идёт по порядку и останавливается на первой ошибке — база не «перепрыгивает» сломанную версию, следующий apply продолжит с места остановки. Ключевое отличие от automigrate: исполнение — отдельный шаг с одним исполнителем, а не побочный эффект старта приложения.
Проверка: apply на втором инстансе
На деплое три пода. Первый успел исполнить atlas migrate apply и упал на старте по другой причине. Что увидят остальные два, если и они запустят apply?
Чеклист миграций перед продом#
- Правка схемы всегда в три шага: go generate из седьмого урока, atlas migrate diff с осмысленным именем, чтение SQL глазами перед коммитом.
- Применённые версии не редактируются никогда — даже чтобы «поправить опечатку»: исправление идёт новой версией, atlas.sum сторожит историю.
- Переименование колонки — три версии, а не одна: добавить новую, скопировать данные UPDATE-ом, убрать старую (и то — осознанным drop после периода совместимости).
- Разрушительные операции — только с lint-отчётом и планом отката: down-миграции у вас нет, откат пишется руками заранее.
- apply — отдельный шаг деплоя, не строка в main.go каждого пода; multi-instance-старт с Schema.Create — лотерея.
- На существующей базе начните с baseline: объявите текущее состояние применённым и ведите историю дальше.
- Локальные проверки — на dev-базе из atlas.hcl; продовая база получает только то, что прошло diff, lint и ревью.
Авто-миграция против версионных: карта выбора#
| Свойство | Schema.Create (automigrate) | atlas migrate (versioned) |
|---|---|---|
| Исполнитель | приложение на старте | CLI на шаге деплоя |
| Источник правды | сгенерированный migrate/schema.go | SQL-файлы каталога миграций |
| История версий | нет — только текущее состояние | да — каталог + таблица ревизий |
| Откат (down-миграция) | нет вовсе | пишется руками, заранее |
| Ревью человеком | нет — SQL не видит никто | да — файл в PR, lint-отчёт |
| Удаление колонок | только с WithDropColumn(true) | явный DROP COLUMN в версии |
| Переименование | тихая потеря данных | diff подсвечивает, ревью решает |
| Гонка multi-instance | каждый под мигрирует сам | один apply на деплой |
| Целостность истории | нечего проверять | atlas.sum, apply падает на подмене |
| Каноническое место | dev, прототип, пет-проект | prod, команда, CI/CD |
Runner-ов в курсе ent нет, и песочница не появится и здесь: миграциям нужны настоящая база и CLI — правило всего курса. Возьмите базу из домашек восьмого урока, включите лог SQL драйвера и запустите Schema.Create дважды: второй прогон обязан быть тихим — ни одного стейтмента. Затем добавьте поле about в схему User и посмотрите след: один ALTER TABLE, старая bio на месте, данные в ней — те же. Поставьте atlas, соберите atlas.hcl и сгенерируйте init-версию: прочитайте её SQL от начала до конца — это вся ваша схема, как её видит база. В финале переименуйте bio в about и посмотрите, что diff предложит: ALTER ADD about — и тихое молчание про старую колонку.
Соберите обе стратегии
Впишите вызов авто-миграции, опцию drop-режима и подкоманду atlas для генерации версии.
Путь одной правки схемы
Соберите конвейер версионной миграции: от правки поля до обновлённой базы на проде.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Что дальше#
Вы умеете доводить схему до базы двумя путями: один вызов Schema.Create для разработки и полный конвейер atlas для прода — diff, lint, apply, ревью с atlas.sum в роли сторожа. Это замыкает контур из седьмого урока: схемы — код, кодогенерация — компилятор миграций, а база получает ровно то, что описано в Go. Дальше — две главы про поведение мутаций в рантайме. Шестнадцатый урок разберёт транзакции: client.Tx, идиому commit/rollback, вложенные Tx через savepoints — и вы наконец атомарно закроете «детей, потом родителя» из четырнадцатого. Семнадцатый — хуки: функции, которые перехватывают каждую мутацию до записи, валидируют её и пишут аудит-логи; там же вы автоматизируете счётчик posts_count, который двигали руками в тринадцатом. Домашнее задание ниже проведёт обе стратегии по вашей базе и заставит читать SQL как документ. Полный план из 25 уроков — на странице курса, механика кодогенерации — в седьмом уроке, тестовая инфраструктура с автосозданием схемы — в двадцать третьем, текстом.
Карточки: миграции
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- ent миграции живут в двух формах: авто-миграция client.Schema.Create(ctx) на старте приложения и версионные миграции atlas с SQL-файлами, ревью и ревизиями; механика у обеих одна — diff желаемого (схемы) и фактического (база).
- Авто-миграция — append-only: создаёт таблицы (CREATE TABLE IF NOT EXISTS), добавляет колонки и индексы, и никогда не удаляет — исчезновение поля из схемы не трогает данные.
- Переименование поля — самая коварная операция automigrate: новая колонка добавится пустой, старая останется с данными без единой ошибки; честный путь — добавить, скопировать UPDATE-ом, убрать старую отдельной версией.
- Опции Schema.Create: WithDropColumn/WithDropIndex включают DROP, WithGlobalUniqueID — сквозные id для полиморфных связей, WithForeignKeys(false) — FK руками; каждая опция — осознанный риск.
- Границы automigrate: нет истории, нет down-миграции, нет ревью, гонка при multi-instance-старте — стратегия разработки, не прода.
- Конвейер atlas: atlas migrate diff пишет версию в каталог ent/migrate/migrations и обновляет atlas.sum; lint подсвечивает разрушительные операции, apply применяет отложенные версии и пишет ревизии, status показывает расхождения.
- atlas.sum — целостность истории: отредактированная применённая версия останавливает apply; правки идут новыми версиями, а не правками старых.
- Правило деплоя: apply — отдельный шаг, один раз на выкладку; automigrate в проде заменяют версионными миграциями с baseline для существующих баз.
Домашнее задание
Работайте на базе из восьмого урока — анна, борис, вера, их посты и группа go-ninja — с включённым логом SQL драйвера. Первое: запустите Schema.Create дважды подряд и докажите себе, что второй прогон тихий: ни одного CREATE TABLE, ни одного ALTER — запишите, сколько стейтментов исполнилось на пустой базе при первом. Второе: добавьте в схему User поле about (String, Optional, Nillable) и запустите automigrate: найдите в логе ALTER TABLE ADD COLUMN, затем SELECT-ом убедитесь, что bio и все её данные на месте. Третье: смоделируйте переименование — переименуйте bio в about, запустите Schema.Create и опишите письменно, что получилось в базе: две колонки, старая с данными, новая пустая, ни одной ошибки; затем верните схему к канону и допишите план честного переименования в три версии. Четвёртое: поставьте atlas, соберите atlas.hcl с docker-строкой dev-базы и сгенерируйте init-версию — прочитайте её SQL целиком и найдите в нём: join-таблицу group_users с двумя FK, уникальный индекс на email, все колонки posts. Пятое: добавьте about, снимите diff add_about, запустите atlas migrate lint и убедитесь, что отчёт чист; затем отредактируйте применённый init-файл (одну букву) и наблюдайте, как apply/status останавливается на atlas.sum — восстановите файл и убедитесь, что конвейер снова зелёный. Вопрос на подумать: почему ent не встроил down-миграции ни в automigrate, ни в atlas-конвейер, и какие три изменения в вашей схеме вы бы потребовали описать планом отката до ревью?
Личные заметкиПусто
Вопросы и ответы по уроку
Schema.Create или atlas: что выбрать для проекта?
По зрелости: разработка и прототип — Schema.Create на старте, она бесплатна и безопасна в append-only режиме. Как только у базы появился прод, команда и вопросы «а что было в прошлый вторник» — версионные миграции: diff в PR, lint, ревью, один apply на деплой. Переход не разрушителен: baseline объявляет текущее состояние применённым, и дальше вы живёте версиями.
Опасно ли включать WithDropColumn?
Опасно ровно настолько, насколько слепо включать drop-режим глобально. Держите его в отдельных запусках с конкретной целью: убрать хвост после переименования, снять индекс, признанный мёртвым. В версионном конвейере та же операция честнее: DROP COLUMN живёт в отдельной версии, проходит lint и ревью, где его видят глаза, а не флаг в main.go.
Как переименовать колонку без потери данных?
Никак одной операцией — ни automigrate, ни atlas не знают ваш замысел. Три версии: добавить новую колонку, скопировать данные (UPDATE ... SET new = old, можно дозировать по id), убрать старую после периода совместимости. В atlas-конвейере каждый шаг — отдельный файл с ревью и lint-отчётом; в automigrate шаги размоются, и «убрать старую» потребует WithDropColumn. Урок один: переименование — это миграционный план, а не правка поля.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
go-kit: микросервисный тулкит
Курс go-kit: endpoint, транспорт, логирование, метрики, трейсинг, circuit breaker, service discovery. 25 уроков с итоговым микросервисом.
- Понимать архитектуру endpoint → transport → service
- Оборачивать сервисы middleware: логи, метрики, трейсинг
- Подключать HTTP и gRPC транспорты
- Ставить rate limiting и circuit breaker