๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
08-TypeORM-Theory

1. Typeorm๊ณต๋ถ€์šฉ ํ”„๋กœ์ ํŠธ

Typeorm๊ณต๋ถ€์šฉ ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ญ์‹œ๋‹ค.

1
nest new typeorm
2
yarn add @nestjs/typeorm typeorm pg
docker-compose.yaml
1
# ์„œ๋น„์Šค์ •์˜
2
services:
3
postgres:
4
image: postgres:15
5
# ์‹คํ–‰์‹œ๋งˆ๋‹ค ์žฌ์‹œ์ž‘
6
restart: always
7
# ๋„์ปค์ปดํฌ์ฆˆ ํŒŒ์ผ์— ์กด์žฌํ•˜๋Š” ์œ„์น˜์— ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ hostOS์— ์ €์žฅ
8
volumes:
9
# ํ˜„์žฌ ๋„์ปค์ปดํฌ์ฆˆ ํŒŒ์ผ์ด ์กด์žฌํ•˜๋Š” ๊ฒฝ๋กœ : ์ด๋ฏธ์ง€์•ˆ์—์กด์žฌํ•˜๋Š” ๊ฒฝ๋กœ ๋งคํ•‘
10
- ./postgres-data:/var/lib/postgresql/data
11
ports:
12
# hostport:์ด๋ฏธ์ง€์˜ํฌํŠธ
13
# 5432ํฌํŠธ ์š”์ฒญ -> ์ด๋ฏธ์ง€์˜ ํฌํŠธ๋กœ ์š”์ณฅ
14
- '5808:5432'
15
environment:
16
POSTGRES_USER: postgres
17
POSTGRES_PASSWORD: postgres
18
POSTGRES_DB: typeormstudy
app.module.ts
1
import { Module } from '@nestjs/common'
2
import { TypeOrmModule } from '@nestjs/typeorm'
3
4
import { AppController } from './app.controller'
5
import { AppService } from './app.service'
6
7
@Module({
8
imports: [
9
TypeOrmModule.forRoot({
10
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…
11
type: 'postgres',
12
host: '127.0.0.1',
13
port: 5808,
14
username: 'postgres',
15
password: 'postgres',
16
database: 'typeormstudy',
17
// entitiesํด๋”์— ์ž‘์„ฑํ•œ PostsModel ๊ฐ€์ ธ์˜ค๊ธฐ
18
entities: [],
19
synchronize: true,
20
}),
21
],
22
controllers: [AppController],
23
providers: [AppService],
24
})
25
export class AppModule {}

2. Column Annotation๋“ค

src/entity/user.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

user.entity.ts
1
import {
2
Column,
3
CreateDateColumn,
4
Entity,
5
Generated,
6
PrimaryGeneratedColumn,
7
UpdateDateColumn,
8
VersionColumn,
9
} from 'typeorm'
10
11
@Entity()
12
export class UserModel {
13
/*** ID
14
* ์ž๋™์œผ๋กœ ID๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
15
*
16
* ๐Ÿ“Œ @PrimaryGeneratedColumn()
17
* Primary Column์€ ๋ชจ๋“  ํ…Œ์ด๋ธ”์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค
18
* ํ…Œ์ด๋ธ” ์•ˆ์—์„œ ๊ฐ๊ฐ์˜ Row๋ฅผ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋Š” ์ปฌ๋Ÿผ์ด๋‹ค.
19
* @PrimaryColumn()
20
*
21
* ๐Ÿ“Œ @PrimaryGeneratedColumn('uuid')
22
* PrimaryGeneratedColumn => ์ˆœ์„œ๋Œ€๋กœ ์œ„๋กœ ์˜ฌ๋ผ๊ฐ„๋‹ค.
23
* 1, 2, 3, 4, 5 -> 999999
24
*
25
* UUID : ์ ˆ๋Œ€๋กœ ๊ฒน์น˜์ง€ ์•Š๋Š” ๊ณ ์œ ํ•œ ๊ฐ’์„ ๋งŒ๋“ค์–ด์คŒ
26
* ea36ed96-8d1c-44d9-9fbe-4ec6960e95a8
27
*/
28
@PrimaryGeneratedColumn()
29
id: number
30
31
@Column()
32
title: string
33
34
/** ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์ผ์ž
35
* ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜๋Š” ๋‚ ์งœ์™€ ์‹œ๊ฐ„์ด ์ž๋™์œผ๋กœ ์ฐํžŒ๋‹ค.
36
*/
37
@CreateDateColumn()
38
createdAt: Date
39
40
/** ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์ผ์ž
41
* ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๋‚ ์งœ์™€ ์‹œ๊ฐ„์ด ์ž๋™์œผ๋กœ ์ฐํžŒ๋‹ค.
42
*/
43
@UpdateDateColumn()
44
updateAt: Date
45
46
/** ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋  ๋–„๋งˆ๋‹ค 1์”ฉ ์˜ฌ๋ผ๊ฐ„๋‹ค
47
* ์ฒ˜์Œ ์ƒ์„ฑ๋˜๋ฉด ๊ฐ’์€ 1์ด๋‹ค.
48
* save() ํ•จ์ˆ˜๊ฐ€ ๋ช‡ ๋ฒˆ ๋ถˆ๋ ธ๋Š”์ง€ ๊ธฐ์–ตํ•œ๋‹ค.
49
*/
50
@VersionColumn()
51
version: number
52
53
/**
54
* ๐Ÿ“Œ @Generated('increment')
55
* additionalId: number
56
* PrimaryColumn์€ ์•„๋‹Œ๋ฐ, ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•  ๋–„๋งˆ๋‹ค, 1์”ฉ ์˜ฌ๋ผ๊ฐ€๋Š” ์ปฌ๋Ÿผ
57
*
58
* ๐Ÿ“Œ Generated('uuid')
59
* additionalId: string
60
* ๋Š” ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ,
61
* PrimaryColumn์€ ์•„๋‹Œ๋ฐ, ๋ฐ์ดํ„ฐ ์ƒ์„ฑํ•  ๋–„๋งˆ๋‹ค, ๊ณ ์œ ๊ฐ’์„ ๊ฐ€์ง€๋Š” ์ปฌ๋Ÿผ
62
*/
63
@Column()
64
@Generated('uuid')
65
additionalId: string
66
}
app.module.ts
1
import { Module } from '@nestjs/common'
2
import { TypeOrmModule } from '@nestjs/typeorm'
3
4
import { AppController } from './app.controller'
5
import { AppService } from './app.service'
6
import { UserModel } from './entity/user.entity'
7
8
@Module({
9
imports: [
10
TypeOrmModule.forFeature([UserModel]),
11
TypeOrmModule.forRoot({
12
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…
13
type: 'postgres',
14
host: '127.0.0.1',
15
port: 5808,
16
username: 'postgres',
17
password: 'postgres',
18
database: 'typeormstudy',
19
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
20
entities: [UserModel],
21
synchronize: true,
22
}),
23
],
24
controllers: [AppController],
25
providers: [AppService],
26
})
27
export class AppModule {}
app.controller.ts
1
import { Controller, Get, Param, Patch, Post } from '@nestjs/common'
2
import { InjectRepository } from '@nestjs/typeorm'
3
import { UserModel } from './entity/user.entity'
4
import { Repository } from 'typeorm'
5
6
@Controller()
7
export class AppController {
8
constructor(
9
@InjectRepository(UserModel)
10
private readonly userRepository: Repository<UserModel>
11
) {}
12
13
@Post('users')
14
postUser() {
15
return this.userRepository.save({
16
title: 'test title',
17
})
18
}
19
20
@Get('users')
21
getUsers() {
22
return this.userRepository.find()
23
}
24
25
@Patch('users/:id')
26
async patchUser(@Param('id') id: string) {
27
const user = await this.userRepository.findOne({
28
where: { id: parseInt(id) },
29
})
30
31
return this.userRepository.save({
32
...user,
33
title: user.title + '0',
34
})
35
}
36
}

3. Column Property ์ •๋ฆฌ

์†์„ฑ(property)์„ค๋ช…
type : ColumnType์นผ๋Ÿผ ํƒ€์ž…. varchar, text, int, bool๋“ฑ ์นผ๋Ÿผ ํƒ€์ž…
name : string๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋  ์นผ๋Ÿผ ์ด๋ฆ„, ๊ธฐ๋ณธ๊ฐ’์€ ํ”„๋กœํผํ‹ฐ ์ด๋ฆ„์„ ๋”ฐ๋ฆ„
nullable : booleannull ๊ฐ’์ด ๊ฐ€๋Šฅํ•œ์ง€ ์—ฌ๋ถ€. ๊ธฐ๋ณธ๊ฐ’์€ false
update : boolean์—…๋ฐ์ดํŠธ ๊ฐ€๋Šฅ ์—ฌ๋ถ€. false์ผ ๊ฒฝ์šฐ ์ €์žฅ ํ›„ ์—…๋ฐ์ดํŠธ ๋ถˆ๊ฐ€. ๊ธฐ๋ณธ๊ฐ’ true
select : boolean์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ์ง€ ๊ฒฐ์ •. false์ผ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๊ฒŒ ๊ธฐ๋ณธ
default : string์นผ๋Ÿผ ๊ธฐ๋ณธ๊ฐ’
unique : booleanunique constraint ์ ์šฉ ์—ฌ๋ถ€. ๊ธฐ๋ณธ false
comment : string์นผ๋Ÿผ ์ฝ”๋ฉ˜ํŠธ. ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ง€์›๋˜์ง„ ์•Š์Œ
enum : string[]์นผ๋Ÿผ์— ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ ๊ฐ’์„ enum์œผ๋กœ ๋‚˜์—ด
Array : boolean์นผ๋Ÿผ array ํƒ€์ž…์œผ๋กœ ์ƒ์„ฑ (e.g. int[])
user.entity.ts
1
// user.entity.ts ์ƒ๋žต
2
@Column({
3
type: 'varchar',
4
name: 'title',
5
length: 300,
6
nullable: true,
7
update: true,
8
select: false,
9
default: 'default value',
10
unique: false,
11
})
12
title: string
app.controller.ts
1
// app.controller.ts ์ƒ๋žต
2
@Post('users')
3
postUser() {
4
return this.userRepository.save({
5
// title: 'test title',
6
})
7
}
8
9
@Get('users')
10
getUsers() {
11
return this.userRepository.find({
12
select: { id: true, title: true },
13
})
14
}

