Модель данных (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": "Биография"})
- Имя таблицы по умолчанию — имя класса в snake_case (
UserProfile → user_profile), можно перезаписать с помощью__tablename__ = "xxx" - Словарь ограничений
Fieldи декларативная конфигурация класса одинаковы (choices/ge/le/max_length,descriptionподдерживает словарь i18n); движок проверки и проверка конфигурации используют один и тот же источник
Параметры поля
| Параметр | Описание |
|---|---|
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=...) |
| Общий базовый уровень | Движок проверки + словарь ограничений + регистрация категорий типов (одинаковые) | То же самое |
Эмпирическое правило: "как модуль работает" — используйте конфигурационный класс, "какие данные создал пользователь" — используйте модель.
Ограничения и замечания
- Бэкенд определяется глобальной конфигурацией хранения; модель может переопределить
__storage__для использования пользовательского экземпляраBaseStorage(для тестирования) - Поля
list/dict(включая параметризованные типы, напримерlist[int],dict[str, int]) хранятся как столбцы JSON, автоматическая сериализация при чтении и записи - При записи (
create/save) автоматически выполняются проверки ограничений, при ошибке выбрасываетсяValueError(локализованные сообщения) - Автоматическая миграция поддерживает только сценарии добавления новых столбцов (см. ниже); изменения типа столбца и удаление столбцов требуют ручной обработки
- При сбое подключения к хранилищу поведение такое же, как у слоя хранения: фреймворк не падает, после перерыва происходит автоматическое повторное подключение
Автоматическая миграция (шаг 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() # Двусторонняя связь
Ключевые моменты:
relatedпринимает имя класса в виде строки (ленивый парсинг по таблице регистрации классов моделей, порядок определения не важен) или напрямую передает класс модели- Запросы отношений и модели используют один и тот же бэкенд хранения (не поддерживается JOIN между разными бэкендами — запросы отношений — это два отдельных SQL-запроса)
- Запросы отношений не кэшируются, каждый вызов — это немедленный запрос; для произвольных пользовательских запросов можно использовать
User.where(...)