← Назад к вопросам

В каких случаях нужны кастомные миграции

2.2 Middle🔥 101 комментариев
#Django#Базы данных (SQL)

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Кастомные миграции в БД: когда они нужны

Кастомные миграции — это когда разработчик пишет собственный SQL код вместо того, чтобы полагаться на автоматическое сгенерирование инструментами ORM.

1. Когда автогенерация недостаточна

Большинство инструментов миграций (Alembic, Django migrations) генерируют базовые DDL операции:

# Автогенерирует:
ALTER TABLE users ADD COLUMN email VARCHAR(255);
ALTER TABLE users DROP COLUMN phone;
CREATE INDEX idx_users_email ON users(email);

Но есть сценарии, где нужна кастомная логика.

2. Случаи, когда нужны кастомные миграции

Случай 1: Миграция данных (Data Migration)

-- Была таблица:
-- users: id, name, email, phone

-- Изменения:
-- 1. Добавляем новое поле
-- 2. Заполняем его данными из старого
-- 3. Удаляем старое поле

ALTER TABLE users ADD COLUMN country_code VARCHAR(2);

-- Это кастомная логика! Нельзя автогенерировать
UPDATE users 
SET country_code = SUBSTRING(phone, 1, 2)
WHERE phone IS NOT NULL;

ALTER TABLE users DROP COLUMN phone;

На Python (Alembic):

from alembic import op
import sqlalchemy as sa

def upgrade():
    # Добавляем колонку
    op.add_column('users', sa.Column('country_code', sa.String(2)))
    
    # Кастомная миграция данных
    connection = op.get_bind()
    connection.execute(
        "UPDATE users SET country_code = SUBSTRING(phone, 1, 2) WHERE phone IS NOT NULL"
    )
    
    # Удаляем старую колонку
    op.drop_column('users', 'phone')

def downgrade():
    op.add_column('users', sa.Column('phone', sa.String(20)))
    op.drop_column('users', 'country_code')

Случай 2: Переименование колонки (безопасно)

-- ❌ ПЛОХО - теряем данные
ALTER TABLE users DROP COLUMN user_name;
ALTER TABLE users ADD COLUMN name VARCHAR(255);

-- ✅ ПРАВИЛЬНО - кастомная миграция
ALTER TABLE users RENAME COLUMN user_name TO name;

На Python:

def upgrade():
    # PostgreSQL
    op.execute('ALTER TABLE users RENAME COLUMN user_name TO name')
    
    # MySQL
    op.execute('ALTER TABLE users CHANGE COLUMN user_name name VARCHAR(255)')

def downgrade():
    op.execute('ALTER TABLE users RENAME COLUMN name TO user_name')

Случай 3: Создание индексов для оптимизации

from alembic import op
import sqlalchemy as sa

def upgrade():
    # Добавляем новую колонку
    op.add_column('posts', sa.Column('status', sa.String(20)))
    
    # Кастомный индекс для быстрого поиска по статусу
    op.execute(
        'CREATE INDEX idx_posts_status_created '
        'ON posts(status, created_at DESC) '
        'WHERE status = \'published\''  # Partial Index!
    )

def downgrade():
    op.drop_index('idx_posts_status_created')
    op.drop_column('posts', 'status')

Случай 4: Заполнение значений по умолчанию

def upgrade():
    # Добавляем поле с обязательным значением
    op.add_column(
        'users',
        sa.Column('is_verified', sa.Boolean(), nullable=False, server_default='false')
    )
    
    # Кастомная логика для существующих данных
    connection = op.get_bind()
    
    # Помечаем как верифицированы только админов
    connection.execute(
        "UPDATE users SET is_verified = TRUE WHERE role = 'admin'"
    )
    # Остальные остаются FALSE (по умолчанию)

def downgrade():
    op.drop_column('users', 'is_verified')

Случай 5: Денормализация для производительности

def upgrade():
    # Добавляем денормализованное поле
    op.add_column(
        'products',
        sa.Column('total_reviews_count', sa.Integer(), default=0)
    )
    
    # Заполняем начальные значения
    connection = op.get_bind()
    connection.execute("""
        UPDATE products p
        SET total_reviews_count = (
            SELECT COUNT(*) FROM reviews r WHERE r.product_id = p.id
        )
    """)

def downgrade():
    op.drop_column('products', 'total_reviews_count')

Случай 6: Сложные преобразования JSON

def upgrade():
    # Меняем структуру JSON данных
    connection = op.get_bind()
    
    # Было: {"first_name": "John", "last_name": "Doe"}
    # Стало: {"full_name": "John Doe"}
    
    connection.execute("""
        UPDATE users
        SET metadata = jsonb_set(
            metadata - 'first_name' - 'last_name',
            '{full_name}',
            to_jsonb(metadata->>'first_name' || ' ' || metadata->>'last_name')
        )
    """)

