Как расширить стандартную модель в БД?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Расширение стандартной модели в БД
Расширение моделей — это изменение существующих схем БД без нарушения обратной совместимости. Это критически важно для 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. Изменение типа поля
Проблема: Нужно изменить тип данных (например, INT → BIGINT).
-- 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))
Основные принципы
- Всегда используй миграции — никогда не меняй схему вручную
- Добавляй DEFAULT — для новых полей без значений
- Создавай индексы — для часто используемых полей
- Используй CONCURRENTLY — в PostgreSQL для zero-downtime
- Откатывай правильно — каждая миграция должна иметь down-функцию
- Тестируй в dev — перед production применением
Расширение моделей — это вычислительный и архитектурный навык, требующий осторожности!