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 | 아니오 | false | CQRS 커맨드 패턴 활성화 |
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을 사용하는 이유:
- 시간순 정렬: 생성 시간별 자연스러운 정렬
- 더 나은 데이터베이스 인덱스 성능
- 조정이 필요 없음 (시퀀스와 달리)
- 분산 시스템에서 전역 고유
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속성으로 설정 가능) ResponseDTO에 항상 포함CreateRequest와UpdateRequest에서 제외
#[id]
pub id: Uuid,
#[auto]
자동 생성 필드를 표시합니다 (timestamps, sequences).
동작:
From<CreateRequest>에서Default::default()획득CreateRequest와UpdateRequest에서 제외#[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_TYPEDDL을{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
transactions와 upsert(...)가 모두 켜져 있으면 {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?;