Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Полное руководство по всем атрибутам, поддерживаемым 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() вместо DELETE
  • hard_delete() — Окончательно удаляет запись
  • restore() — Устанавливает deleted_at = NULL
  • find_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) — Найти сущность по ID
  • update(id, dto) — Обновить сущность
  • delete(id) — Удалить сущность (учитывает soft_delete)
  • list(limit, offset) — Список сущностей

Особенности:

  • Автоматический откат при ошибке или панике
  • Типобезопасный паттерн builder
  • Полная поддержка CRUD в транзакции

Атрибуты уровня поля

Применяются к отдельным полям.

#[id]

Отмечает поле первичного ключа.

Поведение:

  • Автоматически генерирует UUID (v7 по умолчанию, настраивается атрибутом uuid)
  • Всегда включается в Response DTO
  • Исключается из 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_TYPE enum’а в {Entity}::MIGRATION_TYPES — выполняйте их до MIGRATION_UP
  • Указанное имя сверяется с константой PG_TYPE enum’а на этапе компиляции; несовпадение ломает сборку
  • Опциональный флаг 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]
Использовать случайный UUIDuuid = "v4" на сущности
Использовать упорядоченный по времени UUIDuuid = "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?;