-
-
Notifications
You must be signed in to change notification settings - Fork 0
github-actions[bot] edited this page Jul 26, 2026
·
4 revisions
#[belongs_to]와 #[has_many]를 사용하여 엔티티 간 관계를 정의합니다. 관계는 리포지토리에서 타입 안전 탐색 메서드를 생성합니다.
// 부모 엔티티 #[derive(Entity)] #[entity(table = "users")] #[has_many(Post)] pub struct User { #[id] pub id: Uuid, #[field(create, update, response)] pub name: String, } // 자식 엔티티 #[derive(Entity)] #[entity(table = "posts")] pub struct Post { #[id] pub id: Uuid, #[field(create, response)] #[belongs_to(User)] pub user_id: Uuid, #[field(create, update, response)] pub title: String, }
#[async_trait] impl UserRepository for PgPool { // ... 표준 CRUD 메서드 /// 이 사용자에게 속한 모든 게시물 찾기. async fn find_posts(&self, user_id: Uuid) -> Result<Vec<Post>, Self::Error> { let rows: Vec<PostRow> = sqlx::query_as( "SELECT * FROM posts WHERE user_id = 1ドル ORDER BY created_at DESC" ) .bind(&user_id) .fetch_all(self) .await?; Ok(rows.into_iter().map(Post::from).collect()) } }
#[async_trait] impl PostRepository for PgPool { // ... 표준 CRUD 메서드 /// 이 게시물이 속한 사용자 찾기. async fn find_user(&self, id: Uuid) -> Result<Option<User>, Self::Error> { // 먼저 게시물을 가져와서 user_id 찾기 let post = self.find_by_id(id).await?; if let Some(post) = post { let row: Option<UserRow> = sqlx::query_as( "SELECT * FROM users WHERE id = 1ドル" ) .bind(&post.user_id) .fetch_optional(self) .await?; Ok(row.map(User::from)) } else { Ok(None) } } }
자식 엔티티가 외래 키를 통해 부모를 참조합니다.
#[derive(Entity)] #[entity(table = "comments")] pub struct Comment { #[id] pub id: Uuid, #[field(create, response)] #[belongs_to(Post)] pub post_id: Uuid, #[field(create, response)] #[belongs_to(User)] pub author_id: Uuid, #[field(create, response)] pub content: String, }
생성되는 메서드:
-
find_post(comment_id)→Option<Post> -
find_author(comment_id)→Option<User>(참고: 메서드 이름은_id없이 필드 이름에서 파생)
부모 엔티티가 여러 자식을 가집니다.
#[derive(Entity)] #[entity(table = "users")] #[has_many(Post)] #[has_many(Comment)] pub struct User { #[id] pub id: Uuid, // ... }
생성되는 메서드:
-
find_posts(user_id)→Vec<Post> -
find_comments(user_id)→Vec<Comment>
// 사용자와 게시물 가져오기 async fn get_user_with_posts( pool: &PgPool, user_id: Uuid, ) -> Result<Option<(User, Vec<Post>)>, sqlx::Error> { let user = pool.find_by_id(user_id).await?; if let Some(user) = user { let posts = pool.find_posts(user_id).await?; Ok(Some((user, posts))) } else { Ok(None) } } // 게시물과 작성자 가져오기 async fn get_post_with_author( pool: &PgPool, post_id: Uuid, ) -> Result<Option<(Post, User)>, sqlx::Error> { let post = pool.find_by_id(post_id).await?; if let Some(post) = post { let user = pool.find_user(post.id).await?; if let Some(user) = user { return Ok(Some((post, user))); } } Ok(None) }
#[derive(Serialize)] pub struct PostWithAuthor { #[serde(flatten)] pub post: PostResponse, pub author: UserResponse, } async fn get_posts_with_authors( pool: &PgPool, limit: i64, ) -> Result<Vec<PostWithAuthor>, sqlx::Error> { let posts = pool.list(limit, 0).await?; let mut results = Vec::with_capacity(posts.len()); for post in posts { if let Some(user) = pool.find_user(post.id).await? { results.push(PostWithAuthor { post: PostResponse::from(&post), author: UserResponse::from(&user), }); } } Ok(results) }
use axum::{extract::Path, Json}; #[derive(Serialize)] pub struct UserProfile { pub user: UserResponse, pub posts: Vec<PostResponse>, pub post_count: usize, } async fn get_user_profile( Path(user_id): Path<Uuid>, pool: Extension<PgPool>, ) -> Result<Json<UserProfile>, AppError> { let user = pool.find_by_id(user_id).await? .ok_or(AppError::NotFound)?; let posts = pool.find_posts(user_id).await?; Ok(Json(UserProfile { user: UserResponse::from(&user), post_count: posts.len(), posts: posts.into_iter().map(PostResponse::from).collect(), })) }
엔티티는 여러 관계를 가질 수 있습니다:
#[derive(Entity)] #[entity(table = "organizations")] #[has_many(User)] #[has_many(Project)] #[has_many(Team)] pub struct Organization { #[id] pub id: Uuid, #[field(create, update, response)] pub name: String, } #[derive(Entity)] #[entity(table = "projects")] pub struct Project { #[id] pub id: Uuid, #[field(create, response)] #[belongs_to(Organization)] pub organization_id: Uuid, #[field(create, response)] #[belongs_to(User)] pub owner_id: Uuid, #[field(create, update, response)] pub name: String, }
Organization에 대해 생성됨:
find_users(org_id)find_projects(org_id)find_teams(org_id)
Project에 대해 생성됨:
find_organization(project_id)find_owner(project_id)
즉시 로딩이 있는 복잡한 쿼리의 경우 커스텀 SQL을 사용하세요:
#[derive(Entity)] #[entity(table = "posts", sql = "trait")] pub struct Post { /* ... */ } pub struct PostWithRelations { pub post: Post, pub author: User, pub comments: Vec<Comment>, } pub trait PostRepositoryExt { async fn find_with_relations(&self, id: Uuid) -> Result<Option<PostWithRelations>, sqlx::Error>; async fn list_with_authors(&self, limit: i64) -> Result<Vec<(Post, User)>, sqlx::Error>; } #[async_trait] impl PostRepositoryExt for PgPool { async fn find_with_relations(&self, id: Uuid) -> Result<Option<PostWithRelations>, sqlx::Error> { // JOIN이 있는 단일 쿼리 let row = sqlx::query_as::<_, (PostRow, UserRow)>( r#" SELECT p.*, u.* FROM posts p JOIN users u ON u.id = p.user_id WHERE p.id = 1ドル "# ) .bind(&id) .fetch_optional(self) .await?; if let Some((post_row, user_row)) = row { let comments: Vec<CommentRow> = sqlx::query_as( "SELECT * FROM comments WHERE post_id = 1ドル ORDER BY created_at" ) .bind(&id) .fetch_all(self) .await?; Ok(Some(PostWithRelations { post: Post::from(post_row), author: User::from(user_row), comments: comments.into_iter().map(Comment::from).collect(), })) } else { Ok(None) } } async fn list_with_authors(&self, limit: i64) -> Result<Vec<(Post, User)>, sqlx::Error> { let rows = sqlx::query_as::<_, (PostRow, UserRow)>( r#" SELECT p.*, u.* FROM posts p JOIN users u ON u.id = p.user_id ORDER BY p.created_at DESC LIMIT 1ドル "# ) .bind(limit) .fetch_all(self) .await?; Ok(rows.into_iter() .map(|(p, u)| (Post::from(p), User::from(u))) .collect()) } }
관계를 쿼리 필터링과 결합하세요:
#[derive(Entity)] #[entity(table = "posts")] pub struct Post { #[id] pub id: Uuid, #[field(create, response)] #[belongs_to(User)] #[filter] // user_id로 필터링 활성화 pub user_id: Uuid, #[field(create, update, response)] #[filter(like)] pub title: String, #[field(response)] #[auto] #[filter(range)] pub created_at: DateTime<Utc>, }
사용법:
// 제목 필터로 특정 사용자의 게시물 가져오기 let query = PostQuery { user_id: Some(user_id), title: Some("rust".into()), limit: Some(20), ..Default::default() }; let posts = pool.query(query).await?;
- N+1 쿼리 방지 — 여러 관련 엔티티를 가져올 때 즉시 로딩용 JOIN 사용
-
페이지네이션 사용 — 항상
has_many결과 제한 - 데이터 접근 패턴 고려 — 외래 키 컬럼에 인덱스 추가
- 적절한 경우 캐시 — 자주 접근하는 관련 데이터 캐시
- 프로젝션 사용 — 관련 엔티티에 필요한 필드만 가져오기
through로 정션 테이블을 선언하면 리포지토리는 JOIN 기반 조회와 연결 관리를 얻습니다. migrations는 MIGRATION_JUNCTIONS(두 외래 키에 대한 복합 기본 키, 양쪽 ON DELETE CASCADE)를 생성합니다.
#[derive(Entity)] #[entity(table = "teams", migrations)] #[has_many(User, through = "team_members")] pub struct Team { /* ... */ } for ddl in Team::MIGRATION_JUNCTIONS { sqlx::query(ddl).execute(&pool).await?; } pool.add_user(team_id, user_id).await?; let members: Vec<User> = pool.find_users(team_id).await?; let linked = pool.has_user(team_id, user_id).await?; let removed = pool.remove_user(team_id, user_id).await?;
생성 메서드: find_users(INNER JOIN), add_user(멱등, ON CONFLICT DO NOTHING), remove_user(연결이 없으면 false), has_user(SELECT EXISTS).
🇬🇧 English | 🇷🇺 Русский | 🇰🇷 한국어 | 🇪🇸 Español | 🇨🇳 中文
Getting Started
Features
Advanced
Начало работы
Возможности
Продвинутое
시작하기
기능
고급
Comenzando
Características
Avanzado
入门
功能
高级