메인 콘텐츠로 건너뛰기
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에 속하는 관계입니다.

기본 사용법

post.entity.json
{
  "id": "Post",
  "props": [
    {
      "type": "relation",
      "name": "user",
      "with": "User",
      "relationType": "BelongsToOne",
      "desc": "작성자"
    }
  ]
}
생성되는 컬럼: user_id (integer, not null) 데이터베이스 구조:

옵션

옵션타입설명기본값
nullablebooleanNULL 허용 여부false
useConstraintbooleanForeign Key 제약 조건 사용true
onUpdateRelationOn참조 레코드 수정 시 동작RESTRICT
onDeleteRelationOn참조 레코드 삭제 시 동작RESTRICT
customJoinClausestring커스텀 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_idNULL 허용
  • 부서가 삭제되면 직원의 department_idNULL로 설정됨

TypeScript 사용

// Subset에 relation 포함
const posts = await this.puri()
  .select<PostSubsetA>("A")
  .many();

// posts[0].user.username 접근 가능

OneToOne

1:1 관계 - 두 Entity가 서로 하나씩만 참조하는 관계입니다.

기본 사용법

OneToOne은 두 가지 방식으로 정의할 수 있습니다:
현재 Entity에 FK 컬럼이 생성됩니다.
employee.entity.json
{
  "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

옵션

옵션타입설명기본값
hasJoinColumnbooleanFK 컬럼 생성 여부필수
nullablebooleanNULL 허용 (hasJoinColumn: true 시)false
useConstraintbooleanFK 제약 (hasJoinColumn: true 시)true
onUpdateRelationOn수정 시 동작 (hasJoinColumn: true 시)RESTRICT
onDeleteRelationOn삭제 시 동작 (hasJoinColumn: true 시)RESTRICT
customJoinClausestring커스텀 JOIN 조건-

예제: 양방향 OneToOne

{
  "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를 소유하는 관계입니다.

기본 사용법

user.entity.json
{
  "id": "User",
  "props": [
    {
      "type": "relation",
      "name": "posts",
      "with": "Post",
      "relationType": "HasMany",
      "joinColumn": "user_id",
      "desc": "작성한 게시글"
    }
  ]
}
조건:
  • Post Entity에 user_id 컬럼이 있어야 함
  • 보통 Post에서 BelongsToOne으로 역방향 정의

옵션

옵션타입설명기본값
joinColumnstring상대 테이블의 FK 컬럼명필수
fromColumnstring현재 테이블의 참조 컬럼명id
nullableboolean관계 자체의 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)을 통해 구현합니다.

기본 사용법

post.entity.json
{
  "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 (알파벳 순 아님)

양방향 정의

{
  "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 필터링

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 TableJoin Table은 Sonamu가 자동으로 관리하므로 별도의 Entity를 만들 필요가 없습니다. 추가 컬럼이 필요한 경우에만 별도 Entity로 분리하세요.

다음 단계