NodeJS Back-end Инженер

NodeJS Back-end Инженер

Роадмап навыков для прокачки

Что такое миграция базы данных?

DatabasesSQLMigrations

Основная идея

Миграция базы данных — это контролируемое изменение структуры (схемы) базы данных с отслеживанием версий. Миграции позволяют синхронизировать схему БД с кодом приложения, откатывать изменения и работать в команде с единой версией структуры данных.

Ключевые аспекты

  • Версионирование — каждая миграция имеет уникальный номер/timestamp, применяется последовательно
  • Up и Down — миграция содержит код применения (up) и отката (down)
  • Таблица миграций — БД хранит список применённых миграций для отслеживания состояния
  • Идемпотентность — миграция должна безопасно применяться повторно (или проверять, была ли применена)
  • Автоматизация — миграции запускаются автоматически при деплое

Что можно делать в миграциях

  • Создавать/удалять таблицы и колонки
  • Добавлять/удалять индексы и constraints
  • Менять типы данных колонок
  • Переносить данные между таблицами
  • Заполнять начальные данные (seed)

Частые ошибки на собеседованиях

  • Путают миграции с backup/restore — миграции меняют структуру, backup сохраняет данные
  • Забывают про down миграции — без них невозможен откат
  • Редактируют уже применённые миграции — это нарушает консистентность; нужна новая миграция
  • Не тестируют миграции локально — миграция может сломать production
  • Делают деструктивные изменения без подготовки — удаление колонки с данными без backup

Введение и проблематика

Зачем нужны миграции?

Представьте ситуацию: у вас работает приложение в production. Нужно добавить новое поле в таблицу пользователей. Как это сделать безопасно?

Без системы миграций:

  • Ручное выполнение SQL на сервере
  • Нет истории изменений
  • Невозможно откатить
  • Разработчики не синхронизированы

С миграциями:

  • Автоматическое применение изменений
  • Полная история в git
  • Возможность отката
  • Все разработчики с одинаковой схемой

Миграции — это "git для схемы базы данных". Как git отслеживает изменения кода, миграции отслеживают изменения структуры БД.


Базовая теория

Что такое миграция?

Миграция — это файл (или набор файлов), описывающий изменения структуры базы данных. Каждая миграция содержит:

  1. Уникальный идентификатор — timestamp или порядковый номер
  2. Up операция — код для применения изменений
  3. Down операция — код для отката изменений

Как работают миграции

graph LR A[Код миграции] --> B[Инструмент миграций] B --> C{Проверка таблицы миграций} C -->|Не применена| D[Выполнить UP] C -->|Уже применена| E[Пропустить] D --> F[Записать в таблицу миграций]

Таблица миграций

Инструмент миграций создаёт служебную таблицу для отслеживания:

sql
-- Пример таблицы миграций (Knex.js)
CREATE TABLE knex_migrations (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,      -- имя файла миграции
  batch INTEGER NOT NULL,          -- номер группы миграций
  migration_time TIMESTAMP         -- когда применена
);

