Пошук уроків, статей та іншого контенту
Організуєте міграції схеми бази даних у production без втрати даних і неконтрольованих змін.
Міграція бази даних — це версійна зміна структури або даних бази. Замість ручного редагування production-бази команда зберігає послідовність змін у репозиторії.
Наприклад:
prisma/
schema.prisma
migrations/
20260817100000_add_user_display_name/
migration.sql
20260818120000_make_display_name_required/
migration.sqlКожна міграція:
має унікальний ідентифікатор;
застосовується в певному порядку;
зберігається разом із кодом застосунку;
після застосування не змінюється;
реєструється в спеціальній таблиці бази даних.
У Next.js міграції не є частиною React-компонентів або API Route. Їх запускають як окремий крок процесу розгортання.
У цьому уроці використаємо Prisma як інструмент міграцій для Next.js.
Зазвичай використовують такі середовища:
локальне — для розробки;
тестове або staging — для перевірки міграцій;
production — для реальних даних.
Для кожного середовища має бути власне значення DATABASE_URL.
Наприклад, у локальному .env:
DATABASE_URL="postgresql://app:password@localhost:5432/app_development"Файл із production-секретами не потрібно зберігати в репозиторії.
Основні команди мають різне призначення:
npx prisma migrate devСтворює та застосовує нову міграцію під час локальної розробки. Команда може синхронізувати схему, генерувати Prisma Client і виконувати додаткові дії, потрібні для development.
npx prisma migrate deployЗастосовує вже створені міграції, яких ще немає в базі. Ця команда призначена для staging і production.
npx prisma migrate statusПоказує стан міграцій: чи є невиконані міграції, конфлікти або розбіжності.
У production не слід використовувати prisma migrate dev. Вона призначена для інтерактивної розробки та може змінювати структуру локальної бази не так, як очікується під час контрольованого розгортання.
Міграції повинні бути частиною коміту, який змінює код застосунку.
Наприклад, якщо додається поле displayName, коміт має містити:
prisma/schema.prisma
prisma/migrations/20260817100000_add_user_display_name/migration.sql
src/app/profile/page.tsxНе можна створювати міграцію локально, а потім додавати її до .gitignore.
Після створення та перевірки міграції не редагуйте її вручну без крайньої потреби. Якщо зміна ще не потрапила в жодне спільне середовище, простіше видалити локальну міграцію і створити нову. Якщо міграція вже застосована в staging або production, її потрібно вважати незмінною.
Початкова схема:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
createdAt DateTime @default(now())
}Припустімо, потрібно додати користувачам відображуване ім’я. Під час першого кроку поле має бути необов’язковим:
model User {
id Int @id @default(autoincrement())
email String @unique
displayName String?
createdAt DateTime @default(now())
}Створюємо міграцію локально:
npx prisma migrate dev --name add_user_display_namePrisma створить SQL-подібну міграцію. Для PostgreSQL вона може мати такий вигляд:
ALTER TABLE "User"
ADD COLUMN "displayName" TEXT;Ця операція безпечніша за негайне додавання обов’язкового поля, оскільки наявні рядки отримають NULL, а не помилку.
Після застосування міграції код може почати записувати нове поле:
// src/lib/users.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export async function updateDisplayName(
userId: number,
displayName: string,
) {
return prisma.user.update({
where: { id: userId },
data: { displayName },
});
}Production-процес має складатися щонайменше з таких кроків:
Створити артефакт застосунку.
Перевірити код і міграції.
Запустити міграції окремим release-кроком.
Запустити нову версію Next.js.
Для цього можна оголосити скрипти в package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"db:migrate:deploy": "prisma migrate deploy",
"db:migrate:status": "prisma migrate status"
}
}У production запускають:
npm ci
npm run db:migrate:deploy
npm run build
npm run startЗначення DATABASE_URL має бути доступним процесу, який виконує db:migrate:deploy.
Міграцію краще запускати в окремому release job або deployment job, а не під час першого HTTP-запиту. Інакше одночасні запити можуть звернутися до бази до завершення зміни схеми.
Не варто бездумно запускати міграцію в кожному екземплярі застосунку:
// Невдалий підхід: не запускайте міграції під час обробки запиту
export async function GET() {
// await migrateDatabase();
return Response.json({ ok: true });
}У production може працювати кілька екземплярів Next.js. Якщо кожен із них спробує змінити схему одночасно, розгортання стане непередбачуваним.
Під час розгортання стара і нова версії застосунку можуть короткий час працювати одночасно. Це особливо важливо для:
кількох інстансів;
rolling deployment;
serverless-платформ;
довгих background-задач;
кешованих сторінок і запитів.
Тому міграція повинна бути сумісною з обома версіями коду.
Наприклад, небезпечно одразу:
перейменувати колонку;
видалити стару колонку;
розгорнути код, який очікує лише нову назву.
Стара версія ще може виконати запит до старої колонки.
Надійніший підхід називають expand and contract.
Спочатку додаємо нову структуру, не видаляючи стару:
ALTER TABLE "User"
ADD COLUMN "displayName" TEXT;Нова колонка є необов’язковою, тому старий код продовжує працювати.
Новий код може записувати значення в нову колонку, а за потреби тимчасово підтримувати обидві колонки.
Після цього заповнюємо нову колонку даними зі старої:
UPDATE "User"
SET "displayName" = "name"
WHERE "displayName" IS NULL;Для великої таблиці таку операцію потрібно планувати окремо: вона може довго блокувати ресурси бази або створити значне навантаження.
Перевіряємо, що:
усі потрібні рядки заповнені;
новий код читає нову колонку;
старі екземпляри застосунку більше не використовуються;
фонові процеси також оновлені.
Лише після цього видаляємо стару структуру або робимо нову колонку обов’язковою.
Наприклад:
ALTER TABLE "User"
ALTER COLUMN "displayName" SET NOT NULL;Після такої зміни Prisma-схема може виглядати так:
model User {
id Int @id @default(autoincrement())
email String @unique
displayName String
createdAt DateTime @default(now())
}Це часто не одна, а кілька міграцій і розгортань. Такий підхід повільніший, але зменшує ризик простою та втрати даних.
Розглянемо небезпечну зміну:
model User {
id Int @id @default(autoincrement())
email String @unique
displayName String
createdAt DateTime @default(now())
}Якщо в таблиці вже є користувачі, база не знає, яке значення записати в displayName. Міграція може завершитися помилкою або вимагати значення за замовчуванням, яке не відповідає бізнес-логіці.
Безпечна послідовність:
Додати поле як nullable.
Розгорнути код, який починає його заповнювати.
Окремо заповнити поле для старих записів.
Перевірити відсутність NULL.
Зробити поле NOT NULL.
Перед фінальним кроком можна виконати перевірку:
SELECT COUNT(*)
FROM "User"
WHERE "displayName" IS NULL;Якщо результат не дорівнює нулю, робити поле обов’язковим ще рано.
Міграція схеми змінює структуру: таблиці, колонки, індекси або обмеження. Міграція даних змінює самі записи.
Наприклад, перенесення значень із name до displayName — це міграція даних.
Для невеликого обсягу даних її іноді можна виконати SQL-командою в самій міграції:
UPDATE "User"
SET "displayName" = "name"
WHERE "displayName" IS NULL;Для великої таблиці краще:
обробляти записи частинами;
контролювати тривалість операції;
уникати довгої транзакції;
вимірювати навантаження;
заздалегідь перевірити операцію на копії production-даних.
Не слід покладатися на useEffect у сторінці Next.js або на перший запит користувача для одноразового перенесення мільйонів записів. Це може виконатися кілька разів, перерватися посередині або створити навантаження на вебпроцес.
Якщо операція має бути повторно запущеною, зробіть її ідемпотентною:
UPDATE "User"
SET "displayName" = "name"
WHERE "displayName" IS NULL;Повторний запуск такої команди не перезапише вже оброблені значення.
Перед production перевірте міграцію на staging із даними, близькими за обсягом до production.
Корисний мінімальний процес:
npm ci
npx prisma generate
npm run db:migrate:status
npm run db:migrate:deploy
npm run buildПісля міграції перевірте:
запуск Next.js;
читання та запис змінених моделей;
API Route або Server Action, які використовують нові поля;
старі сценарії, які не повинні були зламатися;
індекси та обмеження;
тривалість виконання міграції;
логи release job.
Для production-копії бази важливо мати резервну копію перед ризикованими змінами. Резервна копія не замінює тестування: відновлення з неї також має бути перевірене заздалегідь.
Відкат коду і відкат бази — не завжди одна й та сама операція.
Якщо новий код не працює, часто безпечніше:
зупинити або призупинити розгортання;
повернути попередню версію коду;
залишити сумісні додаткові колонки;
виправити код і розгорнути нову версію.
Видалення колонки як автоматичний rollback може спричинити втрату даних. Тому деструктивні зміни зазвичай відкладають на окремий етап після перевірки.
Перед міграцією визначте:
чи є резервна копія;
як відновити базу;
як зупинити розгортання;
як повернути попередню версію застосунку;
чи можна тимчасово працювати зі старою схемою.
Проблема виникає, коли схему бази змінюють вручну, але не створюють відповідну міграцію. Тоді структура бази і Git-репозиторій розходяться.
Для перевірки використовуйте:
npx prisma migrate statusОзнаки проблеми:
у базі є зміни, яких немає в міграціях;
міграція застосована частково;
у production відсутня міграція з репозиторію;
локальна схема відрізняється від staging;
команда намагається повторно змінити вже застосовану міграцію.
У production не виправляйте таку розбіжність випадковою ручною SQL-командою. Спочатку зафіксуйте поточний стан, з’ясуйте походження зміни та сформуйте контрольований план вирівнювання.
Окремий скрипт може зупиняти процес при першій помилці:
#!/usr/bin/env bash
set -euo pipefail
echo "Перевірка стану міграцій"
npx prisma migrate status
echo "Застосування міграцій"
npx prisma migrate deploy
echo "Генерація Prisma Client"
npx prisma generate
echo "Збірка Next.js"
npm run buildТакий скрипт не виконує міграції під час обробки запитів. Якщо prisma migrate deploy завершується з помилкою, збірка або запуск нової версії не повинні продовжуватися.
Конкретний спосіб підключення цього скрипту залежить від платформи розгортання, але принцип залишається однаковим: міграції запускаються один раз як контрольований етап release-процесу.
migrate dev у productionprisma migrate dev призначена для локальної розробки. Для production використовуйте міграції, що вже збережені в репозиторії, і команду prisma migrate deploy.
Якщо файл міграції вже застосований у production, його редагування не змінить базу, але створить розбіжність між репозиторієм і журналом міграцій.
Створюйте нову міграцію.
Стара версія застосунку може ще звертатися до цієї колонки. Спочатку розгорніть сумісну зміну, переведіть код, і лише потім видаляйте стару структуру.
NOT NULL до заповненої таблиціНаявні записи не мають значень для нової колонки. Використовуйте послідовність nullable → заповнення даних → NOT NULL.
Кілька інстансів можуть почати змінювати схему одночасно. Запускайте міграції з одного release job.
Запит не повинен відповідати за зміну структури бази. Це створює гонки, повільні відповіді та непередбачувану поведінку.
Міграція, яка працює на маленькій локальній базі, може бути надто повільною на production. Перевіряйте ризиковані зміни на даних і обсягах, наближених до реальних.
Міграції зберігають зміни бази як послідовність версій у Git.
Для локальної розробки використовують prisma migrate dev, а для production — prisma migrate deploy.
Міграції запускають окремим release-кроком, не під час HTTP-запитів.
Зміни повинні бути сумісними зі старою і новою версіями застосунку.
Для небезпечних змін використовуйте підхід expand and contract.
Обов’язкові поля додавайте поетапно: спочатку nullable, потім заповнення даних, потім NOT NULL.
Уже застосовані міграції не редагуйте.
Перед production перевіряйте міграції на staging і готуйте план відновлення.