4. Enum Column

user.entity.ts
1
export enum Role {
2
USER = 'user',
3
ADMIN = 'admin',
4
}
5
6
@Entity()
7
export class UserModel {
8
// ์ƒ๋žต
9
title: string
10
11
@Column({
12
type: 'enum',
13
enum: Role,
14
default: Role.USER,
15
})
16
role: Role
17
18
// ์ƒ๋žต
19
}
app.controller.ts
1
@Post('users')
2
postUser() {
3
return this.userRepository.save({
4
// title: 'test title',
5
role: Role.ADMIN, // ๊ด€๋ฆฌ์ž ์—ญํ• ์„ ๋„ฃ๊ณ ์‹ถ์„ ๋–„
6
})
7
}
8
9
// app.controller.ts์˜ select์˜ต์…˜ ์ง€์šฐ๊ธฐ
10
@Get('users')
11
getUsers() {
12
return this.userRepository.find({})
13
}

5. Entity Embedding

src/entity/person.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

person.entity.ts
1
import { Column, Entity, PrimaryColumn } from 'typeorm'
2
3
export class Name {
4
@Column()
5
first: string
6
7
@Column()
8
last: string
9
}
10
11
@Entity()
12
export class StudentModel {
13
@PrimaryColumn()
14
id: number
15
16
@Column(() => Name)
17
name: Name
18
19
@Column()
20
class: string
21
}
22
23
@Entity()
24
export class TeacherModel {
25
@PrimaryColumn()
26
id: number
27
28
@Column(() => Name)
29
name: Name
30
31
@Column()
32
salary: number
33
}

app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
// ์ƒ๋žต
2
@Module({
3
imports: [
4
TypeOrmModule.forFeature([UserModel]),
5
TypeOrmModule.forRoot({
6
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…
7
type: 'postgres',
8
host: '127.0.0.1',
9
port: 5808,
10
username: 'postgres',
11
password: 'postgres',
12
database: 'typeormstudy',
13
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
14
entities: [UserModel, StudentModel, TeacherModel],
15
synchronize: true,
16
}),
17
],
18
controllers: [AppController],
19
providers: [AppService],
20
})
21
export class AppModule {}

DB์— ๋“ค์–ด๊ฐ„ ์ปฌ๋Ÿผ๋ช…์„ ํ™•์ธํ•ด๋ณด๋ฉด, nameFirst, nameLast๋กœ ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.


6. Entity Inheritance

src/entity/inheritance.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

inheritance.entity.ts
1
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
2
3
export class BaseModel {
4
@PrimaryGeneratedColumn()
5
id: number
6
7
@CreateDateColumn()
8
createdAt: Date
9
10
@UpdateDateColumn()
11
updateat: Date
12
}
13
14
@Entity()
15
export class BookModel extends BaseModel {
16
@Column()
17
name: string
18
}
19
20
@Entity()
21
export class CarModel extends BaseModel {
22
@Column()
23
brand: string
24
}

app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([UserModel]),
4
TypeOrmModule.forRoot({
5
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…
6
type: 'postgres',
7
host: '127.0.0.1',
8
port: 5808,
9
username: 'postgres',
10
password: 'postgres',
11
database: 'typeormstudy',
12
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
13
entities: [
14
UserModel,
15
StudentModel, //
16
TeacherModel,
17
BookModel,
18
CarModel,
19
],
20
synchronize: true,
21
}),
22
],
23
controllers: [AppController],
24
providers: [AppService],
25
})
26
export class AppModule {}

DB์— ๋“ค์–ด๊ฐ„ ์ปฌ๋Ÿผ๋ช…์„ ํ™•์ธํ•ด๋ณด๋ฉด, ์ƒ์†๋ฐ›์€ ์†์„ฑ์ด ๋“ค์–ด๊ฐ„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ‰์†Œ์—๋Š” ์œ„์™€ ๊ฐ™์€ ์ผ๋ฐ˜์ ์ธ ์ƒ์†์„ ์“ฐ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๋‹ค๋งŒ, ๊ตณ์ด ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

inheritance.entity.ts
1
import {
2
ChildEntity,
3
Column,
4
CreateDateColumn,
5
Entity,
6
PrimaryGeneratedColumn,
7
TableInheritance,
8
UpdateDateColumn,
9
} from 'typeorm'
10
11
export class BaseModel {
12
@PrimaryGeneratedColumn()
13
id: number
14
15
@CreateDateColumn()
16
createdAt: Date
17
18
@UpdateDateColumn()
19
updateat: Date
20
}
21
22
@Entity()
23
export class BookModel extends BaseModel {
24
@Column()
25
name: string
26
}
27
28
@Entity()
29
export class CarModel extends BaseModel {
30
@Column()
31
brand: string
32
}
33
34
@Entity()
35
@TableInheritance({
36
column: {
37
name: 'type',
38
type: 'varchar',
39
},
40
})
41
export class SingleBaseModel {
42
@PrimaryGeneratedColumn()
43
id: number
44
45
@CreateDateColumn()
46
createdAt: Date
47
48
@UpdateDateColumn()
49
updateat: Date
50
}
51
52
@ChildEntity()
53
export class ComputerModel extends SingleBaseModel {
54
@Column()
55
brand: string
56
}
57
58
@ChildEntity()
59
export class AirplaneModel extends SingleBaseModel {
60
@Column()
61
country: string
62
}

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([UserModel]),
4
TypeOrmModule.forRoot({
5
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํƒ€์ž…
6
type: 'postgres',
7
host: '127.0.0.1',
8
port: 5808,
9
username: 'postgres',
10
password: 'postgres',
11
database: 'typeormstudy',
12
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
13
entities: [
14
UserModel,
15
StudentModel, //
16
TeacherModel,
17
BookModel,
18
CarModel,
19
SingleBaseModel,
20
ComputerModel,
21
AirplaneModel,
22
],
23
synchronize: true,
24
}),
25
],
26
controllers: [AppController],
27
providers: [AppService],
28
})
29
export class AppModule {}

DB์— ๋“ค์–ด๊ฐ„ ์ปฌ๋Ÿผ๋ช…์„ ํ™•์ธํ•ด๋ณด๋ฉด, ์ž์‹ ์ปฌ๋Ÿผ์ด ๋“ค์–ด๊ฐ„ single_base_model ํ•˜๋‚˜๋งŒ ์ƒ์„ฑ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.


7. Relationship ์ด๋ก 

TypeORM์ด ์ œ๊ณตํ•˜๋Š” 4๊ฐ€์ง€ Relationship

๊ด€๊ณ„์„ค๋ช…
@OneToOneA ํ…Œ์ด๋ธ”์˜ Row ํ•˜๋‚˜์™€ B ํ…Œ์ด๋ธ”์˜ Row ํ•˜๋‚˜๊ฐ€ ์—ฐ๊ฒฐ๋˜๋Š” ๊ด€๊ณ„
@ManyToOneA ํ…Œ์ด๋ธ”์˜ Row ์—ฌ๋Ÿฌ ๊ฐœ์™€ B ํ…Œ์ด๋ธ”์˜ Row ํ•˜๋‚˜๊ฐ€ ์—ฐ๊ฒฐ๋˜๋Š” ๊ด€๊ณ„
@OneToManyA ํ…Œ์ด๋ธ”์˜ Row ํ•˜๋‚˜์™€ B ํ…Œ์ด๋ธ”์˜ Row ์—ฌ๋Ÿฌ๊ฐœ๊ฐ€ ์—ฐ๊ฒฐ๋˜๋Š” ๊ด€๊ณ„
@ManyToManyA ํ…Œ์ด๋ธ”์˜ Row ์—ฌ๋Ÿฌ ๊ฐœ์™€ B ํ…Œ์ด๋ธ”์˜ Row ์—ฌ๋Ÿฌ ๊ฐœ๊ฐ€ ์—ฐ๊ฒฐ๋˜๋Š” ๊ด€๊ณ„

7.1 Relationship Annotation ์ ์šฉ

  • ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž…๋ ฅํ•œ๋‹ค. (Class Transformer Type๊ณผ ๊ฐ™์€ ๊ฐœ๋…)
  • ๋‘๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ž…๋ ฅํ•œ ํด๋ž˜์Šค์˜ ์นผ๋Ÿผ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž…๋ ฅํ•œ๋‹ค. ์ด ์นผ๋Ÿผ์€ ์„œ๋กœ ๊ด€๋ จ์ง€์„ ํ”„๋กœํผํ‹ฐ์—ฌ์•ผํ•œ๋‹ค
  • e.g. ManyToOne ๊ด€๊ณ„์ด๋‹ˆ photo ํ…Œ์ด๋ธ”์— user_id ์นผ๋Ÿผ์ด ์ƒ์„ฑ๋˜๋ฉฐ user ํ…Œ์ด๋ธ”๊ณผ ๊ด€๊ณ„๊ฐ€ ํ˜•์„ฑ๋œ๋‹ค
  • ํŠน์ • photo์™€ ๊ด€๋ จ์žˆ๋Š” user๋Š” photo.user๋กœ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๊ณ  user์™€ ๊ด€๋ จ์žˆ๋Š” photo๋“ค์€ user.photos๋กœ ๋ถˆ๋Ÿฌ ์˜ฌ ์ˆ˜ ์žˆ๋‹ค