def downgrade():
    pass  # Реверс очень сложен, может быть невозможен

Случай 7: Создание VIEW

def upgrade():
    op.execute("""
        CREATE VIEW user_stats AS
        SELECT 
            u.id,
            u.name,
            COUNT(p.id) as post_count,
            COUNT(c.id) as comment_count
        FROM users u
        LEFT JOIN posts p ON u.id = p.user_id
        LEFT JOIN comments c ON u.id = c.user_id
        GROUP BY u.id, u.name
    """)

def downgrade():
    op.execute('DROP VIEW IF EXISTS user_stats')

Случай 8: Добавление триггеров

def upgrade():
    op.execute("""
        CREATE TRIGGER update_post_modified
        BEFORE UPDATE ON posts
        FOR EACH ROW
        BEGIN
            SET NEW.updated_at = NOW();
        END
    """)

def downgrade():
    op.execute('DROP TRIGGER IF EXISTS update_post_modified')

Случай 9: Bulk операции с большими таблицами

def upgrade():
    op.add_column('orders', sa.Column('category', sa.String(50)))
    
    connection = op.get_bind()
    
    # Обновляем большую таблицу батчами (не весь датасет за раз)
    batch_size = 10000
    offset = 0
    
    while True:
        connection.execute("""
            UPDATE orders
            SET category = CASE 
                WHEN amount < 100 THEN 'small'
                WHEN amount < 1000 THEN 'medium'
                ELSE 'large'
            END
            WHERE category IS NULL
            LIMIT :batch_size
        """, {'batch_size': batch_size})
        
        # Проверяем, остались ли строки для обновления
        result = connection.execute(
            "SELECT COUNT(*) FROM orders WHERE category IS NULL"
        )
        if result.fetchone()[0] == 0:
            break

def downgrade():
    op.drop_column('orders', 'category')

Случай 10: Работа с ENUM типами

def upgrade():
    # PostgreSQL
    op.execute("CREATE TYPE status_enum AS ENUM ('active', 'inactive', 'deleted')")
    
    op.add_column(
        'users',
        sa.Column('status', sa.Enum('active', 'inactive', 'deleted'), default='active')
    )
    
    # Миграция данных
    connection = op.get_bind()
    connection.execute("""
        UPDATE users
        SET status = CASE 
            WHEN is_deleted THEN 'deleted'::status_enum
            WHEN is_active THEN 'active'::status_enum
            ELSE 'inactive'::status_enum
        END
    """)
    
    op.drop_column('users', 'is_deleted')
    op.drop_column('users', 'is_active')

def downgrade():
    # Реверс сложный, обычно не реализуют полностью
    pass

3. Best Practices для кастомных миграций

# ✅ ПРАВИЛЬНО
def upgrade():
    # 1. Добавляем с default для обратной совместимости
    op.add_column(
        'users',
        sa.Column('new_field', sa.String(100), server_default='default_value')
    )
    
    # 2. Отдельно обновляем данные
    connection = op.get_bind()
    connection.execute("UPDATE users SET new_field = 'actual_value' WHERE condition")
    
    # 3. Затем удаляем default если больше не нужен
    op.alter_column('users', 'new_field', server_default=None)

# ❌ ПЛОХО
def upgrade():
    # Не добавляй новые constraints сразу
    op.add_column(
        'users',
        sa.Column('email', sa.String(100), nullable=False, unique=True)
    )
    # Если в таблице уже есть строки, это упадет!

4. Когда НЕ нужны кастомные миграции

# ✅ Автогенерация справится:
op.add_column('users', sa.Column('email', sa.String(255)))  # Новая колонка
op.drop_column('users', 'old_field')  # Удаление пустой колонки
op.create_index(...)  # Простой индекс
op.create_table(...)  # Новая таблица
op.add_constraint(...)  # Constraint без данных

Резюме: Кастомные миграции нужны для:

  1. Data migrations — преобразование существующих данных
  2. Rename операции — безопасное переименование
  3. Сложные индексы — partial indexes, expressions
  4. Заполнение значений — логика для default значений
  5. Денормализация — добавление кэш-полей
  6. JSON преобразования — сложные структуры
  7. Views и Triggers — функции БД
  8. Bulk операции — обновление больших таблиц
  9. ENUM типы — с миграцией из старых типов
  10. Откат операций — downgrade сложной логики

Правило: если ORM/инструмент не может это сделать безопасно, пиши кастомную миграцию!

В каких случаях нужны кастомные миграции | PrepBro