-
-
Notifications
You must be signed in to change notification settings - Fork 0
Атрибуты
Полное руководство по всем атрибутам, поддерживаемым entity-derive.
Применяются к структуре с помощью #[entity(...)]:
#[derive(Entity)] #[entity( table = "users", schema = "core", sql = "full", dialect = "postgres", uuid = "v7", soft_delete, returning = "full", error = "AppError", events, hooks, commands, transactions )] pub struct User { /* ... */ }
| Атрибут | Обязательный | По умолчанию | Описание |
|---|---|---|---|
table |
Да | — | Имя таблицы БД |
schema |
Нет | "public" |
Схема БД |
sql |
Нет | "full" |
Уровень генерации SQL |
dialect |
Нет | "postgres" |
Диалект БД |
uuid |
Нет | "v7" |
Версия UUID для генерации ID |
soft_delete |
Нет | false |
Включить мягкое удаление |
returning |
Нет | "full" |
Режим RETURNING |
upsert(...) |
Нет | — | Генерация upsert-метода через INSERT ... ON CONFLICT
|
api(guard = "...") |
Нет | — | Принудительный FromRequestParts-гвард в генерируемых хендлерах |
error |
Нет | sqlx::Error |
Пользовательский тип ошибки |
events |
Нет | false |
Генерировать события жизненного цикла |
hooks |
Нет | false |
Генерировать трейт хуков |
commands |
Нет | false |
Включить паттерн CQRS-команд |
transactions |
Нет | false |
Генерировать адаптер для транзакций |
Имя таблицы базы данных.
#[entity(table = "users")] // → FROM users #[entity(table = "user_profiles")] // → FROM user_profiles
Схема базы данных. По умолчанию: "public".
#[entity(table = "users")] // → FROM public.users #[entity(table = "users", schema = "core")] // → FROM core.users #[entity(table = "users", schema = "auth")] // → FROM auth.users
Уровень генерации SQL. По умолчанию: "full".
| Значение | Repository Trait | Реализация PgPool | Применение |
|---|---|---|---|
"full" |
Да | Да | Стандартные CRUD-сущности |
"trait" |
Да | Нет | Пользовательские запросы (joins, CTE) |
"none" |
Нет | Нет | Только DTO, без БД |
#[entity(table = "users", sql = "full")] // Полная автоматизация (по умолчанию) #[entity(table = "users", sql = "trait")] // Только трейт, SQL реализуете сами #[entity(table = "users", sql = "none")] // Нет слоя БД вообще
Диалект БД для генерации SQL. По умолчанию: "postgres".
| Диалект | Алиасы | Тип клиента | Статус |
|---|---|---|---|
"postgres" |
"pg", "postgresql"
|
sqlx::PgPool |
Стабильно |
"clickhouse" |
"ch" |
clickhouse::Client |
Не реализовано — ошибка компиляции |
"mongodb" |
"mongo" |
mongodb::Client |
Не реализовано — ошибка компиляции |
Версия UUID для автоматически генерируемых первичных ключей. По умолчанию: "v7".
| Версия | Метод | Свойства |
|---|---|---|
"v7" |
Uuid::now_v7() |
Упорядочен по времени, сортируемый (рекомендуется) |
"v4" |
Uuid::new_v4() |
Случайный, широко совместимый |
#[entity(table = "users", uuid = "v7")] // Упорядоченный по времени (по умолчанию) #[entity(table = "sessions", uuid = "v4")] // Случайный UUID
Почему UUID v7?
- Упорядочен по времени: естественная сортировка по времени создания
- Лучшая производительность индексов БД
- Не требует координации (в отличие от sequences)
- Глобально уникален в распределённых системах
Включает мягкое удаление для пометки записей как удалённых вместо их удаления.
#[derive(Entity)] #[entity(table = "documents", soft_delete)] pub struct Document { #[id] pub id: Uuid, #[field(create, response)] pub title: String, #[field(skip)] pub deleted_at: Option<DateTime<Utc>>, // Обязательное поле }
Генерируемые методы:
-
delete()— Устанавливаетdeleted_at = NOW()вместо DELETE -
hard_delete()— Окончательно удаляет запись -
restore()— Устанавливаетdeleted_at = NULL -
find_by_id()/list()— Автоматически фильтрует удалённые записи -
find_by_id_with_deleted()/list_with_deleted()— Включает удалённые записи
Управляет тем, какие данные возвращаются после INSERT/UPDATE. По умолчанию: "full".
| Режим | SQL-клауза | Применение |
|---|---|---|
"full" |
RETURNING * |
Получить все поля включая сгенерированные БД (по умолчанию) |
"id" |
RETURNING id |
Подтвердить вставку, вернуть готовую сущность |
"none" |
(без RETURNING) | Fire-and-forget, самый быстрый вариант |
"col1, col2" |
RETURNING col1, col2 |
Вернуть определённые колонки |
#[entity(table = "logs", returning = "none")] // Самый быстрый #[entity(table = "users", returning = "full")] // Получить сгенерированные БД значения #[entity(table = "events", returning = "id, created_at")] // Пользовательские колонки
Надёжная доставка событий через транзакционный outbox. Просто events лишь генерирует enum; со streams NOTIFY — fire-and-forget. events(outbox) заставляет каждую генерируемую запись вставлять сериализованное событие в таблицу entity_outbox в той же транзакции, а рантайм OutboxDrainer (entity-core, фича outbox) доставляет строки через FOR UPDATE SKIP LOCKED с экспоненциальным backoff и парковкой после max_attempts. At-least-once — обработчики должны быть идемпотентными. Совместимо со streams.
#[derive(Entity, Serialize, Deserialize)] #[entity(table = "orders", events(outbox), migrations)] pub struct Order { /* ... */ } sqlx::query(Order::MIGRATION_OUTBOX).execute(&pool).await?; struct Notifier; #[async_trait::async_trait] impl entity_core::outbox::OutboxHandler for Notifier { type Error = anyhow::Error; async fn handle(&self, row: &OutboxRow) -> Result<(), Self::Error> { deliver(&row.entity, &row.payload).await } } entity_core::outbox::OutboxDrainer::new(pool, Notifier).run().await;
Генерирует метод репозитория upsert на основе INSERT ... ON CONFLICT.
#[derive(Entity)] #[entity(table = "users", upsert(conflict = "email"))] pub struct User { #[id] pub id: Uuid, #[field(create, response)] #[column(unique)] pub email: String, #[field(create, update, response)] pub name: String, }
| Опция | Обязательна | По умолчанию | Описание |
|---|---|---|---|
conflict |
Да | — | Колонки конфликта через запятую |
action |
Нет | "update" |
"update" (DO UPDATE) или "nothing" (DO NOTHING) |
Генерируется:
-
action = "update"→async fn upsert(&self, dto: CreateUserRequest) -> Result<User, Error>— перезаписывает все неконфликтные колонки (DO UPDATE SET col = EXCLUDED.col) и возвращает сохранённую строку -
action = "nothing"→async fn upsert(&self, dto: CreateUserRequest) -> Result<Option<User>, Error>— существующая строка не изменяется;Noneозначает, что конфликтующая строка уже существовала
Проверки на этапе компиляции:
- колонки конфликта должны существовать и иметь гарантию уникальности (
#[id],#[column(unique)]или подходящийunique_index(...)) - требуется
returning = "full"(значение по умолчанию) - для
action = "update"нужна хотя бы одна неконфликтная обновляемая колонка
При включённых streams upsert публикует уведомление Created для каждой возвращённой строки.
Принудительная аутентификация в генерируемых хендлерах. security = "..." лишь документирует auth в OpenAPI; guard внедряет axum-extractor (FromRequestParts) первым аргументом каждого генерируемого CRUD- и командного хендлера — неудачная экстракция отклоняет запрос до выполнения тела хендлера.
pub struct RequireAuth; impl<S: Send + Sync> FromRequestParts<S> for RequireAuth { type Rejection = StatusCode; async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> { parts.headers.contains_key("authorization") .then_some(Self) .ok_or(StatusCode::UNAUTHORIZED) } } #[derive(Entity)] #[entity(table = "users", api(tag = "Users", handlers, guard = "RequireAuth", guard(list = "none")))] pub struct User { /* ... */ }
Пооперационные оверрайды: guard(create = "Admin", list = "none", ...) с операциями create, get, update, delete, list, commands; литерал "none" отключает гвард. Команды из public = [...] гвард не получают.
Пользовательский тип ошибки для репозитория. По умолчанию: sqlx::Error.
#[derive(Debug)] pub enum AppError { Database(sqlx::Error), NotFound, Validation(String), } impl std::error::Error for AppError {} impl std::fmt::Display for AppError { /* ... */ } // Обязательно: преобразование из sqlx::Error impl From<sqlx::Error> for AppError { fn from(err: sqlx::Error) -> Self { AppError::Database(err) } } #[derive(Entity)] #[entity(table = "users", error = "AppError")] pub struct User { /* ... */ } // Сгенерированный репозиторий использует AppError: // impl UserRepository for PgPool { // type Error = AppError; // ... // }
Генерирует enum событий жизненного цикла. Подробнее см. События.
#[entity(table = "orders", events)]Генерируется:
pub enum OrderEvent { Created(Order), Updated { id: Uuid, changes: UpdateOrderRequest }, Deleted(Uuid), }
Генерирует трейт хуков жизненного цикла. Подробнее см. Хуки.
#[entity(table = "users", hooks)]Генерируется:
#[async_trait] pub trait UserHooks: Send + Sync { type Error: std::error::Error + Send + Sync; async fn before_create(&self, dto: &mut CreateUserRequest) -> Result<(), Self::Error>; async fn after_create(&self, entity: &User) -> Result<(), Self::Error>; async fn before_update(&self, id: &Uuid, dto: &mut UpdateUserRequest) -> Result<(), Self::Error>; async fn after_update(&self, entity: &User) -> Result<(), Self::Error>; async fn before_delete(&self, id: &Uuid) -> Result<(), Self::Error>; async fn after_delete(&self, id: &Uuid) -> Result<(), Self::Error>; }
Включает паттерн CQRS-команд. Подробнее см. Команды.
#[entity(table = "users", commands)] #[command(Register)] #[command(Deactivate, requires_id)]
Генерирует адаптер репозитория для типобезопасных транзакций с несколькими сущностями.
#[derive(Entity)] #[entity(table = "accounts", transactions)] pub struct Account { #[id] pub id: Uuid, #[field(create, update, response)] pub balance: i64, }
Генерируется:
-
AccountTransactionRepo<'t>— Адаптер репозитория для контекста транзакции - Трейт
TransactionWithAccountс методомwith_accounts()
Использование:
use entity_core::prelude::*; async fn transfer(pool: &PgPool, from: Uuid, to: Uuid, amount: i64) -> Result<(), AppError> { Transaction::new(pool) .with_accounts() .run(|mut ctx| async move { let from_acc = ctx.accounts().find_by_id(from).await? .ok_or(AppError::NotFound)?; ctx.accounts().update(from, UpdateAccountRequest { balance: Some(from_acc.balance - amount), }).await?; ctx.accounts().update(to, UpdateAccountRequest { balance: Some(to_acc.balance + amount), }).await?; Ok(()) }) .await }
Методы транзакции:
-
create(dto)— Создать сущность в транзакции -
find_by_id(id)— Найти сущность по ID -
update(id, dto)— Обновить сущность -
delete(id)— Удалить сущность (учитываетsoft_delete) -
list(limit, offset)— Список сущностей
Особенности:
- Автоматический откат при ошибке или панике
- Типобезопасный паттерн builder
- Полная поддержка CRUD в транзакции
Применяются к отдельным полям.
Отмечает поле первичного ключа.
Поведение:
- Автоматически генерирует UUID (v7 по умолчанию, настраивается атрибутом
uuid) - Всегда включается в
ResponseDTO - Исключается из
CreateRequestиUpdateRequest
#[id] pub id: Uuid,
Отмечает автоматически генерируемые поля (timestamps, sequences).
Поведение:
- Получает
Default::default()вFrom<CreateRequest> - Исключается из
CreateRequestиUpdateRequest - Может включаться в
Responseс#[field(response)] - Исключается из генерируемого
INSERT, поэтому сmigrationsnon-nullable колонка с временным типом получает соответствующий default в БД:NOW()дляTIMESTAMPTZиTIMESTAMP,CURRENT_DATEдляDATE,CURRENT_TIMEдляTIME. Явный#[column(default = "...")]имеет приоритет
#[auto] #[field(response)] pub created_at: DateTime<Utc>,
Управляет включением в DTO. Комбинируйте несколько опций:
#[field(create)] // Только в CreateRequest #[field(update)] // Только в UpdateRequest #[field(response)] // Только в Response #[field(create, response)] // В Create и Response #[field(create, update, response)] // Во всех трёх #[field(skip)] // Исключено из всех DTO
Включает поле в CreateRequest DTO.
#[field(create)] pub email: String, // Генерируется: pub struct CreateUserRequest { pub email: String, }
Включает поле в UpdateRequest DTO.
Важно: Необязательные поля автоматически оборачиваются в Option<T> для частичных обновлений.
#[field(update)] pub name: String, // Не Option // Генерируется: pub struct UpdateUserRequest { pub name: Option<String>, // Обёрнуто автоматически }
Включает поле в Response DTO.
#[field(response)] pub email: String, // Генерируется: pub struct UserResponse { pub id: Uuid, // Всегда включено (имеет #[id]) pub email: String, // Включено }
Исключает поле из всех DTO. Используйте для конфиденциальных данных.
#[field(skip)] pub password_hash: String,
Важно: skip перекрывает все другие опции поля. Поле будет существовать только в:
- Оригинальной структуре сущности
- Структуре
Row(для чтения из БД) - Структуре
Insertable(для записи в БД)
Подключает Postgres-enum (ValueObject) к генерации DDL.
#[derive(ValueObject, Debug, Clone, Serialize, Deserialize)] #[value_object(pg_type = "order_status", sqlx)] pub enum OrderStatus { Pending, Shipped, Delivered } #[derive(Entity)] #[entity(table = "orders", migrations)] pub struct Order { #[id] pub id: Uuid, #[field(create, update, response)] #[column(pg_enum = "order_status")] pub status: OrderStatus, } for ddl in Order::MIGRATION_TYPES { sqlx::query(ddl).execute(&pool).await?; } sqlx::query(Order::MIGRATION_UP).execute(&pool).await?;
- Задаёт тип колонки в DDL (иначе enum-поля откатываются к TEXT)
- Регистрирует идемпотентный
PG_CREATE_TYPEenum'а в{Entity}::MIGRATION_TYPES— выполняйте их доMIGRATION_UP - Указанное имя сверяется с константой
PG_TYPEenum'а на этапе компиляции; несовпадение ломает сборку - Опциональный флаг
sqlxуValueObjectгенерирует импелыsqlx::Type/Encode/Decode; не указывайте его, если уже деривитеsqlx::Type
Уровень сущности. Объявляет выборку по OR-группе колонок одного типа — «строки, где участник задействован в любой роли».
#[derive(Entity)] #[entity(table = "disputes")] #[scope(involving: requester_id | subject_id)] #[scope(handled: requester_id | subject_id, within = parcel_id)] pub struct Dispute { /* ... */ }
Генерируется: list_involving(value, limit, offset) и list_handled(parcel_id, value, limit, offset). Значение биндится один раз и сравнивается со всеми объявленными колонками; within сначала сужает группу до одного родителя. Нужны минимум две колонки, они должны существовать и совпадать по типу — всё проверяется на компиляции. Учитывает мягкое удаление, порядок по id по убыванию.
Скоупинг строк по владельцу. Помечает колонку с id владельца; репозиторий получает scoped-методы, которые не раскрывают существование чужих строк и учитывают soft_delete.
#[derive(Entity)] #[entity(table = "orders")] pub struct Order { #[id] pub id: Uuid, #[owner] pub user_id: Uuid, #[field(create, update, response)] pub note: String, } let mine = pool.list_by_owner(user_id, 20, 0).await?; let order = pool.find_by_id_scoped(id, user_id).await?; let updated = pool.update_scoped(id, user_id, patch).await?; let removed = pool.delete_scoped(id, user_id).await?;
Генерируется: find_by_id_scoped, list_by_owner, update_scoped (при наличии update-полей; None, если строка не принадлежит владельцу), delete_scoped. Максимум одно поле #[owner]; сочетание с #[id] отклоняется на этапе компиляции.
Генерирует поля фильтрации запросов. Подробнее см. Фильтрация.
#[filter] // Точное совпадение: WHERE field = $n #[filter(eq)] // То же самое #[filter(like)] // Совпадение по шаблону: WHERE field ILIKE $n #[filter(range)] // Диапазон: WHERE field >= $n AND field <= $m
Связь по внешнему ключу. Подробнее см. Связи.
#[belongs_to(User)] pub user_id: Uuid,
Генерируется: метод find_user() в репозитории.
Связь один-ко-многим (на уровне сущности). Подробнее см. Связи.
#[has_many(Post)] pub struct User { /* ... */ }
Генерируется: метод find_posts() в репозитории.
Переопределяет описание поля в OpenAPI. Список токенов передаётся в
utoipa без изменений и попадает на каждую сгенерированную структуру с
ToSchema — DTO создания и ответа, а также объединённое представление.
Колонка JSONB с типом serde_json::Value документируется как объект
произвольной формы; переопределение называет реальную форму:
#[field(create, response)] #[schema(value_type = Option<SizeCm>)] pub size_cm: Option<serde_json::Value>,
Без возможности api ни одна сгенерированная структура не выводит
ToSchema, поэтому атрибут отбрасывается. DTO обновления его не несут:
макрос переписывает типы их полей ради семантики PATCH, и объявленный
value_type противоречил бы сгенерированному типу.
Генерирует структуру частичного представления (на уровне сущности).
#[projection(Public: id, name, avatar)] #[projection(Admin: id, name, email, role)] pub struct User { /* ... */ }
Генерируется:
UserPublic { id, name, avatar }UserAdmin { id, name, email, role }- Реализации
From<User> - Методы
find_by_id_public(),find_by_id_admin()
Применяются на уровне сущности с помощью #[command(...)].
| Синтаксис | Эффект |
|---|---|
#[command(Name)] |
Использует все поля #[field(create)]
|
#[command(Name: field1, field2)] |
Использует только указанные поля (добавляет requires_id) |
#[command(Name, requires_id)] |
Добавляет поле ID, без других полей |
#[command(Name, source = "create")] |
Явно использовать create-поля (по умолчанию) |
#[command(Name, source = "update")] |
Использовать update-поля (опциональные, добавляет requires_id) |
#[command(Name, source = "none")] |
Без полей payload |
#[command(Name, payload = "Type")] |
Использует пользовательскую структуру payload |
#[command(Name, result = "Type")] |
Использует пользовательский тип результата |
#[command(Name, kind = "create")] |
Подсказка: создаёт сущность (по умолчанию) |
#[command(Name, kind = "update")] |
Подсказка: изменяет сущность |
#[command(Name, kind = "delete")] |
Подсказка: удаляет сущность (возвращает ()) |
#[command(Name, kind = "custom")] |
Подсказка: пользовательская операция |
Подробнее см. Команды.
#[derive(Entity)] #[entity( table = "posts", schema = "blog", sql = "full", dialect = "postgres", uuid = "v7", soft_delete, returning = "full", events, hooks, commands )] #[has_many(Comment)] #[projection(Summary: id, title, author_id, created_at)] #[command(Publish)] #[command(Archive, requires_id)] pub struct Post { #[id] pub id: Uuid, #[field(create, update, response)] #[filter(like)] pub title: String, #[field(create, update, response)] pub content: String, #[field(create, response)] #[belongs_to(User)] #[filter] pub author_id: Uuid, #[field(update, response)] pub published: bool, #[field(response)] #[filter(range)] pub view_count: i64, #[field(skip)] pub moderation_notes: String, #[field(skip)] pub deleted_at: Option<DateTime<Utc>>, #[auto] #[field(response)] #[filter(range)] pub created_at: DateTime<Utc>, #[auto] #[field(response)] pub updated_at: DateTime<Utc>, }
| Я хочу... | Атрибуты |
|---|---|
| Автогенерировать первичный ключ | #[id] |
| Использовать случайный UUID |
uuid = "v4" на сущности |
| Использовать упорядоченный по времени UUID |
uuid = "v7" (по умолчанию) |
| Принимать в теле POST | #[field(create)] |
| Принимать в теле PATCH | #[field(update)] |
| Возвращать в ответе API | #[field(response)] |
| Принимать и возвращать | #[field(create, update, response)] |
| Скрыть от всех API | #[field(skip)] |
| Автогенерировать timestamp |
#[auto] + #[field(response)]
|
| Только для чтения (управляет БД) | только #[field(response)]
|
| Только для записи (без возврата) | только #[field(create)]
|
| Пользовательские SQL-запросы | sql = "trait" |
| Только DTO, без БД | sql = "none" |
| Мягкое удаление записей |
soft_delete на сущности |
| Пользовательский тип ошибки |
error = "MyError" на сущности |
| Фильтровать по точному значению |
#[filter] на поле |
| Фильтровать по шаблону |
#[filter(like)] на поле |
| Фильтровать по диапазону |
#[filter(range)] на поле |
| Отслеживать изменения сущности |
events на сущности |
| Выполнять код при жизненном цикле |
hooks на сущности |
| Использовать доменные команды |
commands на сущности + #[command(...)]
|
| Использовать транзакции с несколькими сущностями |
transactions на сущности |
| Определить связь |
#[belongs_to(Entity)] или #[has_many(Entity)]
|
| Частичное представление сущности | #[projection(Name: fields)] |
| Переопределить тип поля в OpenAPI | #[schema(value_type = T)] |
Генерируемые обновления — настоящие частичные патчи: SET-клауза строится в рантайме из реально присутствующих полей, пропущенные поля не трогаются. Nullable-колонки используют двойной Option (None = не менять, Some(None) = записать NULL, Some(Some(v)) = записать v) через entity_core::serde_helpers::double_option.
Цепочечные сеттеры выражают тот же патч без вложенности: set_{field} для каждой обновляемой колонки, clear_{field} для nullable и expecting_version, если объявлен #[version].
let patch = UpdateUserRequest::default() .set_name("Neo".into()) .clear_nickname();
Конструирование через структурный литерал продолжает работать — сеттеры только дополняют его.
// {} → nothing changes // {"nickname": null} → nickname = NULL // {"nickname": "neo"} → nickname = 'neo' let patch: UpdateProfileRequest = serde_json::from_str(body)?; let profile = pool.update(id, patch).await?;
Помимо простого флага, migrations принимает DDL-опции: touch_updated_at (общая plpgsql-функция + BEFORE UPDATE триггер на таблицу, обновляющий updated_at; требует поля updated_at, проверяется на компиляции), audit (таблица entity_audit_log + триггер с диффами to_jsonb(OLD/NEW)) и extensions = "pg_trgm, pgcrypto" (идемпотентные CREATE EXTENSION). Новые константы: MIGRATION_TRIGGERS (выполнять после MIGRATION_UP) и MIGRATION_EXTENSIONS (до).
#[entity(table = "articles", migrations(touch_updated_at, audit, extensions = "pg_trgm"))] pub struct Article { /* ... */ } for ddl in Article::MIGRATION_EXTENSIONS { sqlx::query(ddl).execute(&pool).await?; } sqlx::query(Article::MIGRATION_UP).execute(&pool).await?; for ddl in Article::MIGRATION_TRIGGERS { sqlx::query(ddl).execute(&pool).await?; }
Оптимистичная блокировка. Помечает целочисленную колонку (i16/i32/i64); Update DTO получает обязательное поле expected_version, генерируемый UPDATE инкрементирует колонку и применяется только пока версия в базе совпадает — устаревшая запись падает с ошибкой конфликта вместо перезаписи свежих данных. DDL по умолчанию INTEGER NOT NULL DEFAULT 0. Работает в обычном, scoped и транзакционном update.
#[derive(Entity)] #[entity(table = "orders", migrations)] pub struct Order { #[id] pub id: Uuid, #[field(create, update, response)] pub note: String, #[version] #[field(response)] #[auto] pub version: i32, } let patch = UpdateOrderRequest { note: Some("v2".into()), expected_version: order.version }; let updated = pool.update(order.id, patch).await?;
Макрос знает каждый constraint, который создаёт. С этим флагом генерируемые write-методы резолвят имена нарушенных constraint'ов (unique-колонки, внешние ключи belongs_to, check'и колонок, имена unique_index) и отдают entity_core::ConstraintError { kind, constraint, field } вместо сырой ошибки драйвера. Требует кастомного типа error с From<ConstraintError>; без флага поведение не меняется.
#[entity(table = "users", typed_constraints, error = "AppError")] pub struct User { #[id] pub id: Uuid, #[field(create, response)] #[column(unique)] pub email: String, } match pool.create(dto).await { Err(AppError::Constraint(v)) if v.field == Some("email") => conflict_409(), other => other?, }
Разворачивает value object в плоские колонки с префиксом. DDL, Row-структура, CRUD SQL и динамический PATCH работают с price_amount_cents / price_currency, а DTO и сущность несут саму структуру. Объявленная форма деструктурируется против реальной структуры на этапе компиляции — переименованное, перетипизированное, отсутствующее или лишнее поле ломает сборку. Родители Option<T> пока не поддерживаются.
pub struct Money { pub amount_cents: i64, pub currency: String } #[derive(Entity)] #[entity(table = "products", migrations)] pub struct Product { #[id] pub id: Uuid, #[field(create, update, response)] #[embed(prefix = "price_", fields(amount_cents: i64, currency: String))] pub price: Money, }
Включает garde::Validate на генерируемых DTO как поддерживаемую альтернативу validator. Правила #[validate(...)] (length, range, email, url, pattern) транслируются в синтаксис garde; поля без ограничений получают garde(skip); Option-обёртки Update DTO валидируют внутреннее значение через inner(...). При включении обеих фич приоритет у validate.
#[field(create, update, response)] #[validate(length(min = 3, max = 8))] pub name: String, let dto: CreateUserRequest = serde_json::from_str(body)?; garde::Validate::validate(&dto)?;
Объявляет constraint'ы, которые макрос не может вывести — внешние ключи по натуральным ключам, CHECK'и с кастомными именами, индексы из ручных миграций — чтобы нарушения резолвились в ConstraintError с указанным полем. Виды: unique, foreign_key, check. Кастомные записи имеют приоритет над выведенными с тем же именем. Требует typed_constraints.
#[entity( table = "orders", typed_constraints, constraint(name = "orders_currency_fkey", kind = "foreign_key", field = "currency"), constraint(name = "orders_window_check", kind = "check"), )]
При включённых transactions и upsert(...) адаптер {Entity}TransactionRepo получает upsert с той же SQL-семантикой, что и метод пула, но на хендле транзакции — для потоков, где upsert должен быть атомарен с соседними стейтментами.
let mut tx = pool.begin().await?; sqlx::query("UPDATE users SET username = NULL WHERE ...").execute(&mut *tx).await?; let user = UserTransactionRepo::new(&mut tx).upsert(dto).await?; tx.commit().await?;
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级