๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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_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์€ ๋‘ ๊ฐ€์ง€ ๋ฐฉ์‹์œผ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
ํ˜„์žฌ 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๋กœ ๋ถ„๋ฆฌํ•˜์„ธ์š”.

๋‹ค์Œ ๋‹จ๊ณ„