Структура проекта с миграциями

      • 20240101_create_users.js
      • 20240102_add_user_email.js
      • 20240115_create_orders.js
      • 20240120_add_order_status.js
    • knexfile.js

  • Практические примеры

    Создание таблицы

    js
    // migrations/20240101_create_users.js
    exports.up = function(knex) {
      return knex.schema.createTable('users', (table) => {
        table.increments('id').primary();
        table.string('email', 255).notNullable().unique();
        table.string('password_hash', 255).notNullable();
        table.string('name', 100);
        table.enum('status', ['active', 'inactive', 'banned'])
          .defaultTo('active');
        table.timestamps(true, true); // created_at, updated_at
      });
    };
     
    exports.down = function(knex) {
      return knex.schema.dropTable('users');
    };

    Добавление колонки

    js
    // migrations/20240102_add_user_avatar.js
    exports.up = function(knex) {
      return knex.schema.alterTable('users', (table) => {
        table.string('avatar_url', 500);
        table.boolean('email_verified').defaultTo(false);
      });
    };
     
    exports.down = function(knex) {
      return knex.schema.alterTable('users', (table) => {
        table.dropColumn('avatar_url');
        table.dropColumn('email_verified');
      });
    };

    Создание индекса

    js
    // migrations/20240103_add_user_indexes.js
    exports.up = function(knex) {
      return knex.schema.alterTable('users', (table) => {
        table.index('status', 'idx_users_status');
        table.index(['status', 'created_at'], 'idx_users_status_created');
      });
    };
     
    exports.down = function(knex) {
      return knex.schema.alterTable('users', (table) => {
        table.dropIndex('status', 'idx_users_status');
        table.dropIndex(['status', 'created_at'], 'idx_users_status_created');
      });
    };

    Миграция данных

    js
    // migrations/20240110_split_user_name.js
    // Разделяем "John Doe" на firstName и lastName
     
    exports.up = async function(knex) {
      // 1. Добавляем новые колонки
      await knex.schema.alterTable('users', (table) => {
        table.string('first_name', 50);
        table.string('last_name', 50);
      });
     
      // 2. Мигрируем данные
      const users = await knex('users').whereNotNull('name');
      for (const user of users) {
        const [firstName, ...lastParts] = user.name.split(' ');
        const lastName = lastParts.join(' ');
     
        await knex('users')
          .where('id', user.id)
          .update({ first_name: firstName, last_name: lastName });
      }
     
      // 3. Удаляем старую колонку
      await knex.schema.alterTable('users', (table) => {
        table.dropColumn('name');
      });
    };
     
    exports.down = async function(knex) {
      // Обратная операция
      await knex.schema.alterTable('users', (table) => {
        table.string('name', 100);
      });
     
      const users = await knex('users');
      for (const user of users) {
        const name = [user.first_name, user.last_name].filter(Boolean).join(' ');
        await knex('users').where('id', user.id).update({ name });
      }
     
      await knex.schema.alterTable('users', (table) => {
        table.dropColumn('first_name');
        table.dropColumn('last_name');
      });
    };

    Жизненный цикл миграций

    Создание миграции

    Разработчик создаёт файл миграции с описанием изменений.

    Локальное тестирование

    Миграция применяется на локальной базе данных для проверки.

    Code review

    Миграция проверяется в pull request вместе с кодом приложения.

    Применение на staging

    Миграция тестируется на staging окружении.

    Применение в production

    Миграция автоматически применяется при деплое.


    Команды миграций

    Knex.js

    bash
    # Создать новую миграцию
    npx knex migrate:make create_orders
     
    # Применить все новые миграции
    npx knex migrate:latest
     
    # Откатить последнюю группу миграций
    npx knex migrate:rollback
     
    # Откатить все миграции
    npx knex migrate:rollback --all
     
    # Показать статус миграций
    npx knex migrate:status

    Prisma

    bash
    # Создать миграцию из изменений в schema.prisma
    npx prisma migrate dev --name add_orders
     
    # Применить миграции в production
    npx prisma migrate deploy
     
    # Сбросить БД и применить все миграции
    npx prisma migrate reset
     
    # Показать статус
    npx prisma migrate status

    Пограничные кейсы

    ⚠️

    Никогда не редактируйте уже применённые миграции! Если миграция применена в production, создайте новую миграцию для исправления. Редактирование сломает консистентность между окружениями.

    🚫
    Деструктивные операции требуют осторожности:
    • DROP COLUMN удаляет данные безвозвратно
    • Изменение типа колонки может потерять данные
    • Всегда делайте backup перед деструктивными миграциями

    Безопасное удаление колонки

    Перестать использовать колонку в коде

    Код больше не читает и не пишет в колонку.

    Задеплоить изменения кода

    Убедиться, что колонка не используется.

    Создать миграцию удаления

    Только после предыдущих шагов.

    Сделать backup

    На случай, если данные понадобятся.

    Применить миграцию

    Удалить колонку из базы данных.


    Плюсы и минусы

    ПлюсыМинусы
    История изменений в gitТребует дисциплины в команде
    Возможность откатаСложные миграции данных
    Синхронизация командыКонфликты при параллельной работе
    Автоматизация деплояБлокировки при ALTER TABLE
    ВоспроизводимостьНеобходимость поддержки down

    Вопросы интервьюера

    Q: Что делать, если миграция упала в production?

    Откатить миграцию (down), исправить проблему, создать новую версию миграции. Никогда не редактировать упавшую миграцию — она уже может быть частично применена.

    Q: Как тестировать миграции?

    Применять на локальной копии production базы. Проверять, что up работает, down работает, и up после down даёт тот же результат.

    Q: Можно ли запускать миграции параллельно?

    Нет, миграции должны выполняться последовательно. Инструменты миграций используют блокировки для предотвращения параллельного выполнения.

    Q: Как избежать downtime при миграции?

    Использовать online schema change инструменты, делать изменения в несколько этапов (expand-contract pattern), добавлять колонки как nullable.


    Источники