1
@Entity()
2
export class Photo {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
url: string
8
9
// ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ : ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
10
// ๋‘๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ : ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ž…๋ ฅํ•œ ํด๋ž˜์Šค์˜ ์นผ๋Ÿผ์ค‘ ํ•˜๋‚˜๋ฅผ ์ž…๋ ฅ
11
@ManyToOne(() => User, (user) => user.photos)
12
user: User
13
}
1
@Entity()
2
export class User {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
name: string
8
9
@OneToMany(() => Photo, (photo) => photo.user)
10
photos: Photo[]
11
}

7.2 ManyToOne & OneToMany ๊ด€๊ณ„

  • photo ํ…Œ์ด๋ธ”์—๋Š” user_id ์นผ๋Ÿผ์ด ์ž๋™์œผ๋กœ ์ƒ๊ธด๋‹ค. ๋„ค์ด๋ฐ ํŒจํ„ด์€ {์ƒ๋Œ€ ํ…Œ์ด๋ธ” ์ด๋ฆ„}_id
  • user_id๋Š” user ํ…Œ์ด๋ธ”์˜ id ์นผ๋Ÿผ์„ Foreign Key๋กœ ๋ ˆํผ๋Ÿฐ์Šคํ•œ๋‹ค
  • user ํ…Œ์ด๋ธ”์€ ์ถ”๊ฐ€๋กœ ์นผ๋Ÿผ์ด ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค.
    • ์›๋ž˜ ManyToOne ๋˜๋Š” OneToMany ๊ด€๊ณ„๋Š” Foreign Key ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๋“ค๊ณ ์žˆ๋Š” ํ…Œ์ด๋ธ”์ด Many ์ž…์žฅ์ด๋‹ค
Photo ํ…Œ์ด๋ธ”
idintPRIMARY KEY AUTO_INCREMENT
urlvarchar
user_idvarcharFOREIGN KEY
User ํ…Œ์ด๋ธ”
idintPRIMARY KEY AUTO_INCREMENT
namevarchar

7.3 OneToOne ๊ด€๊ณ„

  • OneToOne Relationship๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Annotation์„ ์›ํ•˜๋Š” ํ”„๋กœํผํ‹ฐ์— ์ •์˜ํ•ด์ฃผ๋ฉด ๋œ๋‹ค
  • ManyToOne์€ ์ƒ๋Œ€์˜ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๊ฐ–๋Š” ํ…Œ์ด๋ธ”์ด ๋ช…ํ™•ํ•˜๋‹ค
  • OneToOne์€ ๋‘ ํ…Œ์ด๋ธ” ๋ˆ„๊ฐ€ ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๋“ค๊ณ  ์žˆ์–ด๋„ ์ƒ๊ด€์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์–ด๋–ค ํ…Œ์ด๋ธ”์ด ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๋“ค๊ณ  ์žˆ์„์ง€ ๋ช…์‹œํ•ด์•ผ ํ•œ๋‹ค
  • @JoinTable Annotation์„ ์‚ฌ์šฉํ•ด์„œ ์–ด๋–ค ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋ ˆํผ๋Ÿฐ์Šค ๋“ค๊ณ  ์žˆ์„์ง€ ์ •ํ•ด ์ค„ ์ˆ˜ ์žˆ๋‹ค
  • @JoinTable์€ ๊ผญ ํ•œ์ชฝ์—๋งŒ ์ ์šฉํ•ด์•ผํ•œ๋‹ค. ๋‘˜ ๋ชจ๋‘ ์ ์šฉํ•˜๋Š”๊ฑด ๊ฐ€๋Šฅํ•˜๊ณ  ์˜๋ฏธ๋„ ์—†๋‹ค
1
@Entity()
2
export class Profile {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
gender: string
8
9
@Column()
10
photos: string
11
12
@OneToOne(() => User, (user) => user.profile)
13
user: User
14
}
1
@Entity()
2
export class User {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
name: string
8
9
@OneToOne(() => Profile)
10
@JoinColumn()
11
profile: Profile
12
}

7.4 ManyToMany ๊ด€๊ณ„

  • ManyToMany Relationship๋„ OneToOne Relationship๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ @JoinTable Annotation์„ ํ•œ์ชฝ์— ์ ์šฉ ํ•ด์ค˜์•ผํ•œ๋‹ค
  • ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋ ๋•Œ @JoinTable์ด ์ ์šฉ๋œ ํ…Œ์ด๋ธ” ์ด๋ฆ„์ด ๋จผ์ € ์œ„์น˜ํ•˜๊ฒŒ๋œ๋‹ค.
  • ManyToMany Annotation์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์ด ์ƒ์„ฑ๋œ๋‹ค
  • ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์˜ ์นผ๋Ÿผ์€ ๊ฐ๊ฐ ๋ ˆํผ๋Ÿฐ์Šค ํ…Œ์ด๋ธ”์˜ {ํ…Œ์ด๋ธ” ์ด๋ฆ„}_id๋กœ ์ •์˜๋œ๋‹ค
1
@Entity()
2
export class Category {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
name: string
8
9
@ManyToMany(() => Question)
10
questions: Question[]
11
}
1
@Entity()
2
export class Question {
3
@PrimaryGeneratedColumn()
4
id: number
5
6
@Column()
7
title: string
8
9
@Column()
10
text: string
11
12
@ManyToMany(() => Category)
13
@JoinTable()
14
categories: Category[]
15
}

8. 1:1 ๊ด€๊ณ„ ์ž‘์—…

src/entity/profile.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

profile.entity.ts
1
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'
2
import { UserModel } from './user.entity'
3
4
@Entity()
5
export class ProfileModel {
6
@PrimaryGeneratedColumn()
7
id: number
8
9
// UserModel์— user์˜ profile ์ปฌ๋Ÿผ๊ณผ 1:1 ์—ฐ๊ฒฐ
10
@OneToOne(() => UserModel, (user) => user.profile)
11
// ์ƒ๋Œ€๋ฐฉ ํ…Œ์ด๋ธ”์˜ id๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ(๋งŒ์•ฝ ์ƒ๋Œ€๋ฐฉ์ด ๊ฐ–๊ณ ์žˆ์œผ๋ฉด ์ƒ๋Œ€๋ฐฉ์ด ์ด ํ…Œ์ด๋ธ” id๋ฅผ ๊ฐ€์ง)
12
@JoinColumn()
13
user: UserModel
14
15
@Column()
16
profileImg: string
17
}

์—ฐ๊ฒฐํ•  ๋ชจ๋ธ(user.entity.ts)์— 1:1๋กœ ์—ฐ๊ฒฐํ•  ๋ชจ๋ธ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

user.entity.ts
1
// ์ƒ๋žต
2
// ProfileModel์— profile์˜ user ์ปฌ๋Ÿผ๊ณผ 1:1 ์—ฐ๊ฒฐ
3
@OneToOne(() => ProfileModel, profile => profile.user)
4
profile: ProfileModel

app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ(ProfileModel)์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
@Module({
2
imports: [
3
// ProfileModel ์ถ”๊ฐ€
4
TypeOrmModule.forFeature([UserModel, ProfileModel]),
5
TypeOrmModule.forRoot({
6
// ์ƒ๋žต
7
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
8
entities: [
9
// ์ƒ๋žต
10
ProfileModel,
11
],
12
synchronize: true,
13
}),
14
],
15
controllers: [AppController],
16
providers: [AppService],
17
})

user ์—”ํ‹ฐํ‹ฐ์˜ title์„ ์ง€์šฐ๊ณ  ๋Œ€์‹ , email ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ๊ธฐ์กด ๋ฐ์ดํ„ฐ๊ฐ€ ์ €์žฅ๋˜์–ด์žˆ๋Š” ๊ณณ(postgres-data)์„ ์ง€์› ๋‹ค๊ฐ€ ๋‹ค์‹œ ์ƒ์„ฑํ•œ๋‹ค.(docker-compose up)

user.entity.ts
1
// user.entity.ts
2
// title: string์€ ์ฃผ์„์ฒ˜๋ฆฌ
3
@Column()
4
email: string

app ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

app.controller.ts
1
// ์ƒ๋žต
2
@Controller()
3
export class AppController {
4
constructor(
5
@InjectRepository(UserModel)
6
private readonly userRepository: Repository<UserModel>,
7
@InjectRepository(ProfileModel)
8
private readonly profileRepository: Repository<ProfileModel>
9
) {}
10
11
@Post('users')
12
postUser() {
13
return this.userRepository.save({})
14
}
15
16
@Get('users')
17
getUsers() {
18
return this.userRepository.find({
19
// ์—ฐ๋™๋œ ๋ฐ์ดํ„ฐ ์ปฌ๋Ÿผ(profile)๋„ ๊ฐ€์ ธ์˜ค๊ธฐ
20
relations: {
21
profile: true,
22
},
23
})
24
}
25
26
@Patch('users/:id')
27
async patchUser(@Param('id') id: string) {
28
const user = await this.userRepository.findOne({
29
where: { id: parseInt(id) },
30
})
31
32
return this.userRepository.save({
33
...user,
34
})
35
}
36
37
@Post('user/profile')
38
async createUserAndProfile() {
39
const user = await this.userRepository.save({
40
email: 'asd@gmail.ai',
41
})
42
const profile = await this.profileRepository.save({
43
profileImg: 'asdf.png',
44
user,
45
})
46
return user
47
}
48
}

9. M:1 & 1:M ๊ด€๊ณ„ ๊ตฌํ˜„

src/entity/post.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

post.entity.ts
1
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
2
import { UserModel } from './user.entity'
3
4
@Entity()
5
export class PostModel {
6
@PrimaryGeneratedColumn()
7
id: number
8
9
// 1:M ๊ด€๊ณ„์ด๋‹ˆ posts๋กœ ๋ณต์ˆ˜ํ˜•์œผ๋กœ ์„ ์–ธ
10
@ManyToOne(() => UserModel, (user) => user.posts)
11
author: UserModel
12
13
@Column()
14
title: string
15
}

ํ…Œ์ด๋ธ”์ด ์–ด๋А ์ง€์ ์„ ๋ฐ”๋ผ๋ณด๋ƒ์— ๋”ฐ๋ผ M:1์ด๋‚˜ 1:M์ด ๋œ๋‹ค.

user.entity.ts
1
// user.entity.ts ์ƒ๋žต
2
@OneToMany(() => PostModel, post => post.author)
3
posts: PostModel[]

app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ(PostModel)์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([
4
UserModel,
5
ProfileModel,
6
PostModel, // ์ถ”๊ฐ€
7
]),
8
TypeOrmModule.forRoot({
9
// ์ƒ๋žต
10
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
11
entities: [
12
// ์ƒ๋žต
13
PostModel,
14
],
15
synchronize: true,
16
}),
17
],
18
controllers: [AppController],
19
providers: [AppService],
20
})

ํ™•์ธ์šฉ api๋ฅผ app.controller.ts์— ๋งŒ๋“ค๊ณ  ํฌ์ŠคํŠธ๋งจ์—์„œ ํ™•์ธํ•ด๋ณด์„ธ์š”.

app.controller.ts
1
// ์ƒ๋žต
2
3
@Controller()
4
export class AppController {
5
constructor(
6
@InjectRepository(UserModel)
7
private readonly userRepository: Repository<UserModel>,
8
@InjectRepository(ProfileModel)
9
private readonly profileRepository: Repository<ProfileModel>,
10
@InjectRepository(PostModel)
11
private readonly postRepository: Repository<PostModel>
12
) {}
13
14
// ์ƒ๋žต
15
@Get('users')
16
getUsers() {
17
return this.userRepository.find({
18
relations: {
19
profile: true,
20
posts: true, // ์ถ”๊ฐ€
21
},
22
})
23
}
24
25
@Post('user/post')
26
async createUserAndPost() {
27
const user = await this.userRepository.save({
28
email: 'postuser@gmail.ai',
29
})
30
await this.postRepository.save({
31
author: user,
32
title: 'post 1',
33
})
34
await this.postRepository.save({
35
author: user,
36
title: 'post 2',
37
})
38
39
return user
40
}
41
}

10. M : M ๊ด€๊ณ„ ๊ตฌํ˜„

src/entity/tag.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

tag.entity.ts
1
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm'
2
import { PostModel } from './post.entity'
3
4
@Entity()
5
export class TagModel {
6
@PrimaryGeneratedColumn()
7
id: number
8
9
// M:M ์—ฐ๊ฒฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‘˜ ๋‹ค ๋ณต์ˆ˜๋กœ ์„ ์–ธ
10
@ManyToMany(() => PostModel, (post) => post.tags)
11
posts: PostModel[]
12
13
@Column()
14
name: string
15
}

post.entity.ts์— tags๋ฅผ ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋กœ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

post.entity.ts
1
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
2
import { UserModel } from './user.entity'
3
import { TagModel } from './tag.entity'
4
5
@Entity()
6
export class PostModel {
7
@PrimaryGeneratedColumn()
8
id: number
9
10
// 1:M ๊ด€๊ณ„์ด๋‹ˆ posts๋กœ ๋ณต์ˆ˜ํ˜•์œผ๋กœ ์„ ์–ธ
11
@ManyToOne(() => UserModel, (user) => user.posts)
12
author: UserModel
13
14
// M:M ์—ฐ๊ฒฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋‘˜ ๋‹ค ๋ณต์ˆ˜๋กœ ์„ ์–ธ
15
@ManyToMany(() => TagModel, (tag) => tag.posts)
16
// M:M ์—ฐ๊ฒฐ์—์„œ JoinTable์€ ๋‘˜ ์ค‘ ํ•˜๋‚˜ ์•„๋ฌด๊ตฐ๋ฐ ์„ ์–ธํ•ด์ฃผ๋ฉด ๋œ๋‹ค
17
@JoinTable()
18
tags: TagModel[]
19
20
@Column()
21
title: string
22
}

app.module.ts์— ์ƒ์„ฑํ•œ ๋ชจ๋“ˆ(TagModel)์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
// ์ƒ๋žต
2
@Module({
3
imports: [
4
TypeOrmModule.forFeature([
5
UserModel,
6
ProfileModel, //
7
PostModel,
8
TagModel,
9
]),
10
TypeOrmModule.forRoot({
11
// ์ƒ๋žต
12
// entitiesํด๋”์— ์ž‘์„ฑํ•œ Model ๊ฐ€์ ธ์˜ค๊ธฐ
13
entities: [
14
// ์ƒ๋žต
15
TagModel,
16
],
17
synchronize: true,
18
}),
19
],
20
controllers: [AppController],
21
providers: [AppService],
22
})
23
export class AppModule {}

app ์ปจํŠธ๋กค๋Ÿฌ์— tag ์š”์ฒญ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.controller.ts
1
// ์ƒ๋žต
2
@Controller()
3
export class AppController {
4
constructor(
5
@InjectRepository(UserModel)
6
private readonly userRepository: Repository<UserModel>,
7
@InjectRepository(ProfileModel)
8
private readonly profileRepository: Repository<ProfileModel>,
9
@InjectRepository(PostModel)
10
private readonly postRepository: Repository<PostModel>,
11
@InjectRepository(TagModel)
12
private readonly tagRepository: Repository<TagModel>
13
) {}
14
15
// ์ƒ๋žต
16
17
@Post('posts/tags')
18
async createPostsTags() {
19
const post1 = await this.postRepository.save({
20
title: 'NestJS ์ˆ˜์—…',
21
})
22
23
const post2 = await this.postRepository.save({
24
title: 'ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ˆ˜์—…',
25
})
26
27
const tag1 = await this.tagRepository.save({
28
name: 'Javascript',
29
posts: [post1, post2],
30
})
31
32
const tag2 = await this.tagRepository.save({
33
name: 'Typescript',
34
posts: [post1],
35
})
36
37
const post3 = await this.postRepository.save({
38
title: 'NextJS ์ˆ˜์—…',
39
tags: [tag1, tag2],
40
})
41
42
return true
43
}
44
45
@Get('posts')
46
getPosts() {
47
return this.postRepository.find({
48
relations: {
49
tags: true,
50
},
51
})
52
}
53
54
@Get('tags')
55
getTags() {
56
return this.tagRepository.find({
57
relations: {
58
posts: true,
59
},
60
})
61
}
62
}

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ ์š”์ฒญํ•ด ํ™•์ธํ•ด๋ณด์„ธ์š”.


11. Relation Options

postgres-data ํŒŒ์ผ์„ ์ง€์›Œ ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ๋‹ค์‹œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. user.entity.ts์—์„œ ๊ด€๊ณ„๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์— ๋Œ€ํ•ด ์•Œ์•„๋ด…์‹œ๋‹ค.

user.entity.ts
1
// ProfileModel์— profile์˜ user ์ปฌ๋Ÿผ๊ณผ 1:1 ์—ฐ๊ฒฐ
2
@OneToOne(() => ProfileModel, profile => profile.user, {
3
// find() ์‹คํ–‰ํ•  ๋•Œ๋งˆ๋‹ค ํ•ญ์ƒ ๊ฐ™์ด ๊ฐ€์ ธ์˜ฌ relation ์„ค์ •(๊ธฐ๋ณธ๊ฐ’ false)
4
eager: true,
5
// ์ €์žฅํ•  ๋–„, relation์„ ํ•œ ๋ฒˆ์— ๊ฐ™์ด ์ €์žฅ(๊ธฐ๋ณธ๊ฐ’ false)
6
cascade: true,
7
// null์ด ๊ฐ€๋Šฅํ•œ์ง€ ์—ฌ๋ถ€(๊ธฐ๋ณธ๊ฐ’ true)
8
nullable: true,
9
// ๊ด€๊ณ„๋ฅผ ์‚ญ์ œํ–ˆ์„ ๋–„, ์–ด๋–ป๊ฒŒ ์‚ญ์ œํ•  ๊ฒƒ์ธ์ง€
10
// - NO ACTION : ์•„๋ฌด๊ฒƒ๋„ ์•ˆํ•จ
11
// - CASCADE : ์ฐธ์กฐํ•˜๋Š” row๋„ ๊ฐ™์ด ์‚ญ์ œ
12
// - SET NULL : ์ฐธ์กฐํ•˜๋Š” row์—์„œ ์ฐธ์กฐ id๋ฅผ null๋กœ ๋ณ€๊ฒฝ
13
// - set default : ๊ธฐ๋ณธ ์„ธํŒ…์œผ๋กœ ์„ค์ •(ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธ ์„ธํŒ…)
14
// - RESTRICT : ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” row๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ์ฐธ์กฐ๋‹นํ•˜๋Š” row ์‚ญ์ œ ๋ถˆ๊ฐ€
15
onDelete: 'NO ACTION',
16
})
17
profile: ProfileModel

๊ด€๊ณ„ ์‚ญ์ œ ์˜ต์…˜ ํ…Œ์ŠคํŠธ์šฉ์„ app.controller.ts์— ๋„ฃ์–ด์„œ ํ™•์ธํ•ด๋ณธ๋‹ค

app.controller.ts
1
@Delete('user/profile/:id')
2
async deleteProfile(@Param('id') id: string) {
3
await this.profileRepository.delete(+id)
4
}

12. FindManyOptions ํŒŒ๋ผ๋ฏธํ„ฐ

app.controller.ts์—์„œ find()์— ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์— ๋Œ€ํ•ด ์•Œ์•„๋ด…์‹œ๋‹ค.

