简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

Модель данных (ORM)

С версии 2.9.0 фреймворк включает декларативный слой модели данных: наследуйте Model и объявляйте поля с помощью Field, чтобы получить автоматическое создание таблиц и возможности добавления, удаления, изменения и поиска данных. Модели строятся на базе встроенной слоистой системы хранения — SQLite / MySQL / PostgreSQL определяется параметром ErisPulse.storage.backend, переключение бэкенда не требует изменения кода моделей.

Объявление модели

from ErisPulse.Core.Bases import Model, Field

class User(Model):
    id: int = Field(primary_key=True, autoincrement=True)
    name: str = Field(max_length=64)
    age: int = Field(default=0, ge=0, le=150)
    role: str = Field(default="user", choices=["user", "admin"])
    tags: list = Field(default_factory=list)          # JSON-столбец, автоматическая сериализация при чтении и записи
    bio: str = Field(default="", description={"i18n": "user.bio", "default": "Биография"})

Параметры поля

Параметр Описание
default Значение по умолчанию (если не предоставлено и не автоинкремент → обязательное поле NOT NULL)
default_factory Фабрика для изменяемых значений по умолчанию (например, list)
primary_key Первичный ключ
autoincrement Автоинкрементный первичный ключ (явно указывается как ключ, create автоматически заполняет)
max_length Максимальная длина строки (генерирует VARCHAR(n), проверка при записи)
nullable Допускается ли NULL (по умолчанию False)
index Генерация обычного индекса
unique Уникальное ограничение
choices / ge / le Перечисление / диапазон значений (проверка при записи)
description Описание (словарь i18n, формат совпадает с конфигурационным классом)
column_type Переопределение определения типа SQL-столбца

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

await User.create_table()                      #幂等操作 (IF NOT EXISTS)

user = await User.create(name="Alice", age=20) # Вставка (автоинкрементный ключ автоматически заполняется)
got = await User.get(id=user.id)               # Поиск первой строки по равенству

users = await User.where(User.age > 18).order_by("-age").limit(10).all()
first = await User.where(User.name == "Alice").first()
total = await User.count(User.age > 18)

user.age = 21
await user.save()                              # Обновление по первичному ключу (сначала проверка ограничений); если ключ отсутствует, переходит в режим вставки (автоинкрементный ключ также заполняется в экземпляр)
await user.delete()                            # Удаление по первичному ключу

await User.update_all(User.age > 18, role="adult")  # Массовое обновление
await User.delete_all(User.age > 100)               # Массовое удаление

Выражения для запросов поддерживают > >= < <= == != и in_([...]), а также комбинации & (и) / | (или):

await User.where((User.age > 18) & User.name.in_(["Alice", "Bob"])).all()

Транзакции

ORM и встроенная система хранения используют одну и ту же систему маршрутизации транзакций: если операции create / save / delete и запросы находятся в контексте storage.atransaction(), то они автоматически используют один и тот же соединение транзакции, подтверждение или откат происходит вместе с транзакцией, не происходит отдельного подтверждения.

async with storage.atransaction():
    await User.create(name="Alice")
    ...  # Если в блоке возникает исключение, INSERT откатывается

Отношение к классам декларативной конфигурации

Декларация моделей и ConfigClass (@dataclass + field(metadata=...)) разделяют один и тот же базовый уровень — словарь ограничений, движок проверки (validate_field_constraints), регистрация категорий типов (python_type_category); однако базовые классы намеренно разделены: поля конфигурации являются обычными значениями (TOML-двоичный обмен, горячая перезагрузка при однократной загрузке), поля моделей — дескрипторы столбцов (доступ к классу = выражение запроса, доступ к экземпляру построчно). Один и тот же синтаксис декларации, две раздельные базы.

Когда использовать какой тип декларации

