Полное руководство по всем атрибутам, поддерживаемым 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 | Генерировать адаптер для транзакций |
table (обязательный)
Имя таблицы базы данных.
#[entity(table = "users")] // → FROM users
#[entity(table = "user_profiles")] // → FROM user_profiles
schema (опциональный)
Схема базы данных. По умолчанию: "public".
#[entity(table = "users")] // → FROM public.users
#[entity(table = "users", schema = "core")] // → FROM core.users
#[entity(table = "users", schema = "auth")] // → FROM auth.users
sql (опциональный)
Уровень генерации 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")] // Нет слоя БД вообще
dialect (опциональный)
Диалект БД для генерации SQL. По умолчанию: "postgres".
| Диалект | Алиасы | Тип клиента | Статус |
|---|---|---|---|
"postgres" | "pg", "postgresql" | sqlx::PgPool | Стабильно |
"clickhouse" | "ch" | clickhouse::Client | Планируется |
"mongodb" | "mongo" | mongodb::Client | Планируется |
uuid (опциональный)
Версия UUID для автоматически генерируемых первичных ключей. По умолчанию: "v7".
| Версия | Метод | Свойства |
|---|---|---|
"v7" | Uuid::now_v7() | Упорядочен по времени, сортируемый (рекомендуется) |
"v4" | Uuid::new_v4() | Случайный, широко совместимый |
#[entity(table = "users", uuid = "v7")] // Упорядоченный по времени (по умолчанию)
#[entity(table = "sessions", uuid = "v4")] // Случайный UUID
Почему UUID v7?
- Упорядочен по времени: естественная сортировка по времени создания
- Лучшая производительность индексов БД
- Не требует координации (в отличие от sequences)
- Глобально уникален в распределённых системах
soft_delete (опциональный)
Включает мягкое удаление для пометки записей как удалённых вместо их удаления.
#[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()вместо DELETEhard_delete()— Окончательно удаляет записьrestore()— Устанавливаетdeleted_at = NULLfind_by_id()/list()— Автоматически фильтрует удалённые записиfind_by_id_with_deleted()/list_with_deleted()— Включает удалённые записи
returning (опциональный)
Управляет тем, какие данные возвращаются после 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")] // Пользовательские колонки
events(outbox) (опциональный)
Надёжная доставка событий через транзакционный 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(...) (опциональный)
Генерирует метод репозитория 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 для каждой возвращённой строки.
api(guard = "...") (опциональный)
Принудительная аутентификация в генерируемых хендлерах. 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 = [...] гвард не получают.
error (опциональный)
Пользовательский тип ошибки для репозитория. По умолчанию: 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;
// ...
// }
events (опциональный)
Генерирует enum событий жизненного цикла. Подробнее см. [[События|События]].
#[entity(table = "orders", events)]
Генерируется:
pub enum OrderEvent {
Created(Order),
Updated { id: Uuid, changes: UpdateOrderRequest },
Deleted(Uuid),
}
hooks (опциональный)
Генерирует трейт хуков жизненного цикла. Подробнее см. [[Хуки|Хуки]].
#[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>;
}
commands (опциональный)
Включает паттерн CQRS-команд. Подробнее см. [[Команды|Команды]].
#[entity(table = "users", commands)]
#[command(Register)]
#[command(Deactivate, requires_id)]
transactions (опциональный)
Генерирует адаптер репозитория для типобезопасных транзакций с несколькими сущностями.
#[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)— Найти сущность по IDupdate(id, dto)— Обновить сущностьdelete(id)— Удалить сущность (учитываетsoft_delete)list(limit, offset)— Список сущностей
Особенности:
- Автоматический откат при ошибке или панике
- Типобезопасный паттерн builder
- Полная поддержка CRUD в транзакции
Атрибуты уровня поля
Применяются к отдельным полям.
#[id]
Отмечает поле первичного ключа.
Поведение:
- Автоматически генерирует UUID (v7 по умолчанию, настраивается атрибутом
uuid) - Всегда включается в
ResponseDTO - Исключается из
CreateRequestиUpdateRequest
#[id]
pub id: Uuid,
#[auto]
Отмечает автоматически генерируемые поля (timestamps, sequences).
Поведение:
- Получает
Default::default()вFrom<CreateRequest> - Исключается из
CreateRequestиUpdateRequest - Может включаться в
Responseс#[field(response)]
#[auto]
#[field(response)]
pub created_at: DateTime<Utc>,
#[field(...)]
Управляет включением в DTO. Комбинируйте несколько опций:
#[field(create)] // Только в CreateRequest
#[field(update)] // Только в UpdateRequest
#[field(response)] // Только в Response
#[field(create, response)] // В Create и Response
#[field(create, update, response)] // Во всех трёх
#[field(skip)] // Исключено из всех DTO
create
Включает поле в CreateRequest DTO.
#[field(create)]
pub email: String,
// Генерируется:
pub struct CreateUserRequest {
pub email: String,
}
update
Включает поле в UpdateRequest DTO.
Важно: Необязательные поля автоматически оборачиваются в Option<T> для частичных обновлений.
#[field(update)]
pub name: String, // Не Option
// Генерируется:
pub struct UpdateUserRequest {
pub name: Option<String>, // Обёрнуто автоматически
}
response
Включает поле в Response DTO.
#[field(response)]
pub email: String,
// Генерируется:
pub struct UserResponse {
pub id: Uuid, // Всегда включено (имеет #[id])
pub email: String, // Включено
}
skip
Исключает поле из всех DTO. Используйте для конфиденциальных данных.
#[field(skip)]
pub password_hash: String,
Важно: skip перекрывает все другие опции поля. Поле будет существовать только в:
- Оригинальной структуре сущности
- Структуре
Row(для чтения из БД) - Структуре
Insertable(для записи в БД)
#[column(pg_enum = "...")]
Подключает 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
#[owner]
Скоупинг строк по владельцу. Помечает колонку с 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] / #[filter(...)]
Генерирует поля фильтрации запросов. Подробнее см. [[Фильтрация|Фильтрация]].
#[filter] // Точное совпадение: WHERE field = $n
#[filter(eq)] // То же самое
#[filter(like)] // Совпадение по шаблону: WHERE field ILIKE $n
#[filter(range)] // Диапазон: WHERE field >= $n AND field <= $m
#[belongs_to(Entity)]
Связь по внешнему ключу. Подробнее см. [[Связи|Связи]].
#[belongs_to(User)]
pub user_id: Uuid,
Генерируется: метод find_user() в репозитории.
#[has_many(Entity)]
Связь один-ко-многим (на уровне сущности). Подробнее см. [[Связи|Связи]].
#[has_many(Post)]
pub struct User { /* ... */ }
Генерируется: метод find_posts() в репозитории.
#[projection(Name: fields)]
Генерирует структуру частичного представления (на уровне сущности).
#[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)] |
Update DTO: семантика PATCH
Генерируемые обновления — настоящие частичные патчи: SET-клауза строится в рантайме из реально присутствующих полей, пропущенные поля не трогаются. Nullable-колонки используют двойной Option (None = не менять, Some(None) = записать NULL, Some(Some(v)) = записать v) через entity_core::serde_helpers::double_option.
// {} → 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(...)
Помимо простого флага, 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?; }
#[version]
Оптимистичная блокировка. Помечает целочисленную колонку (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?;
typed_constraints (опциональный)
Макрос знает каждый 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?,
}
#[embed(prefix = "...", fields(...))]
Разворачивает 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 (бэкенд валидации)
Включает 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(...) (опциональный, вместе с typed_constraints)
Объявляет 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"),
)]
Транзакционный upsert
При включённых 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?;