app.controller.ts
1
@Get('users')
2
getUsers() {
3
return this.userRepository.find({
4
// ์–ด๋–ค ์†์„ฑ์„ ์„ ํƒํ• ์ง€ (๊ธฐ๋ณธ์€ ๋ชจ๋“  ์†์„ฑ์„ ๊ฐ€์ ธ์˜ด)
5
// select๋ฅผ ์ •์˜ํ•˜๋ฉด, ์ •์˜ํ•œ ์†์„ฑ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค
6
select: {
7
id: true,
8
email: true,
9
version: true,
10
profile: {
11
id: true,
12
},
13
},
14
// ํ•„ํ„ฐ๋งํ•  ์กฐ๊ฑด์„ ์ž…๋ ฅํ•œ๋‹ค ({}์•ˆ์—์„œ๋Š” ์ „๋ถ€ and ์กฐ๊ฑด์œผ๋กœ ํ•„ํ„ฐ๋ง)
15
where: [
16
// id๊ฐ€ 3์ด๊ฑฐ๋‚˜ or version์ด 1
17
{
18
id: 3,
19
},
20
{
21
version: 1,
22
},
23
],
24
// ------ ๋‹ค๋ฅธ ๊ด€๊ณ„๋ฅผ ํ•„ํ„ฐ๋งํ•˜๋Š” ๋ฒ•
25
// where: {
26
// profile: {
27
// id: 3,
28
// },
29
// },
30
// ๊ด€๊ณ„๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฒ•
31
relations: {
32
profile: true,
33
},
34
// ์˜ค๋ฆ„์ฐจ(ASC)-๊ธฐ๋ณธ, ๋‚ด๋ฆผ์ฐจ(DESC)
35
order: {
36
id: 'DESC',
37
},
38
// ์ฒ˜์Œ ๋ช‡ ๊ฐœ๋ฅผ ์ œ์™ธํ•  ์ง€ (๊ธฐ๋ณธ์€ 0) 1์ด๋ฉด 1๊ฐœ ์Šคํ‚ต
39
skip: 0,
40
// ๋ช‡ ๊ฐœ๋ฅผ ๊ฐ€์ ธ์˜ฌ์ง€ (๊ธฐ๋ณธ๊ฐ’์€ 0, ์ „์ฒด) 1์ด๋ฉด 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ด
41
take: 0,
42
})
43
}

13. Typeorm ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋„๊ตฌ

app.controller.ts์—์„œ find()์— ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ์˜ต์…˜์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

app.controller.ts
1
@Post('users')
2
async postUser() {
3
for (let i = 0; i < 100; i++) {
4
await this.userRepository.save({
5
email: `user-${i}@google.com`,
6
})
7
}
8
}
9
10
@Get('users')
11
getUsers() {
12
return this.userRepository.find({
13
where: {
14
// ๐Ÿ“Œ (1) 1์ด ์•„๋‹Œ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
15
id: Not(1),
16
17
// ๐Ÿ“Œ (2) 30 ๋ฏธ๋งŒ์˜ ์ ์€ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
18
id: LessThan(30),
19
20
// ๐Ÿ“Œ (3) 30 ์ดํ•˜์˜ ์ ์€ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
21
id: LessThanOrEqual(30),
22
23
// ๐Ÿ“Œ (4) 30 ์ดˆ๊ณผ์˜ ๋งŽ์€ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
24
id: MoreThan(30)
25
26
// ๐Ÿ“Œ (5) 30 ์ด์ƒ์˜ ๋งŽ์€ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
27
id: MoreThanOrEqual(30)
28
29
// ๐Ÿ“Œ (6) ๊ฐ™์€ ๊ฒฝ์šฐ ๊ฐ€์ ธ์˜ค๊ธฐ
30
id: Equal(30)
31
32
// ๐Ÿ“Œ (7) ์œ ์‚ฌ๊ฐ’, %๋กœ ์œ ์‚ฌํ•œ ๋ฌธ์ž ์ฐพ๊ธฐ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ํ•จ)
33
email: Like('%0@google%'),
34
35
// ๐Ÿ“Œ (8) ์œ ์‚ฌ๊ฐ’, %๋กœ ์œ ์‚ฌํ•œ ๋ฌธ์ž ์ฐพ๊ธฐ (๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„์•ˆํ•จ)
36
email: ILike('%GOOGLE%'),
37
38
// ๐Ÿ“Œ (9) ์‚ฌ์ด๊ฐ’, 10~15๋ฒˆ ์‚ฌ์ด๊นŒ์ง€์˜ ๊ฐ’
39
id: Between(10, 15),
40
41
// ๐Ÿ“Œ (10) ํ•ด๋‹น๋˜๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฐ’, 1, 3, 5, 7, 99์˜ id ์ฐพ๊ธฐ
42
id: In([1, 3, 5, 7, 99]),
43
44
// ๐Ÿ“Œ (11) ID๊ฐ€ null์ธ ๊ฒฝ์šฐ ์ฐพ๊ธฐ
45
id: IsNull(),
46
},
47
// ์ฃผ์„ ์ƒ๋žต
48
})
49
}

๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ณ , 1~100๊ฐœ์˜ ์ž„์˜์˜ ์œ ์ €๋ฅผ ๋งŒ๋“  ํ›„, ๊ฐ๊ฐ์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ํ™•์ธํ•œ๋‹ค.


14. Repository

  • Repository๋Š” ์ง€์ •ํ•œ Entity์— ๋Œ€ํ•œ CRUD ์ฟผ๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค
  • TypeORM์— ์ •์˜๋ผ์žˆ๋Š” ๋ฉ”์„œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•ด์„œ ์ง์ ‘ SQL์„ ์ž‘์„ฑํ•˜์ง€ ์•Š๋”๋ผ๋„ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค
1
const userRepository = dataSources.getRepository(User)
2
const user = await userRepository.findOne({ id: 1 })
3
user.name = '๋ญ์‹œ๊ธฐ'
4
await userRepository.save(user)

Repository์˜ ์ฃผ์š” ๊ธฐ๋Šฅ๋“ค

์ฃผ์š” ๊ธฐ๋Šฅ๋“ค์ฃผ์š” ๋ฉ”์„œ๋“œ
Create & Delete ๊ด€๋ จcreate(), save(), upsert(), delete(), softDelete(), restore()
Update ๊ด€๋ จupdate(), increment(), decrement()
Find ๊ด€๋ จfind(), findAndCount(), findOne(), exists(), preload(), query()
ํ†ต๊ณ„ ๊ด€๋ จ ๋ฐ ๊ธฐํƒ€count(), sum(), average(), minimum(), maximum(), query()

14.1 create()

  • create() ๋ฉ”์„œ๋“œ๋Š” ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค
  • create()๋Š” save()์™€ ๋‹ค๋ฅด๊ฒŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  โ€œ๊ฐ์ฒดโ€๋ฅผ ์ƒ์„ฑ๋งŒ ํ•œ๋‹ค๋Š”๊ฑธ ๊ผญ ๊ธฐ์–ตํ•˜์ž!
1
// const user = new User();
2
const user = repository.create()
3
const user = repository.create({
4
id: 1,
5
firstName: 'Timber',
6
lastName: 'Saw',
7
})

14.2 save()

  • save() ๋ฉ”์„œ๋“œ์— ์ €์žฅํ•  Entity๋ฅผ ์ž…๋ ฅ ํ•ด์ฃผ๋ฉด ์ €์žฅ ํ•  ์ˆ˜ ์žˆ๋‹ค
  • create()์™€ ๋‹ค๋ฅด๊ฒŒ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ์ด ๋œ๋‹ค.
  • ๋งŒ์•ฝ์— ์ด๋ฏธ Row๊ฐ€ ์กด์žฌํ•œ๋‹ค๋ฉด (primary key ๊ฐ’์œผ๋กœ ๊ตฌ๋ถ„) ์—…๋ฐ์ดํŠธํ•œ๋‹ค. (์ฃผ์˜ํ•  ๊ฒƒ!)
  • ์—ฌ๋Ÿฌ ๊ฐ์ฒด๋ฅผ ํ•œ๋ฒˆ์— ์ €์žฅ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค
1
await repository.save(user)
2
await repository.save([category1, category2, category3])

14.3 upsert()

  • update์™€ insert๋ฅผ ํ•ฉ์นœ๊ฒŒ upsert()๋‹ค
  • ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ์‹œ๋„๋ฅผ ํ•œ ํ›„ ๋งŒ์•ฝ์— ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ผ๋ฉด ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค
  • save()์™€ ๋‹ค๋ฅด๊ฒŒ upsert()๋Š” ํ•˜๋‚˜์˜ transaction์—์„œ ์ž‘์—…์ด ์‹คํ–‰๋œ๋‹ค
1
await repository.upsert(
2
[
3
{ externalId: 'abc123', firstName: 'Timber' },
4
{ externalId: 'bca321', firstName: 'Saw' },
5
],
6
['externalId']
7
)
8
/** executes
9
* INSERT INTO user
10
* VALUES
11
* (externalId: "abc123", firstName: 'Timber'),
12
* (externalId: "bca321", firstName: 'Saw')
13
* ON CONFLICT (externalId) DO UPDATE firstName = EXCLUDED.firstName
14
*/

14.4 delete()

  • Row๋ฅผ ์‚ญ์ œํ• ๋•Œ ์‚ฌ์šฉ๋œ๋‹ค
  • ๋Œ€์ฒด์ ์œผ๋กœ Primary Key๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์‚ญ์ œํ•œ๋‹ค
  • ์›ํ•œ๋‹ค๋ฉด findOptionsWhere ์กฐ๊ฑด์œผ๋กœ ์—ฌ๋Ÿฌ ๊ฐ’์„ ์‚ญ์ œ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
1
await repository.delete(1)
2
await repository.delete([1, 2, 3])
3
await repository.delete({ firstName: 'Timber' })

14.5 softDelete(), restore()

  • softDelete()๋Š” ๋น„์˜๊ตฌ์ ์œผ๋กœ ์‚ญ์ œํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค
  • restore()๋ฅผ ์‹คํ–‰ํ•˜๋ฉด softDelete() ํ–ˆ๋˜ Row๋ฅผ ๋ณต๊ตฌ ํ•  ์ˆ˜ ์žˆ๋‹ค
1
await repository.softDelete(1) // ์‚ญ์ œ
2
await repository.restore(1) // ๋ณต๊ตฌ

