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
)]
pub struct User { /* ... */ }

빠른 참조

속성필수기본값설명
table데이터베이스 테이블 이름
schema아니오"public"데이터베이스 스키마
sql아니오"full"SQL 생성 레벨
dialect아니오"postgres"데이터베이스 방언
uuid아니오"v7"ID 생성용 UUID 버전
soft_delete아니오false소프트 삭제 활성화
returning아니오"full"RETURNING 절 모드
upsert(...)아니오INSERT ... ON CONFLICT 기반 upsert 메서드 생성
api(guard = "...")아니오생성된 핸들러에 적용되는 FromRequestParts 가드
error아니오sqlx::Error커스텀 에러 타입
events아니오false생명주기 이벤트 생성
hooks아니오false생명주기 훅 트레이트 생성
commands아니오falseCQRS 커맨드 패턴 활성화

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 TraitPgPool 구현사용 사례
"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을 사용하는 이유:

  • 시간순 정렬: 생성 시간별 자연스러운 정렬
  • 더 나은 데이터베이스 인덱스 성능
  • 조정이 필요 없음 (시퀀스와 달리)
  • 분산 시스템에서 전역 고유

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() — DELETE 대신 deleted_at = NOW() 설정
  • 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 *DB 생성 값 포함 모든 필드 가져오기 (기본값)
"id"RETURNING id삽입 확인, 미리 빌드된 엔티티 반환
"none"(RETURNING 없음)Fire-and-forget, 가장 빠른 옵션
"col1, col2"RETURNING col1, col2특정 컬럼 반환
#[entity(table = "logs", returning = "none")]              // 가장 빠름
#[entity(table = "users", returning = "full")]             // DB 생성 값 가져오기
#[entity(table = "events", returning = "id, created_at")]  // 커스텀 컬럼

events(outbox) (선택)

트랜잭셔널 아웃박스를 통한 내구성 있는 이벤트 전달. events만으로는 enum만 생성되며, streams의 NOTIFY는 fire-and-forget입니다. events(outbox)는 생성된 모든 쓰기가 직렬화된 이벤트를 같은 트랜잭션 안에서 entity_outbox 테이블에 삽입하게 하고, OutboxDrainer 런타임(entity-core, outbox 피처)이 FOR UPDATE SKIP LOCKED, 지수 백오프, 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 기반의 upsert 리포지토리 메서드를 생성합니다.

#[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 = "..."는 OpenAPI 문서화만 수행하지만, guard는 axum FromRequestParts extractor를 모든 생성된 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 (선택)

생명주기 이벤트 열거형을 생성합니다. 자세한 내용은 [[이벤트|이벤트]]를 참조하세요.

#[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)]

필드 레벨 속성

개별 필드에 적용됩니다.

#[id]

기본 키 필드를 표시합니다.

동작:

  • UUID 자동 생성 (기본 v7, uuid 속성으로 설정 가능)
  • Response DTO에 항상 포함
  • CreateRequestUpdateRequest에서 제외
#[id]
pub id: Uuid,

#[auto]

자동 생성 필드를 표시합니다 (timestamps, sequences).

동작:

  • From<CreateRequest>에서 Default::default() 획득
  • CreateRequestUpdateRequest에서 제외
  • #[field(response)]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 = "...")]

ValueObject Postgres enum을 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로 폴백)
  • enum의 멱등 PG_CREATE_TYPE DDL을 {Entity}::MIGRATION_TYPES에 등록 — MIGRATION_UP 전에 실행하세요
  • 선언한 이름은 컴파일 타임에 enum의 PG_TYPE 상수와 대조 검증되며, 불일치 시 빌드가 실패합니다
  • ValueObject의 선택적 sqlx 플래그는 sqlx::Type / Encode / Decode 구현을 생성합니다; 이미 sqlx::Type을 derive한다면 생략하세요

#[owner]

행 단위 소유권 스코핑. 소유 주체의 id를 담는 컬럼을 표시하면, 리포지토리에 스코프 메서드가 생성됩니다 — 다른 소유자의 행 존재 여부를 노출하지 않으며 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)]
읽기 전용 (DB 관리)#[field(response)]
쓰기 전용 (반환 없음)#[field(create)]
커스텀 SQL 쿼리sql = "trait"
DTO만, DB 없음sql = "none"
레코드 소프트 삭제엔티티에 soft_delete
커스텀 에러 타입엔티티에 error = "MyError"
정확한 값으로 필터필드에 #[filter]
패턴으로 필터필드에 #[filter(like)]
범위로 필터필드에 #[filter(range)]
엔티티 변경 추적엔티티에 events
생명주기에서 코드 실행엔티티에 hooks
도메인 커맨드 사용엔티티에 commands + #[command(...)]
관계 정의#[belongs_to(Entity)] 또는 #[has_many(Entity)]
부분 엔티티 뷰#[projection(Name: fields)]

Update DTO: PATCH 시맨틱

생성된 업데이트는 진정한 부분 패치입니다. SET 절은 실제로 존재하는 필드로 런타임에 구성되며 생략된 필드는 변경되지 않습니다. Nullable 컬럼은 entity_core::serde_helpers::double_option을 통한 이중 Option을 사용합니다(None = 유지, Some(None) = NULL 설정, Some(Some(v)) = v 설정).

// {}                   → 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. 일반·스코프·트랜잭션 업데이트 모두에 적용됩니다.

#[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 (선택)

매크로는 자신이 만든 모든 제약을 알고 있습니다. 이 플래그를 켜면 생성된 쓰기 메서드가 위반된 제약 이름(unique 컬럼, belongs_to 외래 키, 컬럼 check, unique_index 이름)을 해석해 원시 드라이버 오류 대신 entity_core::ConstraintError { kind, constraint, field }를 반환합니다. From<ConstraintError>를 구현한 커스텀 error 타입이 필요하며, 플래그가 없으면 동작은 그대로입니다.

#[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(...))]

값 객체를 접두사가 붙은 평면 스칼라 컬럼으로 펼칩니다. 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 피처 (검증 백엔드)

생성된 DTO에 validator의 유지보수되는 대안으로 garde::Validate를 활성화합니다. 필드의 #[validate(...)] 규칙(length, range, email, url, pattern)은 garde 문법으로 변환되고, 제약 없는 필드는 garde(skip)을 받으며, Update DTO의 Option 래퍼는 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와 함께)

매크로가 유추할 수 없는 제약을 선언합니다 — 자연 키에 대한 외래 키, 커스텀 이름의 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

transactionsupsert(...)가 모두 켜져 있으면 {Entity}TransactionRepo 어댑터가 풀 메서드와 동일한 SQL 시맨틱의 upsert를 트랜잭션 핸들에서 제공합니다 — 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?;