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 { qb } = this . getSubsetQueries ( "A" );
const result = await this . executeSubsetQuery ({ subset: "A" , qb , params });
// result.rows[0].user.username ์ ๊ทผ ๊ฐ๋ฅ
type PostSubsetA = {
id : number ;
title : string ;
user : {
id : number ;
username : string ;
};
};
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
}
{
"type" : "relation" ,
"name" : "user" ,
"with" : "User" ,
"relationType" : "OneToOne" ,
"hasJoinColumn" : true ,
"onDelete" : "CASCADE"
}
๊ด๊ณ ์ค๋ช
:
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 ์ฌ์ฉ
Subset ์ ์
ํ์
(์๋ ์์ฑ)
์ฟผ๋ฆฌ
{
"subsets" : {
"A" : [
"id" ,
"username" ,
"posts.id" ,
"posts.title" ,
"posts.created_at"
]
}
}
type UserSubsetA = {
id : number ;
username : string ;
posts : Array <{
id : number ;
title : string ;
created_at : Date ;
}>;
};
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 );
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"
}
{
"type" : "relation" ,
"name" : "posts" ,
"with" : "Post" ,
"relationType" : "ManyToMany" ,
"joinTable" : "posts__tags" ,
"onUpdate" : "CASCADE" ,
"onDelete" : "CASCADE"
}
TypeScript ์ฌ์ฉ
Subset ์ ์
ํ์
(์๋ ์์ฑ)
์ฟผ๋ฆฌ
{
"subsets" : {
"A" : [
"id" ,
"title" ,
"tags.id" ,
"tags.name"
]
}
}
type PostSubsetA = {
id : number ;
title : string ;
tags : Array <{
id : number ;
name : string ;
}>;
};
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 );
});
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 { qb } = this . getSubsetQueries ( "A" );
qb . where ( "user.role" , "admin" );
const result = await this . executeSubsetQuery ({ subset: "A" , qb , params });
// Subset์ ํฌํจ๋ HasMany๋ ํํฐ๋ง ๋ถ๊ฐ
// ๋์ ๋ณ๋ ์ฟผ๋ฆฌ ์ฌ์ฉ
const user = await UserModel . findById ( userId );
const activePosts = await PostModel . getPuri ( "r" ). from ( "posts" )
. where ( "user_id" , userId )
. where ( "status" , "active" );
4. Relation ์ ๋ ฌ
const { qb } = this . getSubsetQueries ( "A" );
qb . orderBy ( "user.username" , "asc" );
const result = await this . executeSubsetQuery ({ subset: "A" , qb , params });
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๋ก ๋ถ๋ฆฌํ์ธ์.
๋ค์ ๋จ๊ณ
Enums Enum ํ์
์ ์ํ๊ณ ํ์ฉํ๊ธฐ
Subset Subset์ ํ์ฉํ ํ์
์์ ํ ์ฟผ๋ฆฌ
Puri Query Builder Relation์ ํ์ฉํ ์ฟผ๋ฆฌ ์์ฑํ๊ธฐ
Performance Relation ์ฟผ๋ฆฌ ์ต์ ํํ๊ธฐ