14.6 update()

  • ์ฒซ๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ฒ€์ƒ‰ ์กฐ๊ฑด์„ ์ž…๋ ฅํ•ด์ค€๋‹ค
  • ๋‘๋ฒˆ์งธ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋ณ€๊ฒฝ ํ•„๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ค€๋‹ค
1
// UPDATE user
2
// SET category = ADULT
3
// WHERE age = 18
4
await repository.update({ age: 18 }, { category: 'ADULT' })
5
6
// UPDATE user
7
// SET firstName = ๋ญ์‹œ๊ธฐ
8
// WHERE id = 1
9
await repository.update(1, { firstName: '๋ญ์‹œ๊ธฐ' })

14.7 find(), findOne(), findAndCount()

  • find() : ํ•ด๋‹น๋˜๋Š” Row๋ฅผ ๋ชจ๋‘ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  • findOne() : ํ•ด๋‹น๋˜๋Š” ์ฒซ๋ฒˆ์งธ Row๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์—†์„๊ฒฝ์šฐ null
  • findAndCount() : ํ•ด๋‹น๋˜๋Š” Row์™€ ์ „์ฒด ๊ฐฏ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
1
const rows = await repository.find({
2
where: { firstName: 'Timber' },
3
})
4
const rows = await repository.findOne({
5
where: { firstName: 'Timber' },
6
})
7
const rows = await repository.findAndCount({
8
where: { firstName: 'Timber' },
9
})

14.7.1 FindOptions

1
export interface FindOneOptions<Entity = any> {
2
select?: FindOneOptionsSelect<Entity> // ๋ถˆ๋Ÿฌ์˜ฌ Column์„ ์ง€์ •
3
where?: FindOptionsWhere<Entity>[] // ํ•„ํ„ฐ๋งํ•  ์กฐ๊ฑด์„ ์„ค์ •
4
relations?: FindOptionsRelations<Entity> // ๋ถˆ๋Ÿฌ์˜ฌ ๊ด€๊ณ„ ํ…Œ์ด๋ธ”์„ ์ง€์ •
5
order?: FindOneOptionsOrder<Entity> // ์ •๋ ฌ์„ ์ง€์ •
6
cache?: boolean | number // ์บ์‹ฑ ๊ธฐ๊ฐ„์„ ์ง€์ •
7
}
8
9
export interface FindManyOptions<Entity = any> extends FindOneOptions<Entity> {
10
skip?: number
11
take?: number
12
}
  • ๋ชจ๋“  find ๊ด€๋ จ๋œ API๋Š” FindOptions๋ฅผ ์•„๊ทœ๋จผํŠธ๋กœ ๋ฐ›๋Š”๋‹ค.
  • FindOptions๋Š” ์–ด๋–ค ๊ฐ’๋“ค์„ ๋ถˆ๋Ÿฌ์˜ฌ์ง€ ํ•„ํ„ฐ๋งํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • FindOptions์˜ ์ •ํ™•ํ•œ TS ํƒ€์ž… ๋ช…์นญ์€ FindOneOptions์™€ FindManyOptions๋กœ ์ •์˜๋ผ์žˆ๋‹ค.
  • FindManyOptions๋Š” FindOneOptions๋ฅผ ์ƒ์†๋ฐ›๊ณ  skip, take 2๊ฐ€์ง€ ํ”„๋กœํผํ‹ฐ๊ฐ€ ๋” ์กด์žฌํ•œ๋‹ค

(1) where ์†์„ฑ
1
// ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
2
const users = await userRepository.find({
3
where: { isActive: true },
4
})
5
6
// ๋‹ค์ค‘์กฐ๊ฑด ์‚ฌ์šฉ๋ฒ•
7
const users = await userRepository.find({
8
where: [
9
{ firstName: 'John', lastName: 'Doe' },
10
{ firstName: 'Jane', lastName: 'Smith' },
11
],
12
})
13
14
// ์ค‘์ฒฉ ์‚ฌ์šฉ๋ฒ•
15
const users = await userRepository.find({
16
where: [
17
isActive: true,
18
profile : { age: MoreThan(25) },
19
],
20
})

(2) order ์†์„ฑ
1
// ๋‹จ์ผ ์ •๋ ฌ ์‚ฌ์šฉ๋ฒ•
2
const users = await userRrepository.find({
3
order: { firstName: 'ASC' },
4
})
5
6
// ๋ณต์ˆ˜ ์ •๋ ฌ ์‚ฌ์šฉ๋ฒ•
7
const users = await userRrepository.find({
8
order: { lastName: 'ASC', firstName: 'DESC' },
9
})

(3) relation ์†์„ฑ
1
const users = await userRepository.find({
2
relations: ['profile', 'photos'],
3
})

(4) select ์†์„ฑ
1
const users = await userRepository.find({
2
select: ['firstName', 'lastName'],
3
})

(5) cache ์†์„ฑ
1
// ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•
2
const users = await userRepository.find({
3
cache: true,
4
})
5
6
// ๊ธฐ๊ฐ„ ์ง์ ‘ ์ •์˜
7
const users = await userRepository.find({
8
cache: 60000, // 60์ดˆ
9
})

14.7.2 FindManyOptions

1
const users = await userRepository.find({
2
skip: 10, // ์ฒ˜์Œ 10๊ฐœ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๊ฐ€์ ธ์˜จ๋‹ค
3
take: 5, // ์ฒ˜์Œ 5๊ฐœ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค
4
})
  • Skip : ์ •๋ ฌ ํ›„ ์Šคํ‚ตํ•  ๋ฐ์ดํ„ฐ ๊ฐฏ์ˆ˜๋ฅผ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค
    • ๊ธฐ๋ณธ๊ฐ’์€ 0, 1์ด๋ฉด 1๊ฐœ๋ฅผ ์ œ์™ธํ•˜๊ณ  ๊ฐ€์ ธ์˜จ๋‹ค
  • Take : ์ฒ˜์Œ ๋ช‡๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ์ง€ ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค
    • ๊ธฐ๋ณธ๊ฐ’์€ 0, 1์ด๋ฉด 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค

14.7.3 Advanced Options

์ฃผ์š” ์˜คํผ๋ ˆ์ดํ„ฐ์ฃผ์š” ๋ฉ”์„œ๋“œ
๋น„๊ตEqual, Not, LessThan, LessThanOrEqual, MoreThan,
MoreThanOrEqual, Between
ํŒจํ„ด๋งค์นญLike, ILike
์ง‘ํ•ฉIn, ArrayContains, ArrayContainedBy, ArrayOverlap
๊ธฐํƒ€IsNull, Or, And, Raw

(1) Equal ์˜คํผ๋ ˆ์ดํ„ฐ
1
// Equal : ๊ฐ™์€ ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { age: Equal(25) },
4
})

(2) Not ์˜คํผ๋ ˆ์ดํ„ฐ
1
// Not : ์•„๋‹Œ ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { age: Not(25) },
4
})

(3) LessThan & LessThanOrEqual ์˜คํผ๋ ˆ์ดํ„ฐ
1
// ์ ์€๊ฐ’ & ์ ๊ฑฐ๋‚˜ ๊ฐ™์€ ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { age: LessThan(30) },
4
})
5
const users = await userRepository.find({
6
where: { age: LessThanOrEqual(30) },
7
})

(4) MoreThan & MoreThanOrEqual ์˜คํผ๋ ˆ์ดํ„ฐ
1
// ๋” ํฐ๊ฐ’ & ํฌ๊ฑฐ๋‚˜ ๊ฐ™์€ ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { age: MoreThan(20) },
4
})
5
const users = await userRepository.find({
6
where: { age: MoreThanOrEqual(20) },
7
})

(5) Between
1
// ์‚ฌ์ด ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { age: Between(20, 30) },
4
})

(6) Like & ILike
1
// ์ŠคํŠธ๋ง์— ๋งค์นญ๋˜๋Š” ๊ฐ’์„ ์ฐพ์„๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. Like๋Š” ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ํ• ๋•Œ, ILike๋Š” ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š์„๋•Œ ์‚ฌ์šฉ
2
const users = await userRepository.find({
3
where: { firstName: Like('Co%') },
4
})
5
const users = await userRepository.find({
6
where: { firstName: ILike('co%') },
7
})

(7) In
1
// ๋ฆฌ์ŠคํŠธ์— ๋งค์นญ๋˜๋Š” ๊ฐ’์„ ์ฐพ๋Š”๋‹ค
2
const users = await userRepository.find({
3
where: { age: In([20, 25, 30]) },
4
})

(8) ArrayContains & ArrayContainedBy & ArrayOverlap
1
// ArrayContains : ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ ๊ฐ’์ด ํƒ€๊ฒŸ ๋ฆฌ์ŠคํŠธ์™€ ์™„์ „ ๋˜‘๊ฐ™์€ ๊ฒฝ์šฐ๋ฅผ ํ•„ํ„ฐ๋ง
2
const users = await userRepository.find({
3
where: { roles: ArrayContains(['admin']) },
4
})
5
6
// ArrayContainedBy : ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ ๊ฐ’์ด ํƒ€๊ฒŸ ๋ฆฌ์ŠคํŠธ ์•ˆ์— ๋ชจ๋‘ ํฌํ•จ๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ•„ํ„ฐ๋ง
7
const users = await userRepository.find({
8
where: { roles: ArrayContainedBy(['admin', 'user']) },
9
})
10
11
// ArrayOverlap : ์—”ํ‹ฐํ‹ฐ ๋ฆฌ์ŠคํŠธ ๊ฐ’์ด ํ•˜๋‚˜๋ผ๋„ ํƒ€๊ฒŸ ๋ฆฌ์ŠคํŠธ์™€ ๊ฒน์น˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ•„ํ„ฐ๋ง
12
const users = await userRepository.find({
13
where: { roles: ArrayOverlap(['admin', 'guest']) },
14
})

(8-1) ArrayContains

1
// Record 1
2
{
3
id: 1,
4
tags: ['admin', 'user', 'manager'],
5
}
6
// Record 2
7
{
8
id: 2,
9
roles: ['user', 'guest'],
10
}
1
const users = await getRepository(User).find({
2
// Record 2์˜ Tags๊ฐ€ ์™„์ „ํžˆ ๊ฐ™์œผ๋‹ˆ Record 2๋งŒ ๋ฐ˜ํ™˜
3
tags: ArrayContains(['user', 'guest']),
4
})

(8-2) ArrayContainedBy

1
// Record 1
2
{
3
id: 1,
4
tags: ['admin', 'user', 'manager'],
5
}
6
// Record 2
7
{
8
id: 2,
9
roles: ['user', 'guest'],
10
}
1
const users = await getRepository(User).find({
2
// Record 1๊ณผ Record 2์˜ Tags๋“ค์ด ๋ชจ๋‘ ํ•„ํ„ฐ ๋ฆฌ์ŠคํŠธ์— ํฌํ•จ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋‘˜ ๋‹ค ๋ฐ˜ํ™˜
3
tags: ArrayContainedBy(['admin', 'user', 'guest', 'manager']),
4
})

(8-3) ArrayOverlap

1
// Record 1
2
{
3
id: 1,
4
tags: ['admin', 'user', 'manager'],
5
}
6
// Record 2
7
{
8
id: 2,
9
roles: ['user', 'guest'],
10
}
1
const users = await getRepository(User).find({
2
// Record 1์˜ Tags์™€ Record 2์˜ Tags๊ฐ€ ๋ชจ๋‘ ํ•„ํ„ฐ ๋ฆฌ์ŠคํŠธ์™€ ๊ฒน์น˜๋Š” ๋ถ€๋ถ„์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋‘˜ ๋‹ค ๋ฐ˜ํ™˜
3
tags: ArrayOverlap(['guest', 'admin']),
4
})

(9) IsNull
1
const users = await userRepository.find({
2
where: { age: IsNull() }, // age๊ฐ€ null์ธ ๊ฒฝ์šฐ
3
})

(10) Or
1
const loadedPosts = await dataSource.getRepository(Post).findBy({
2
// 2๊ฐœ์˜ ์กฐ๊ฑด ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑํ•˜๋ฉด ๋ฐ˜ํ™˜
3
title: Or(Equal('About #2'), ILike('About%')),
4
})

(11) And
1
const loadedPosts = await dataSource.getRepository(Post).findBy({
2
// 2๊ฐœ์˜ ์กฐ๊ฑด ๋ชจ๋‘ ๋งŒ์กฑํ•˜๋ฉด ๋ฐ˜ํ™˜
3
title: And(Not(Equal('About #2'), ILike('About%'))),
4
})

14.7.4 ๋ณต์žกํ•œ ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ฟผ๋ฆฌ

1
const users = await userRepository.find({
2
where: [
3
{ isActive: true, age: MoreThan(25) },
4
{ firstName: 'John', age: LessThan(50) },
5
],
6
order: { fistName: 'ASC' },
7
relations: ['profile'],
8
select: ['firstName', 'lastName'],
9
skip: 0,
10
take: 10,
11
cache: true,
12
loadRelationIds: true,
13
loadEagerRelations: false,
14
withDeleted: false,
15
})

14.7 exists()

  • exists() : ํŠน์ • ์กฐ๊ฑด์˜ Row๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ boolean ๊ฐ’์„ ๋ฐ˜ํ™˜ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค
1
const exists = await repository.exists({
2
where: {
3
firstName: 'Timber',
4
},
5
})

14.8 preload()

  • preload()๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๊ฐ’์„ Primary Key ๊ธฐ์ค€์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ณ  ์ž…๋ ฅ๋œ ๊ฐ์ฒด์˜ ๊ฐ’์œผ๋กœ ํ”„๋กœํผํ‹ฐ๋ฅผ ๋ฎ์–ด์“ด๋‹ค
  • ๋ฎ์–ด์“ฐ๋Š” ๊ณผ์ •์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—…๋ฐ์ดํŠธ ์š”์ฒญ์ด ๋ณด๋‚ด์ง€์ง€๋Š” ์•Š๋Š”๋‹ค
1
const partiaIUser = {
2
id: 1,
3
firstName: 'Timber',
4
profile: { id: 1 },
5
}
6
const user = await repository.preload(partiaIUser)

15. ๊ฐ์ข… ํ†ต๊ณ„

  • count() : ํ•ด๋‹น๋˜๋Š” ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  • sum() : ํ•ด๋‹น๋˜๋Š” ์†์„ฑ๋“ค์˜ ๊ฐ’์„ ์ „๋ถ€ ํ•ฉ์นœ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  • average() : ํ•ด๋‹น๋˜๋Š” ์†์„ฑ๋“ค์˜ ํ‰๊ท ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  • minimum() : ํ•ด๋‹น๋˜๋Š” ์†์„ฑ๋“ค์˜ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค
  • maximum() : ํ•ด๋‹น๋˜๋Š” ์†์„ฑ๋“ค์˜ ์ตœ๋Œ€๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค
1
// firstName์ด Timber์ธ ๊ฐ’๋“ค์˜ ๊ฐœ์ˆ˜
2
const count = await repository.count({
3
where: { firstName: 'Timber' },
4
})
5
// age ์ปฌ๋Ÿผ์˜ firstName์ด Timber์ธ ๊ฐ’๋“ค์˜ ํ•ฉ๊ณ„
6
const sum = await repository.sum('age', {
7
firstName: 'Timber',
8
})
9
// age ์ปฌ๋Ÿผ์˜ firstName์ด Timber์ธ ๊ฐ’๋“ค์˜ ํ‰๊ท ๊ฐ’
10
const average = await repository.average('age', {
11
firstName: 'Timber',
12
})
13
// age ์ปฌ๋Ÿผ์˜ firstName์ด Timber์ธ ๊ฐ’๋“ค์˜ ์ตœ์†Œ๊ฐ’
14
const minimum = await repository.minimum('age', {
15
firstName: 'Timber',
16
})
17
// age ์ปฌ๋Ÿผ์˜ firstName์ด Timber์ธ ๊ฐ’๋“ค์˜ ์ตœ๋Œ€๊ฐ’
18
const maximum = await repository.maximum('age', {
19
firstName: 'Timber',
20
})

16. Repository ์‹ค์Šต

user.entity.ts์— count ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

1
@Column({ default: 0 })
2
count: number

app ์ปจํŠธ๋กค๋Ÿฌ์— sample ์š”์ฒญ์„ ๋งŒ๋“ค๊ณ  ์‹ค์Šตํ•œ๋‹ค.

app.controller.ts
1
@Post('sample')
2
async sample() {
3
// ๐Ÿ“Œ (1) ๋ชจ๋ธ์— ํ•ด๋‹น๋˜๋Š” ๊ฐ์ฒด ์ƒ์„ฑ - ์ €์žฅ์€ ์•ˆํ•จ
4
const user1 = await this.userRepository.create({
5
email: 'test@gmail.ai',
6
})
7
8
// ๐Ÿ“Œ (2) ๋ชจ๋ธ์— ํ•ด๋‹น๋˜๋Š” ๊ฐ์ฒด ์ƒ์„ฑ - DB์— ์ €์žฅํ•จ
9
const user2 = await this.userRepository.save({
10
email: 'test@gmail.ai',
11
})
12
13
/*** ๐Ÿ“Œ (3) preload
14
* ์ž…๋ ฅ๋œ ๊ฐ’์„ ๊ธฐ๋ฐ˜์œผ๋กœ DB์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ ,
15
* ์ถ”๊ฐ€์ž…๋ ฅ๋œ ๊ฐ’์œผ๋กœ DB์— ๊ฐ€์ ธ์˜จ ๊ฐ’๋“ค์„ ๋Œ€์ฒดํ•จ
16
* ์ €์žฅํ•˜์ง€๋Š” ์•Š์Œ
17
*/
18
const user3 = await this.userRepository.preload({
19
id: 101,
20
email: 'test๋ณ€๊ฒฝ-์ €์žฅx@gmail.ai',
21
})
22
23
// ๐Ÿ“Œ (4) ์‚ญ์ œํ•˜๊ธฐ
24
await this.userRepository.delete(101)
25
26
// ๐Ÿ“Œ (5) ์ˆซ์žํ˜• ์ปฌ๋Ÿผ ์ฆ๊ฐ€ (id๊ฐ€ 1์ธ count ์ปฌ๋Ÿผ์„ 2๋งŒํผ ์ฆ๊ฐ€)
27
await this.userRepository.increment(
28
{ id: 1 }, //
29
'count',
30
2,
31
)
32
33
// ๐Ÿ“Œ (6) ์ˆซ์žํ˜• ์ปฌ๋Ÿผ ๊ฐ์†Œ (id๊ฐ€ 1์ธ count ์ปฌ๋Ÿผ์„ 1๋งŒํผ ๊ฐ์†Œ)
34
await this.userRepository.decrement(
35
{ id: 1 }, //
36
'count',
37
1,
38
)
39
40
// ๐Ÿ“Œ (7) ๊ฐœ์ˆ˜ ์นด์šดํŒ…ํ•˜๊ธฐ
41
const count = await this.userRepository.count({
42
where: {
43
email: ILike('%0%'),
44
},
45
})
46
47
// ๐Ÿ“Œ (8) sum : ์†์„ฑ๋“ค์˜ ๊ฐ’ ์ „๋ถ€ ํ•ฉ์น˜๊ธฐ
48
const sum = await this.userRepository.sum('count', {
49
email: ILike('%0%'),
50
})
51
52
// ๐Ÿ“Œ (9) average : ์†์„ฑ์˜ ํ‰๊ท ๊ฐ’ ๊ตฌํ•˜๊ธฐ
53
const average = await this.userRepository.average('count', {
54
id: LessThan(4),
55
})
56
57
// ๐Ÿ“Œ (10) min : ์†์„ฑ์˜ ์ตœ์†Œ๊ฐ’ ๊ตฌํ•˜๊ธฐ
58
const min = await this.userRepository.minimum('count', {
59
id: LessThan(4),
60
})
61
62
// ๐Ÿ“Œ (10) max : ์†์„ฑ์˜ ์ตœ๋Œ€๊ฐ’ ๊ตฌํ•˜๊ธฐ
63
const max = await this.userRepository.maximum('count', {
64
id: LessThan(4),
65
})
66
67
// ๐Ÿ“Œ (11) find์™€ findOne๋„ ์žˆ์Œ(๋งŽ์ด ๋‹ค๋ค˜์œผ๋‹ˆ ์ƒ๋žต)
68
const userOne = await this.userRepository.findOne({
69
where: { id: 3 },
70
})
71
72
// ๐Ÿ“Œ (12) 3๊ฐœ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ, ์ „์ฒด ํ–‰ ๊ฐœ์ˆ˜๋„ ๋ฐ˜ํ™˜ํ•ด์คŒ
73
const usersAndCount = await this.userRepository.findAndCount({
74
take: 3,
75
})
76
77
return usersAndCount
78
}

