-
-
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 )] 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 커맨드 패턴 활성화 |
데이터베이스 테이블 이름입니다.
#[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을 사용하는 이유:
- 시간순 정렬: 생성 시간별 자연스러운 정렬
- 더 나은 데이터베이스 인덱스 성능
- 조정이 필요 없음 (시퀀스와 달리)
- 분산 시스템에서 전역 고유
레코드를 삭제하는 대신 삭제로 표시하는 소프트 삭제를 활성화합니다.
#[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()— 삭제된 레코드 포함
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만으로는 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;
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 알림을 발행합니다.
생성된 핸들러에 인증을 강제합니다. 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 = [...]에 나열된 커맨드는 가드를 받지 않습니다.
리포지토리용 커스텀 에러 타입입니다. 기본값: 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; // ... // }
생명주기 이벤트 열거형을 생성합니다. 자세한 내용은 이벤트를 참조하세요.
#[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)]
개별 필드에 적용됩니다.
기본 키 필드를 표시합니다.
동작:
- UUID 자동 생성 (기본 v7,
uuid속성으로 설정 가능) -
ResponseDTO에 항상 포함 -
CreateRequest와UpdateRequest에서 제외
#[id] pub id: Uuid,
자동 생성 필드를 표시합니다 (timestamps, sequences).
동작:
-
From<CreateRequest>에서Default::default()획득 -
CreateRequest와UpdateRequest에서 제외 -
#[field(response)]로Response에 포함 가능
#[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구조체 (데이터베이스 쓰기용)
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한다면 생략하세요
행 단위 소유권 스코핑. 소유 주체의 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] // 정확히 일치: 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() 메서드.
부분 뷰 구조체를 생성합니다 (엔티티 레벨).
#[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)] |
생성된 업데이트는 진정한 부분 패치입니다. 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는 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. 일반·스코프·트랜잭션 업데이트 모두에 적용됩니다.
#[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?;
매크로는 자신이 만든 모든 제약을 알고 있습니다. 이 플래그를 켜면 생성된 쓰기 메서드가 위반된 제약 이름(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?, }
값 객체를 접두사가 붙은 평면 스칼라 컬럼으로 펼칩니다. 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, }
생성된 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)?;
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级