Relations는 Entity 간의 연관 관계를 정의하여 데이터베이스의 참조 무결성을 보장하고 타입 안전한 조인 쿼리를 작성할 수 있게 합니다.
Relation 타입 개요
Sonamu는 4가지 Relation 타입을 지원합니다:
BelongsToOne N:1 관계 - 다수가 하나를 참조 예: Post → User (여러 게시글이 한 사용자에게 속함)
OneToOne 1:1 관계 - 서로 하나씩만 참조 예: User ↔ Employee (사용자와 직원 정보가 1:1 매칭)
HasMany 1:N 관계 - 하나가 여럿을 소유 예: User → Posts (한 사용자가 여러 게시글 소유)
ManyToMany N:M 관계 - 다대다 관계 예: Post ↔ Tag (게시글과 태그의 다대다 관계)
BelongsToOne
N:1 관계 - 현재 Entity가 다른 Entity에 속하는 관계입니다.
기본 사용법
{
"id" : "Post" ,
"props" : [
{
"type" : "relation" ,
"name" : "user" ,
"with" : "User" ,
"relationType" : "BelongsToOne" ,
"desc" : "작성자"
}
]
}
생성되는 컬럼 : user_id (integer, not null)
데이터베이스 구조 :
옵션 타입 설명 기본값 nullableboolean NULL 허용 여부 falseuseConstraintboolean Foreign Key 제약 조건 사용 trueonUpdateRelationOn 참조 레코드 수정 시 동작 RESTRICTonDeleteRelationOn 참조 레코드 삭제 시 동작 RESTRICTcustomJoinClausestring 커스텀 JOIN 조건 -
RelationOn 옵션
값 설명 사용 예시 CASCADE부모 변경 시 자식도 같이 변경/삭제 사용자 삭제 시 게시글도 삭제 SET NULL부모 삭제 시 자식의 FK를 NULL로 설정 부서 삭제 시 직원의 부서를 NULL로 RESTRICT자식이 있으면 부모 변경/삭제 불가 게시글이 있으면 사용자 삭제 불가 NO ACTIONRESTRICT와 유사, 체크 시점만 다름 - SET DEFAULT부모 삭제 시 기본값으로 설정 -
예제: nullable과 CASCADE
{
"type" : "relation" ,
"name" : "department" ,
"with" : "Department" ,
"relationType" : "BelongsToOne" ,
"nullable" : true ,
"onDelete" : "SET NULL" ,
"desc" : "부서"
}
동작 :
department_id가 NULL 허용
부서가 삭제되면 직원의 department_id가 NULL로 설정됨
TypeScript 사용
// Subset에 relation 포함
const posts = await this . puri ()
. select < PostSubsetA >( "A" )
. many ();
// posts[0].user.username 접근 가능
OneToOne
1:1 관계 - 두 Entity가 서로 하나씩만 참조하는 관계입니다.
기본 사용법
OneToOne은 두 가지 방식으로 정의할 수 있습니다:
hasJoinColumn: true
hasJoinColumn: false
현재 Entity에 FK 컬럼이 생성됩니다. {
"id" : "Employee" ,
"props" : [
{
"type" : "relation" ,
"name" : "user" ,
"with" : "User" ,
"relationType" : "OneToOne" ,
"hasJoinColumn" : true ,
"onDelete" : "CASCADE" ,
"desc" : "사용자 계정"
}
]
}
생성되는 컬럼 : user_id (integer, unique, not null)데이터베이스 구조 :users: id, username
employees: id, user_id (FK, UNIQUE), employee_number
상대 Entity에 FK 컬럼이 있어야 합니다. {
"id" : "User" ,
"props" : [
{
"type" : "relation" ,
"name" : "employee" ,
"with" : "Employee" ,
"relationType" : "OneToOne" ,
"hasJoinColumn" : false ,
"nullable" : true ,
"desc" : "직원 정보"
}
]
}
컬럼 생성 없음 - Employee 테이블의 user_id를 사용
옵션 타입 설명 기본값 hasJoinColumnboolean FK 컬럼 생성 여부 필수 nullableboolean NULL 허용 (hasJoinColumn: true 시) falseuseConstraintboolean FK 제약 (hasJoinColumn: true 시) trueonUpdateRelationOn 수정 시 동작 (hasJoinColumn: true 시) RESTRICTonDeleteRelationOn 삭제 시 동작 (hasJoinColumn: true 시) RESTRICTcustomJoinClausestring 커스텀 JOIN 조건 -
예제: 양방향 OneToOne
user.entity.json
employee.entity.json
{
"type" : "relation" ,
"name" : "employee" ,
"with" : "Employee" ,
"relationType" : "OneToOne" ,
"hasJoinColumn" : false ,
"nullable" : true
}
관계 설명 :
User는 Employee를 선택적으로 가질 수 있음 (nullable)
Employee는 반드시 User를 가져야 함 (not null)
User가 삭제되면 Employee도 함께 삭제됨 (CASCADE)
HasMany
1:N 관계 - 하나의 Entity가 여러 개의 다른 Entity를 소유하는 관계입니다.
기본 사용법
{
"id" : "User" ,
"props" : [
{
"type" : "relation" ,
"name" : "posts" ,
"with" : "Post" ,
"relationType" : "HasMany" ,
"joinColumn" : "user_id" ,
"desc" : "작성한 게시글"
}
]
}
조건 :
Post Entity에 user_id 컬럼이 있어야 함
보통 Post에서 BelongsToOne으로 역방향 정의
옵션 타입 설명 기본값 joinColumnstring 상대 테이블의 FK 컬럼명 필수 fromColumnstring 현재 테이블의 참조 컬럼명 idnullableboolean 관계 자체의 nullable (옵션) false
예제: fromColumn 사용
{
"type" : "relation" ,
"name" : "childPosts" ,
"with" : "Post" ,
"relationType" : "HasMany" ,
"joinColumn" : "parent_post_id" ,
"fromColumn" : "id" ,
"desc" : "하위 게시글"
}
JOIN 쿼리 :
SELECT * FROM posts
WHERE posts . parent_post_id = { user . id }
TypeScript 사용
{
"subsets" : {
"A" : [
"id" ,
"username" ,
"posts.id" ,
"posts.title" ,
"posts.created_at"
]
}
}
HasMany는 DataLoader 패턴 으로 자동 최적화됩니다. N+1 쿼리 문제가 발생하지 않습니다.
ManyToMany
N:M 관계 - 다대다 관계를 중간 테이블(Join Table)을 통해 구현합니다.
기본 사용법
{
"id" : "Post" ,
"props" : [
{
"type" : "relation" ,
"name" : "tags" ,
"with" : "Tag" ,
"relationType" : "ManyToMany" ,
"joinTable" : "posts__tags" ,
"onUpdate" : "CASCADE" ,
"onDelete" : "CASCADE" ,
"desc" : "태그"
}
]
}
자동 생성되는 Join Table :
CREATE TABLE posts__tags (
id INTEGER PRIMARY KEY ,
post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE ,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE ,
UNIQUE (post_id, tag_id)
);
옵션 타입 설명 기본값 joinTablestring 중간 테이블명 ({table1}\_\_${table2}) 필수 onUpdateRelationOn 참조 레코드 수정 시 동작 필수 onDeleteRelationOn 참조 레코드 삭제 시 동작 필수 nullableboolean 관계 자체의 nullable (옵션) false
Join Table 네이밍 규칙 : 두 테이블명을 알파벳 순으로 정렬하여 __로 연결합니다.
올바른 예: posts__tags
잘못된 예: tags__posts (알파벳 순 아님)
양방향 정의
post.entity.json
tag.entity.json
{
"type" : "relation" ,
"name" : "tags" ,
"with" : "Tag" ,
"relationType" : "ManyToMany" ,
"joinTable" : "posts__tags" ,
"onUpdate" : "CASCADE" ,
"onDelete" : "CASCADE"
}
TypeScript 사용
{
"subsets" : {
"A" : [
"id" ,
"title" ,
"tags.id" ,
"tags.name"
]
}
}
Custom Join Clause
복잡한 JOIN 조건이 필요한 경우 SQL 표현식을 직접 작성할 수 있습니다.
{
"type" : "relation" ,
"name" : "latestPost" ,
"with" : "Post" ,
"relationType" : "OneToOne" ,
"hasJoinColumn" : false ,
"customJoinClause" : "users.id = posts.user_id AND posts.is_published = true" ,
"desc" : "최신 발행 게시글"
}
customJoinClause는 고급 기능입니다. 가능하면 표준 Relation을 사용하세요.
Relation 활용 패턴
1. Subset에서 Relation 필드 선택
{
"subsets" : {
"A" : [
"id" ,
"title" ,
"user.username" ,
"user.email" ,
"tags.name"
]
}
}
자동 생성되는 쿼리 :
user: LEFT JOIN
tags: DataLoader로 별도 쿼리
2. 중첩 Relation
{
"subsets" : {
"A" : [
"id" ,
"title" ,
"user.id" ,
"user.username" ,
"user.employee.department.name"
]
}
}
Sonamu가 자동으로 필요한 JOIN을 생성합니다:
FROM posts
LEFT JOIN users ON posts . user_id = users . id
LEFT JOIN employees ON users . id = employees . user_id
LEFT JOIN departments ON employees . department_id = departments . id
3. Relation 필터링
BelongsToOne 필터
HasMany 필터
const posts = await this . puri ()
. where ( "user.role" , "admin" )
. many ();
4. Relation 정렬
const posts = await this . puri ()
. orderBy ( "user.username" , "asc" )
. many ();
Relation 설계 가이드
BelongsToOne vs OneToOne
상황 권장 타입 이유 게시글 → 작성자 BelongsToOne여러 게시글이 한 사용자에 속함 사용자 ↔ 프로필 OneToOne1:1 매칭 관계 주문 → 고객 BelongsToOne여러 주문이 한 고객에 속함
CASCADE vs RESTRICT
상황 권장 동작 이유 사용자 삭제 → 게시글 CASCADE함께 삭제 카테고리 삭제 → 게시글 RESTRICT게시글이 있으면 삭제 불가 부서 삭제 → 직원 SET NULL직원은 유지, 부서만 NULL
nullable 설정
상황 nullable 이유 필수 관계 false항상 참조 필요 선택 관계 true없을 수도 있음 임시 상태 true나중에 설정
주의사항
순환 참조 방지 A → B → C → A 같은 순환 참조는 피하세요. 데이터 생성 시 문제가 발생할 수 있습니다.
JOIN 깊이 제한 너무 깊은 중첩 Relation (3단계 이상)은 성능 문제를 일으킬 수 있습니다. 필요한 경우 별도 쿼리로 분리하세요.
ManyToMany Join Table Join Table은 Sonamu가 자동으로 관리하므로 별도의 Entity를 만들 필요가 없습니다. 추가 컬럼이 필요한 경우에만 별도 Entity로 분리하세요.
다음 단계