Аспект Конфигурационный класс field(metadata=...) Модельное поле Field()
Область применения Параметры поведения модуля (малое количество, удобочитаемые, нуждаются в горячей перезагрузке) Записи бизнес-данных (много строк, программное чтение/запись, нуждаются в запросах)
Формат хранения config.toml (сохранение комментариев, двоичный обмен TOML) Таблица базы данных (автоматическое создание таблицы, SQL-диалект)
Формат значений Обычные значения (прямое чтение атрибута dataclass) Дескрипторы (доступ к классу = выражение столбца, доступ к экземпляру = значение строки)
Объявление ограничений metadata={"choices": ..., "min": ..., "max": ...} Field(choices=..., ge=..., le=..., max_length=...)
Общий базовый уровень Движок проверки + словарь ограничений + регистрация категорий типов (одинаковые) То же самое

Эмпирическое правило: "как модуль работает" — используйте конфигурационный класс, "какие данные создал пользователь" — используйте модель.

Ограничения и замечания

Автоматическая миграция (шаг 2)

create_table() при наличии таблицы автоматически сравнивает существующие столбцы с полями модели: новые поля автоматически выполняют ALTER TABLE ADD COLUMN (удаляются ограничения NOT NULL, заполняются NULL для существующих строк), новые поля с index=True одновременно создают индекс. Нет необходимости вручную писать скрипты миграции.

# После выхода v1 модели обновляются: добавляются поля email / bio
class User(Model):
    __tablename__ = "orm_users"

    id: int = Field(primary_key=True, autoincrement=True)
    name: str = Field(max_length=64)
    age: int = Field(default=0)
    email: str = Field(default="")     # Новое: при следующем create_table() автоматически добавляется столбец
    bio: str = Field(default="")

await User.create_table()              #幂等: только миграция новых столбцов

Ограничения: поддерживается только добавление столбцов; изменение первичного ключа, типа столбца, удаление столбцов требует ручной обработки (чтобы избежать случайных повреждающих ALTER).

Внешний ключ (основа сопоставления отношений)

foreign_key="table.column" объявляет ограничение внешнего ключа на уровне столбца, DDL генерирует подфразу REFERENCES:

class Post(Model):
    __tablename__ = "posts"

    id: int = Field(primary_key=True, autoincrement=True)
    author: int = Field(foreign_key="orm_users.id")

    content: str = Field(max_length=255)

Сопоставление отношений (relationship)

В теле класса модели объявляйте relationship() как атрибут класса, направление определяется автоматически по тому, где объявлен внешний ключ:

from ErisPulse.Core.Bases import Model, Field, relationship

class User(Model):
    __tablename__ = "orm_users"

    id: int = Field(primary_key=True, autoincrement=True)
    name: str = Field(max_length=64)

    posts = relationship("Post", foreign_key="author")   # Внешний ключ в другой таблице → has-many

class Post(Model):
    __tablename__ = "posts"

    id: int = Field(primary_key=True, autoincrement=True)
    author: int = Field(foreign_key="orm_users.id")
    content: str = Field(max_length=255)

    writer = relationship("User", foreign_key="author")  # Внешний ключ в этой таблице → belongs-to

has-many: атрибут экземпляра возвращает набор запросов, доступны все возможности QuerySet, create автоматически заполняет первичный ключ этой таблицы в столбец внешнего ключа другой таблицы:

alice = await User.get(name="Alice")

posts = await alice.posts.all()                          # Все статьи пользователя
latest = await alice.posts.order_by("-id").first()
total = await alice.posts.count()
hot = await alice.posts.where(Post.content != "").all()  # Дополнительные условия по столбцам другой таблицы
await alice.posts.delete()                               # Удаляются только статьи этого пользователя

new_post = await alice.posts.create(content="hi")        # author автоматически = alice.id

belongs-to: при await получается экземпляр другой модели (если нет соответствия или внешний ключ NULL, возвращается None):

post = await Post.get(id=1)
writer = await post.writer          # Экземпляр User или None
await writer.posts.count()          # Двусторонняя связь

Ключевые моменты: