> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Relations

> Entity 간 관계 정의하고 활용하기

Relations는 Entity 간의 연관 관계를 정의하여 데이터베이스의 참조 무결성을 보장하고 타입 안전한 조인 쿼리를 작성할 수 있게 합니다.

## Relation 타입 개요

Sonamu는 4가지 Relation 타입을 지원합니다:

<CardGroup cols={2}>
  <Card title="BelongsToOne" icon="arrow-right">
    N:1 관계 - 다수가 하나를 참조 예: Post → User (여러 게시글이 한 사용자에게 속함)
  </Card>

  <Card title="OneToOne" icon="arrows-left-right">
    1:1 관계 - 서로 하나씩만 참조 예: User ↔ Employee (사용자와 직원 정보가 1:1 매칭)
  </Card>

  <Card title="HasMany" icon="arrow-right-arrow-left">
    1:N 관계 - 하나가 여럿을 소유 예: User → Posts (한 사용자가 여러 게시글 소유)
  </Card>

  <Card title="ManyToMany" icon="arrows-split-up-and-left">
    N:M 관계 - 다대다 관계 예: Post ↔ Tag (게시글과 태그의 다대다 관계)
  </Card>
</CardGroup>

## BelongsToOne

**N:1 관계** - 현재 Entity가 다른 Entity에 속하는 관계입니다.

### 기본 사용법

```json title="post.entity.json" theme={null}
{
  "id": "Post",
  "props": [
    {
      "type": "relation",
      "name": "user",
      "with": "User",
      "relationType": "BelongsToOne",
      "desc": "작성자"
    }
  ]
}
```

**생성되는 컬럼**: `user_id` (integer, not null)

**데이터베이스 구조**:

<FileTree>
  <FileTree.Folder name="데이터베이스" defaultOpen>
    <FileTree.Folder name="users" defaultOpen>
      <FileTree.File name="id (PK)" />

      <FileTree.File name="username" />
    </FileTree.Folder>

    <FileTree.Folder name="posts" defaultOpen>
      <FileTree.File name="id (PK)" />

      <FileTree.File name="user_id (FK → users.id)" highlight />

      <FileTree.File name="title" />
    </FileTree.Folder>
  </FileTree.Folder>
</FileTree>

### 옵션

| 옵션                 | 타입         | 설명                   | 기본값        |
| ------------------ | ---------- | -------------------- | ---------- |
| `nullable`         | boolean    | NULL 허용 여부           | `false`    |
| `useConstraint`    | boolean    | Foreign Key 제약 조건 사용 | `true`     |
| `onUpdate`         | RelationOn | 참조 레코드 수정 시 동작       | `RESTRICT` |
| `onDelete`         | RelationOn | 참조 레코드 삭제 시 동작       | `RESTRICT` |
| `customJoinClause` | string     | 커스텀 JOIN 조건          | -          |

### RelationOn 옵션

| 값             | 설명                       | 사용 예시                 |
| ------------- | ------------------------ | --------------------- |
| `CASCADE`     | 부모 변경 시 자식도 같이 변경/삭제     | 사용자 삭제 시 게시글도 삭제      |
| `SET NULL`    | 부모 삭제 시 자식의 FK를 NULL로 설정 | 부서 삭제 시 직원의 부서를 NULL로 |
| `RESTRICT`    | 자식이 있으면 부모 변경/삭제 불가      | 게시글이 있으면 사용자 삭제 불가    |
| `NO ACTION`   | RESTRICT와 유사, 체크 시점만 다름  | -                     |
| `SET DEFAULT` | 부모 삭제 시 기본값으로 설정         | -                     |

### 예제: nullable과 CASCADE

```json theme={null}
{
  "type": "relation",
  "name": "department",
  "with": "Department",
  "relationType": "BelongsToOne",
  "nullable": true,
  "onDelete": "SET NULL",
  "desc": "부서"
}
```

**동작**:

* `department_id`가 `NULL` 허용
* 부서가 삭제되면 직원의 `department_id`가 `NULL`로 설정됨

### TypeScript 사용

<CodeGroup>
  ```typescript title="쿼리" theme={null}
  // Subset에 relation 포함
  const { qb } = this.getSubsetQueries("A");
  const result = await this.executeSubsetQuery({ subset: "A", qb, params });

  // result.rows[0].user.username 접근 가능
  ```

  ```typescript title="타입 (자동 생성)" theme={null}
  type PostSubsetA = {
    id: number;
    title: string;
    user: {
      id: number;
      username: string;
    };
  };
  ```
</CodeGroup>

## OneToOne

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

### 기본 사용법

OneToOne은 두 가지 방식으로 정의할 수 있습니다:

<Tabs>
  <Tab title="hasJoinColumn: true">
    현재 Entity에 FK 컬럼이 생성됩니다.

    ```json title="employee.entity.json" theme={null}
    {
      "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
    ```
  </Tab>

  <Tab title="hasJoinColumn: false">
    상대 Entity에 FK 컬럼이 있어야 합니다.

    ```json title="user.entity.json" theme={null}
    {
      "id": "User",
      "props": [
        {
          "type": "relation",
          "name": "employee",
          "with": "Employee",
          "relationType": "OneToOne",
          "hasJoinColumn": false,
          "nullable": true,
          "desc": "직원 정보"
        }
      ]
    }
    ```

    **컬럼 생성 없음** - Employee 테이블의 `user_id`를 사용
  </Tab>
</Tabs>

### 옵션

| 옵션                 | 타입         | 설명                              | 기본값        |
| ------------------ | ---------- | ------------------------------- | ---------- |
| `hasJoinColumn`    | boolean    | FK 컬럼 생성 여부                     | *필수*       |
| `nullable`         | boolean    | NULL 허용 (hasJoinColumn: true 시) | `false`    |
| `useConstraint`    | boolean    | FK 제약 (hasJoinColumn: true 시)   | `true`     |
| `onUpdate`         | RelationOn | 수정 시 동작 (hasJoinColumn: true 시) | `RESTRICT` |
| `onDelete`         | RelationOn | 삭제 시 동작 (hasJoinColumn: true 시) | `RESTRICT` |
| `customJoinClause` | string     | 커스텀 JOIN 조건                     | -          |

### 예제: 양방향 OneToOne

<CodeGroup>
  ```json title="user.entity.json" theme={null}
  {
    "type": "relation",
    "name": "employee",
    "with": "Employee",
    "relationType": "OneToOne",
    "hasJoinColumn": false,
    "nullable": true
  }
  ```

  ```json title="employee.entity.json" theme={null}
  {
    "type": "relation",
    "name": "user",
    "with": "User",
    "relationType": "OneToOne",
    "hasJoinColumn": true,
    "onDelete": "CASCADE"
  }
  ```
</CodeGroup>

**관계 설명**:

* User는 Employee를 선택적으로 가질 수 있음 (nullable)
* Employee는 반드시 User를 가져야 함 (not null)
* User가 삭제되면 Employee도 함께 삭제됨 (CASCADE)

## HasMany

**1:N 관계** - 하나의 Entity가 여러 개의 다른 Entity를 소유하는 관계입니다.

### 기본 사용법

```json title="user.entity.json" theme={null}
{
  "id": "User",
  "props": [
    {
      "type": "relation",
      "name": "posts",
      "with": "Post",
      "relationType": "HasMany",
      "joinColumn": "user_id",
      "desc": "작성한 게시글"
    }
  ]
}
```

**조건**:

* `Post` Entity에 `user_id` 컬럼이 있어야 함
* 보통 `Post`에서 `BelongsToOne`으로 역방향 정의

### 옵션

| 옵션           | 타입      | 설명                   | 기본값     |
| ------------ | ------- | -------------------- | ------- |
| `joinColumn` | string  | 상대 테이블의 FK 컬럼명       | *필수*    |
| `fromColumn` | string  | 현재 테이블의 참조 컬럼명       | `id`    |
| `nullable`   | boolean | 관계 자체의 nullable (옵션) | `false` |

### 예제: fromColumn 사용

```json theme={null}
{
  "type": "relation",
  "name": "childPosts",
  "with": "Post",
  "relationType": "HasMany",
  "joinColumn": "parent_post_id",
  "fromColumn": "id",
  "desc": "하위 게시글"
}
```

**JOIN 쿼리**:

```sql theme={null}
SELECT * FROM posts
WHERE posts.parent_post_id = {user.id}
```

### TypeScript 사용

<CodeGroup>
  ```typescript title="Subset 정의" theme={null}
  {
    "subsets": {
      "A": [
        "id",
        "username",
        "posts.id",
        "posts.title",
        "posts.created_at"
      ]
    }
  }
  ```

  ```typescript title="타입 (자동 생성)" theme={null}
  type UserSubsetA = {
    id: number;
    username: string;
    posts: Array<{
      id: number;
      title: string;
      created_at: Date;
    }>;
  };
  ```

  ```typescript title="쿼리" theme={null}
  const { qb } = this.getSubsetQueries("A");
  const result = await this.executeSubsetQuery({ subset: "A", qb, params });

  // result.rows[0].posts는 배열
  console.log(result.rows[0].posts.length);
  ```
</CodeGroup>

<Info>HasMany는 **DataLoader 패턴**으로 자동 최적화됩니다. N+1 쿼리 문제가 발생하지 않습니다.</Info>

## ManyToMany

**N:M 관계** - 다대다 관계를 중간 테이블(Join Table)을 통해 구현합니다.

### 기본 사용법

```json title="post.entity.json" theme={null}
{
  "id": "Post",
  "props": [
    {
      "type": "relation",
      "name": "tags",
      "with": "Tag",
      "relationType": "ManyToMany",
      "joinTable": "posts__tags",
      "onUpdate": "CASCADE",
      "onDelete": "CASCADE",
      "desc": "태그"
    }
  ]
}
```

**자동 생성되는 Join Table**:

```sql theme={null}
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)
);
```

### 옵션

| 옵션          | 타입         | 설명                                | 기본값     |
| ----------- | ---------- | --------------------------------- | ------- |
| `joinTable` | string     | 중간 테이블명 (`{table1}\_\_${table2}`) | *필수*    |
| `onUpdate`  | RelationOn | 참조 레코드 수정 시 동작                    | *필수*    |
| `onDelete`  | RelationOn | 참조 레코드 삭제 시 동작                    | *필수*    |
| `nullable`  | boolean    | 관계 자체의 nullable (옵션)              | `false` |

<Warning>
  **Join Table 네이밍 규칙**: 두 테이블명을 알파벳 순으로 정렬하여 `__`로 연결합니다. - 올바른 예:
  `posts__tags` - 잘못된 예: `tags__posts` (알파벳 순 아님)
</Warning>

### 양방향 정의

<CodeGroup>
  ```json title="post.entity.json" theme={null}
  {
    "type": "relation",
    "name": "tags",
    "with": "Tag",
    "relationType": "ManyToMany",
    "joinTable": "posts__tags",
    "onUpdate": "CASCADE",
    "onDelete": "CASCADE"
  }
  ```

  ```json title="tag.entity.json" theme={null}
  {
    "type": "relation",
    "name": "posts",
    "with": "Post",
    "relationType": "ManyToMany",
    "joinTable": "posts__tags",
    "onUpdate": "CASCADE",
    "onDelete": "CASCADE"
  }
  ```
</CodeGroup>

### TypeScript 사용

<CodeGroup>
  ```typescript title="Subset 정의" theme={null}
  {
    "subsets": {
      "A": [
        "id",
        "title",
        "tags.id",
        "tags.name"
      ]
    }
  }
  ```

  ```typescript title="타입 (자동 생성)" theme={null}
  type PostSubsetA = {
    id: number;
    title: string;
    tags: Array<{
      id: number;
      name: string;
    }>;
  };
  ```

  ```typescript title="쿼리" theme={null}
  const { qb } = this.getSubsetQueries("A");
  const result = await this.executeSubsetQuery({ subset: "A", qb, params });

  // result.rows[0].tags는 배열
  result.rows[0].tags.forEach((tag) => {
    console.log(tag.name);
  });
  ```
</CodeGroup>

## Custom Join Clause

복잡한 JOIN 조건이 필요한 경우 SQL 표현식을 직접 작성할 수 있습니다.

```json theme={null}
{
  "type": "relation",
  "name": "latestPost",
  "with": "Post",
  "relationType": "OneToOne",
  "hasJoinColumn": false,
  "customJoinClause": "users.id = posts.user_id AND posts.is_published = true",
  "desc": "최신 발행 게시글"
}
```

<Warning>`customJoinClause`는 고급 기능입니다. 가능하면 표준 Relation을 사용하세요.</Warning>

## Relation 활용 패턴

### 1. Subset에서 Relation 필드 선택

```json theme={null}
{
  "subsets": {
    "A": ["id", "title", "user.username", "user.email", "tags.name"]
  }
}
```

**자동 생성되는 쿼리**:

* `user`: LEFT JOIN
* `tags`: DataLoader로 별도 쿼리

### 2. 중첩 Relation

```json theme={null}
{
  "subsets": {
    "A": ["id", "title", "user.id", "user.username", "user.employee.department.name"]
  }
}
```

Sonamu가 자동으로 필요한 JOIN을 생성합니다:

```sql theme={null}
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 필터링

<CodeGroup>
  ```typescript title="BelongsToOne 필터" theme={null}
  const { qb } = this.getSubsetQueries("A");
  qb.where("user.role", "admin");
  const result = await this.executeSubsetQuery({ subset: "A", qb, params });
  ```

  ```typescript title="HasMany 필터" theme={null}
  // Subset에 포함된 HasMany는 필터링 불가
  // 대신 별도 쿼리 사용
  const user = await UserModel.findById(userId);
  const activePosts = await PostModel.getPuri("r").from("posts")
    .where("user_id", userId)
    .where("status", "active");
  ```
</CodeGroup>

### 4. Relation 정렬

```typescript theme={null}
const { qb } = this.getSubsetQueries("A");
qb.orderBy("user.username", "asc");
const result = await this.executeSubsetQuery({ subset: "A", qb, params });
```

## Relation 설계 가이드

### BelongsToOne vs OneToOne

| 상황        | 권장 타입          | 이유                |
| --------- | -------------- | ----------------- |
| 게시글 → 작성자 | `BelongsToOne` | 여러 게시글이 한 사용자에 속함 |
| 사용자 ↔ 프로필 | `OneToOne`     | 1:1 매칭 관계         |
| 주문 → 고객   | `BelongsToOne` | 여러 주문이 한 고객에 속함   |

### CASCADE vs RESTRICT

| 상황            | 권장 동작      | 이유               |
| ------------- | ---------- | ---------------- |
| 사용자 삭제 → 게시글  | `CASCADE`  | 함께 삭제            |
| 카테고리 삭제 → 게시글 | `RESTRICT` | 게시글이 있으면 삭제 불가   |
| 부서 삭제 → 직원    | `SET NULL` | 직원은 유지, 부서만 NULL |

### nullable 설정

| 상황    | nullable | 이유       |
| ----- | -------- | -------- |
| 필수 관계 | `false`  | 항상 참조 필요 |
| 선택 관계 | `true`   | 없을 수도 있음 |
| 임시 상태 | `true`   | 나중에 설정   |

## 주의사항

<Warning>
  **순환 참조 방지**

  A → B → C → A 같은 순환 참조는 피하세요. 데이터 생성 시 문제가 발생할 수 있습니다.
</Warning>

<Warning>
  **JOIN 깊이 제한**

  너무 깊은 중첩 Relation (3단계 이상)은 성능 문제를 일으킬 수 있습니다. 필요한 경우 별도 쿼리로 분리하세요.
</Warning>

<Tip>
  **ManyToMany Join Table**

  Join Table은 Sonamu가 자동으로 관리하므로 별도의 Entity를 만들 필요가 없습니다. 추가 컬럼이 필요한 경우에만 별도 Entity로 분리하세요.
</Tip>

## 다음 단계

<CardGroup cols={2}>
  <Card title="Enums" icon="hashtag" href="/ko/core-concepts/entity/enums">
    Enum 타입 정의하고 활용하기
  </Card>

  <Card title="Subset" icon="layer-group" href="/ko/core-concepts/model/using-subsets">
    Subset을 활용한 타입 안전한 쿼리
  </Card>

  <Card title="Puri Query Builder" icon="database" href="/ko/database/puri/basic-queries">
    Relation을 활용한 쿼리 작성하기
  </Card>

  <Card title="Performance" icon="gauge-high" href="/ko/database/performance/query-optimization">
    Relation 쿼리 최적화하기
  </Card>
</CardGroup>