17. QueryBuilder

1
const users = await userRepository
2
.craeteQueryBuilder('user') // user ํ…Œ์ด๋ธ”์„ ๊ธฐ๋ฐ˜์œผ๋กœ QueryBuilder ์ƒ์„ฑ
3
.leftJoinAndSelect('user.profile', 'profile') // user.profile ํ…Œ์ด๋ธ”์„ ์กฐ์ธ
4
.where('user.isActive = :isActive', { isActive: true }) // isActive๊ฐ€ true์ธ ๊ฐ’๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
5
// ์‚ฌ์šฉ๋ฒ• : where("์ปฌ๋Ÿผ = :๋ณ€์ˆ˜๋ช…", { ๋ณ€์ˆ˜๋ช…: ๊ฐ’ })
6
.orderBy('user.firstName', 'ASC') // firstName์„ ๊ธฐ์ค€์œผ๋กœ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ
7
.skip(10) // ์ฒ˜์Œ 10๊ฐœ ์ œ์™ธํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๊ธฐ
8
.take(5) // ์ฒ˜์Œ 5๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
9
.getMany() // ๊ฒฐ๊ณผ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ๋ณต์žกํ•˜์ง€ ์•Š์€ ์ผ๋ฐ˜์ ์ธ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ๋Š” Repository๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ํŽธ๋ฆฌํ•˜๋‹ค.
  • ์กฐ๊ธˆ ๋” ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ ํ•ด์•ผํ•˜๊ฑฐ๋‚˜ ๋‹ค์ด๋‚˜๋ฏนํ•˜๊ฒŒ ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€์•ผ ํ•  ๊ฒฝ์šฐ Query Builder๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค

17.1 Query Builder์˜ 5๊ฐ€์ง€ ์‹คํ–‰ ํƒ€์ž… : (1) select

1
const movie = await dataSource
2
.createQueryBuilder()
3
.select('movie')
4
.from(Movie, 'movie') // from(ํ…Œ์ด๋ธ”๋ช…, '๋ณ„์นญ')
5
.leftJoinAndSelect('movie.detail', 'detail') // leftJoinAndSelect(ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…, '๋ณ„์นญ')
6
.leftJoinAndSelect('movie.director', 'director')
7
.leftJoinAndSelect('movie.genres', 'genres')
8
.where('movie.id = :id', { id: 1 }) // where(์กฐ๊ฑด, { ๋ณ€์ˆ˜๋ช…: ๊ฐ’ }) - ":id"๋Š” ๋ณ€์ˆ˜๋ฅผ ์˜๋ฏธ
9
.getOne() // ๊ฒฐ๊ณผ๊ฐ’์„ ํ•˜๋‚˜๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ, ์—ฌ๋Ÿฌ ๊ฐœ๋Š” getMany()๋ฅผ ์‚ฌ์šฉ

17.2 Query Builder์˜ 5๊ฐ€์ง€ ์‹คํ–‰ ํƒ€์ž… : (2) insert

1
await dataSource
2
.createQueryBuilder()
3
.insert()
4
.into(Movie) // data๋ฅผ ๋„ฃ์„ ํ…Œ์ด๋ธ”
5
.values([
6
{
7
title: 'New Movie', //
8
genre: 'Action',
9
director: director,
10
genres: genres,
11
},
12
])
13
.execute() // ์‹คํ–‰

17.3 Query Builder์˜ 5๊ฐ€์ง€ ์‹คํ–‰ ํƒ€์ž… : (3) update

1
await dataSource
2
.createQueryBuilder()
3
.update(Movie)
4
.set({ title: 'Updated Movie', genre: 'Drama' })
5
.where('id = :id', { id: 1 })
6
.execute()

17.4 Query Builder์˜ 5๊ฐ€์ง€ ์‹คํ–‰ ํƒ€์ž… : (4) delete

1
await dataSource
2
.createQueryBuilder()
3
.delete()
4
.from(Movie)
5
.where('id = :id', { id: 1 }) // id๊ฐ€ 1์ธ ๊ฐ’์„ ์‚ญ์ œ
6
.execute()

17.5 Query Builder์˜ 5๊ฐ€์ง€ ์‹คํ–‰ ํƒ€์ž… : (5) relations

1
const genres = await dataSource
2
.createQueryBuilder()
3
.relation(Movie, 'genres') // Movie ํ…Œ์ด๋ธ”์˜ genres ์ปฌ๋Ÿผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ
4
.of(1) // Movie id๊ฐ€ 1์ธ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ
5
.loadMany() // ๊ฒฐ๊ณผ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ

17.6 getOne(), getMany(), select()

1
// ๋‹จ์ผ Row๋งŒ ๊ฐ€์ ธ์˜ฌ ๋•Œ
2
const uesrs = await connection
3
.getRepository(User) // User ํ…Œ์ด๋ธ”์„ ๊ธฐ๋ฐ˜์œผ๋กœ Repository ์ƒ์„ฑ
4
.createQueryBuilder('user') // createQueryBuilder('๋ณ„์นญ')
5
.select(['user.id', 'user.firstName', 'user.lastName'])
6
.getOne()
7
8
// ๋ณต์ˆ˜ Row๋งŒ ๊ฐ€์ ธ์˜ฌ ๋•Œ
9
const users = await connection
10
.getRepository(User)
11
.craeteQueryBuilder('user')
12
.select(['user.id', 'user.firstName', 'user.lastName'])
13
.getMany()

17.7 where()

1
// ํ•˜๋‚˜์˜ ํ•„ํ„ฐ๋ง ์กฐ๊ฑด ์ ์šฉ
2
const users = await connection
3
.getRepository(User) // User ํ…Œ์ด๋ธ”์„ ๊ธฐ๋ฐ˜์œผ๋กœ Repository ์ƒ์„ฑ
4
.createQueryBuilder('user') // user ํ…Œ์ด๋ธ”์„ ๊ธฐ๋ฐ˜์œผ๋กœ QueryBuilder ์ƒ์„ฑ
5
.where('user.isActive = :isActive', { isActive: true })
6
.getMany()
7
8
// ๋‹ค์ˆ˜์˜ ํ•„ํ„ฐ๋ง ์กฐ๊ฑด ์ ์šฉ
9
const users = await connection
10
.getRepository(User)
11
.createQueryBuilder('user')
12
.where('user.firstName = :firstName', { firstName: 'Timber' })
13
.andWhere('user.lastName = :lastName', { lastName: 'Saw' })
14
.getMany()

17.8 orderBy()

1
const users = await connection
2
.getRepository(User)
3
.createQueryBuilder('user')
4
.orderBy('user.lastName', 'ASC') // lastName์„ ๊ธฐ์ค€์œผ๋กœ ์˜ค๋ฆ„์ฐจ์ˆœ ์ •๋ ฌ
5
.addOrderBy('user.firstName', 'DESC') // firstName์„ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
6
.getMany()

17.9 skip(), take()

1
const users = await connection
2
.getRepository(User)
3
.createQueryBuilder('user')
4
.skip(10) // ์ฒ˜์Œ 10๊ฐœ ์ œ์™ธํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๊ธฐ
5
.take(5) // ์ฒ˜์Œ 5๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
6
.getMany()

17.10 join()

1
// Inner Join
2
const users = await connection
3
.getRepository(User)
4
.createQueryBuilder('user') // createQueryBuilder('๋ณ„์นญ')
5
.innerJoinAndSelect('user.profile', 'profile') // innerJoinAndSelect(ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…, '๋ณ„์นญ')
6
.getMany()
7
8
// Left Join
9
const users = await connection
10
.getRepository(User)
11
.createQueryBuilder('user')
12
.leftJoinAndSelect('user.photos', 'photos') // leftJoinAndSelect(ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…, '๋ณ„์นญ')
13
.getMany()

17.11 Aggregation(์ง‘๊ณ„)

1
const userCount = await connection
2
.getRepository(User)
3
.creawteQueryBuilder('user')
4
.select('COUNT(user.id)', 'count') // select('์ง‘๊ณ„ํ•จ์ˆ˜(ํ…Œ์ด๋ธ”๋ช….์ปฌ๋Ÿผ๋ช…)', '๋ณ„์นญ')
5
.getRawOne()

17.12 Subquery

1
const users = await connection
2
.getRepository(User)
3
.createQueryBuilder('user')
4
.where((qb) => {
5
const subQuery = qb
6
.subQuery()
7
.select('subUser.id')
8
.from(User, 'subUser')
9
.where('subUser.isActive = :isActive', { isActive: true })
10
.getQuery()
11
return 'user.id IN ' + subQuery // user.id๊ฐ€ subQuery์— ํฌํ•จ๋˜๋Š” ๊ฐ’๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
12
})
13
.setParameters('isActive', true)
14
.getMany()

17.12 Raw Query

1
const users = await connection
2
.getRepository(User)
3
.createQueryBuilder()
4
.select('user')
5
.from(User, 'user')
6
.where('user.isActive = :isActive', { isActive: true })
7
.getRawMany()