Миграции на схемата
Накратко: Миграцията е номерирана промяна на схемата на базата, която се пуска веднъж и държи базата на всеки компютър в една и съща версия.
Проблем
„no such column: phone“
Колега добавя колона phone към customers на своя компютър и пише кода, който я показва. На неговия компютър всичко работи. Новата версия отива на сайта и страницата „Клиенти“ пада с no such column: phone, защото в базата на сайта колона няма. На третия компютър в екипа пък има две колони, защото там някой я е добавил ръчно и с друго име.
Кодът е в git и всички имат една и съща версия. Схемата на базата трябва да е по същия начин: записана, с версии и еднаква навсякъде.
Идеята
Миграция: промяна с номер
Миграцията е малък SQL файл с номер. Всяка промяна по схемата е нов файл, и файловете са в git до кода:
migrations/ 001_create_shop.sql 002_reviews.sql 003_customers_phone.sqlБазата помни кои са пуснати в таблица, обикновено schema_migrations. При старта (или при пускане на новата версия) инструментът пуска поред само файловете, които ги няма там. Тази база е на версия 2:
Миграция 3 е промяната и записът за нея, заедно в една транзакция:
Пуснеш ли я втори път в същото поле, ALTER TABLE ще спре с „duplicate column name“. Затова инструментът проверява schema_migrations, преди да пусне файла.
Виж го
Защо в транзакция
Ако миграцията спре по средата без транзакция, първата промяна остава, а записът за версията липсва. Следващият опит пак ще започне отначало и ще се спъне в собствената си първа стъпка. Тук втората заявка е грешка (същата колона два пъти):
Транзакцията остава отворена с първия ALTER TABLE, а COMMIT не стига до базата. Пусни ROLLBACK; в полето и колоната phone изчезва: в SQLite и PostgreSQL промяната на схемата е част от транзакцията. Поправяш файла и го пускаш отново, а базата е точно каквато е била.
Пробвай
Миграция 3: точки за лоялност
Напиши миграция 3 за базата на версия 2: колона loyalty_points към customers (цяло число, задължително, по подразбиране 0) и запис (3, 'loyalty_points', '2026-10-25') в schema_migrations, в една транзакция.
Решение
BEGIN;
ALTER TABLE customers ADD COLUMN loyalty_points INTEGER NOT NULL DEFAULT 0;
INSERT INTO schema_migrations VALUES (3, 'loyalty_points', '2026-10-25');
COMMIT;Без DEFAULT 0 SQLite не добавя NOT NULL колона към таблица с редове, защото 20-те клиента нямат стойност за нея. Със DEFAULT 0 всеки получава 0. Записът в schema_migrations е в същата транзакция, за да не се пусне миграцията втори път.
Идеята
Инструментите
Никой не пише проверката „коя миграция е пусната“ сам. Всеки език има инструмент, който пази таблицата с версиите и пуска файловете поред:
- Java: Flyway или Liquibase (
V3__customers_phone.sql); - C#: EF Core Migrations (
dotnet ef migrations add CustomersPhone) пише миграцията от промяната в класовете; - Python: Alembic (към SQLAlchemy) или миграциите на Django;
- JavaScript: Prisma Migrate, Knex или node-pg-migrate.
Някои пишат и обратна стъпка (down), която отменя миграцията. На сайт с истински данни връщането назад рядко е DROP COLUMN: ако новата колона вече пази данни, обратната стъпка ги губи. Затова „назад“ обикновено е нова миграция напред.
Виж го
Промяна на жива база: на стъпки
Сайтът не спира, докато новата версия се качва: известно време работят и старият, и новият код. Ако миграцията преименува city на town, старият код пада в същата секунда. Затова промяната е на три стъпки:
- Разширяване. Добави
townи я попълни отcity. Старият код не я забелязва. - Преход. Новият код пише и в двете, а чете
town. Изчакваш, докато старият код го няма никъде. - Свиване. Отделна, по-късна миграция маха
city.
Упражнение
Име и фамилия поотделно
Магазинът иска да обръща клиентите по име („Здравей, Мария“). Направи първата стъпка (разширяване): добави колоните first_name и last_name към customers и ги попълни от name (всички имена са от две думи, разделени с интервал). Колоната name остава.
Решение
ALTER TABLE customers ADD COLUMN first_name TEXT;
ALTER TABLE customers ADD COLUMN last_name TEXT;
UPDATE customers
SET first_name = substr(name, 1, instr(name, ' ') - 1),
last_name = substr(name, instr(name, ' ') + 1);instr(name, ' ') дава мястото на интервала, а substr взима текста преди и след него. Колоната name остава, защото старият код още я чете. Махането ѝ е последната стъпка, в отделна миграция.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: ALTER TABLE: промяна на таблица · ACID: всичко или нищо · BEGIN, COMMIT, ROLLBACK · Връзка с базата от код
- Продължава в: Пул от връзки, резервни копия и права
- Задачи по темата: Базата в програмата