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

Как расширить стандартную модель в БД?

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

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

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

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

Расширение стандартной модели в БД

Расширение моделей — это изменение существующих схем БД без нарушения обратной совместимости. Это критически важно для production-систем, где нельзя просто пересоздавать таблицы.

1. Добавление нового поля

Проблема: Нужно добавить новое поле в существующую таблицу.

Решение: Используй миграции (Alembic, Django migrations, Goose).

С Goose (Raw SQL):

-- migrations/0001_add_profile_picture.sql
-- +goose Up
ALTER TABLE users ADD COLUMN profile_picture VARCHAR(255) NULL;
COMMENT ON COLUMN users.profile_picture IS "URL профиля пользователя";

-- +goose Down
ALTER TABLE users DROP COLUMN profile_picture;

С Alembic (SQLAlchemy):

# alembic/versions/001_add_profile_picture.py
from alembic import op
import sqlalchemy as sa

def upgrade():
    op.add_column(
        "users",
        sa.Column("profile_picture", sa.String(255), nullable=True)
    )

def downgrade():
    op.drop_column("users", "profile_picture")

Обновление SQLAlchemy модели:

from sqlalchemy import Column, String, Integer, DateTime
from datetime import datetime
from pytz import UTC

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    profile_picture = Column(String(255), nullable=True)  # Новое поле
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))

2. Добавление поля с дефолтным значением

Проблема: Новое поле должно иметь значение для существующих записей.

-- migrations/0002_add_is_verified.sql
-- +goose Up
ALTER TABLE users ADD COLUMN is_verified BOOLEAN DEFAULT FALSE;

-- +goose Down
ALTER TABLE users DROP COLUMN is_verified;

Важно: Всегда указывай DEFAULT чтобы не сломать существующие записи!

3. Изменение типа поля

Проблема: Нужно изменить тип данных (например, INTBIGINT).

-- migrations/0003_change_user_id_type.sql
-- +goose Up
-- PostgreSQL: безопасное изменение типа
ALTER TABLE posts ALTER COLUMN user_id TYPE BIGINT;

-- MySQL: требует переписания
-- ALTER TABLE posts MODIFY user_id BIGINT;

-- +goose Down
ALTER TABLE posts ALTER COLUMN user_id TYPE INTEGER;

4. Добавление индекса для оптимизации

Проблема: Частые запросы становятся медленными без индекса.

-- migrations/0004_add_email_index.sql
-- +goose Up
CREATE INDEX idx_users_email ON users(email);

-- +goose Down
DROP INDEX idx_users_email;

В SQLAlchemy:

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, nullable=False, index=True)  # Индекс здесь
    profile_picture = Column(String(255), nullable=True)

5. Создание новой таблицы для расширения

Проблема: Нужно добавить функционал, но расширение текущей таблицы усложнит схему.

Решение: Создай отдельную таблицу и связь.

-- migrations/0005_create_user_preferences.sql
-- +goose Up
CREATE TABLE user_preferences (
    id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    user_id BIGINT NOT NULL UNIQUE,
    theme VARCHAR(50) DEFAULT "light",
    notifications_enabled BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
    CONSTRAINT fk_user_preferences_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id);

-- +goose Down
DROP TABLE IF EXISTS user_preferences;

SQLAlchemy модели:

from sqlalchemy import ForeignKey, Relationship

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    
    # Связь One-to-One
    preferences = relationship("UserPreferences", back_populates="user", uselist=False)

class UserPreferences(Base):
    __tablename__ = "user_preferences"
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True)
    theme = Column(String(50), default="light")
    notifications_enabled = Column(Boolean, default=True)
    
    # Обратная связь
    user = relationship("User", back_populates="preferences")

6. Переименование поля без потери данных

Проблема: Нужно переименовать поле, но данные должны сохраниться.

-- migrations/0006_rename_user_field.sql
-- +goose Up
-- PostgreSQL
ALTER TABLE users RENAME COLUMN username TO login;

-- MySQL
ALTER TABLE users CHANGE COLUMN username login VARCHAR(255);

-- +goose Down
ALTER TABLE users RENAME COLUMN login TO username;

7. Использование наследования в ORM (Joined Table Inheritance)

Проблема: Нужны разные типы пользователей (администраторы, модераторы, обычные).

from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    user_type = Column(String(50))  # Discriminator
    
    __mapper_args__ = {
        "polymorphic_identity": "user",
        "polymorphic_on": user_type
    }

class Admin(User):
    __tablename__ = "admins"
    
    id = Column(Integer, ForeignKey("users.id"), primary_key=True)
    permissions = Column(String)
    
    __mapper_args__ = {
        "polymorphic_identity": "admin"
    }

class Moderator(User):
    __tablename__ = "moderators"
    
    id = Column(Integer, ForeignKey("users.id"), primary_key=True)
    moderation_scope = Column(String)
    
    __mapper_args__ = {
        "polymorphic_identity": "moderator"
    }

8. Zero-downtime миграции

Проблема: Миграция с новым полем может заблокировать таблицу в production.

Решение: Добавляй поле CONCURRENTLY:

-- migrations/0007_add_column_safe.sql
-- +goose Up
-- PostgreSQL: не блокирует таблицу
ALTER TABLE users ADD COLUMN new_field VARCHAR(255) NULL;

-- Создание индекса без блокировки
CREATE INDEX CONCURRENTLY idx_new_field ON users(new_field);

-- +goose Down
DROP INDEX CONCURRENTLY idx_new_field;
ALTER TABLE users DROP COLUMN new_field;

9. Практический пример: расширение модели User

-- migrations/0008_extend_user_model.sql
-- +goose Up

-- Добавляем новые поля
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20) NULL;
ALTER TABLE users ADD COLUMN bio TEXT NULL;
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMPTZ NULL;
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE;

-- Создаём индекс для часто используемого поля
CREATE INDEX idx_users_is_active ON users(is_active);

-- +goose Down
DROP INDEX idx_users_is_active;
ALTER TABLE users DROP COLUMN phone_number;
ALTER TABLE users DROP COLUMN bio;
ALTER TABLE users DROP COLUMN last_login_at;
ALTER TABLE users DROP COLUMN is_active;
from datetime import datetime
from pytz import UTC

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    phone_number = Column(String(20), nullable=True)
    bio = Column(Text, nullable=True)
    last_login_at = Column(DateTime(timezone=True), nullable=True)
    is_active = Column(Boolean, default=True, index=True)
    created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(UTC))

Основные принципы

  1. Всегда используй миграции — никогда не меняй схему вручную
  2. Добавляй DEFAULT — для новых полей без значений
  3. Создавай индексы — для часто используемых полей
  4. Используй CONCURRENTLY — в PostgreSQL для zero-downtime
  5. Откатывай правильно — каждая миграция должна иметь down-функцию
  6. Тестируй в dev — перед production применением

Расширение моделей — это вычислительный и архитектурный навык, требующий осторожности!

Как расширить стандартную модель в БД? | PrepBro