Entity 가 있다고 가정해보자
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"
import { Photo } from "./Photo"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@OneToMany((type) => Photo, (photo) => photo.user)
photos: Photo[]
}
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User"
@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number
@Column()
url: string
@ManyToOne((type) => User, (user) => user.photos)
user: User
}
여기서 Timber와 그의 모든 사진을 가지고오고 싶다면
const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.getOne()
결과는
{
id: 1,
name: "Timber",
photos: [{
id: 1,
url: "me-with-chakram.jpg"
}, {
id: 2,
url: "me-with-trees.jpg"
}]
}
"leftJoinAndSelect는 Timber의 모든 사진을 자동으로 불러온다. 첫 번째 인자는 로드하려는 관계이고 두 번째 인자는 이 관계의 테이블에 할당하는 별칭이다.
예를 들어, 삭제되지 않은 Timber의 모든 사진을 가져와보자
const user = await createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.andWhere("photo.isRemoved = :isRemoved", { isRemoved: false })
.getOne()
쿼리문은
SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Timber' AND photo.isRemoved = FALSE
Inner and Left Joins
LEFT JOIN 대신에 INNER JOIN을 사용하고 싶다면 innerJoinAndSelect을 사용
const user = await createQueryBuilder("user")
.innerJoinAndSelect(
"user.photos",
"photo",
"photo.isRemoved = :isRemoved",
{ isRemoved: false },
)
.where("user.name = :name", { name: "Timber" })
.getOne()
쿼리문은
SELECT user.*, photo.* FROM users user
INNER JOIN photos photo ON photo.user = user.id AND photo.isRemoved = FALSE
WHERE user.name = 'Timber'
LEFT JOIN과 INNER JOIN의 차이점은 INNER JOIN은 사용자가 사진을 가지고 있지 않으면 해당 사용자를 반환하지 않는다. 반면에 LEFT JOIN은 사용자가 사진을 가지고 있지 않아도 해당 사용자를 반환한다.
Join without Selection
선택하지 않고 데이터를 조인할 수 있다 -> leftJoin 또는 innerJoin을 사용
const user = await createQueryBuilder("user")
.innerJoin("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.getOne()
쿼리문은
SELECT user.* FROM users user
INNER JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Timber'
Timber의 사진이 있으면 select를 하나, 리턴하지 않음
Joining any entity or table
관련된 엔터티나 테이블 뿐만 아니라 관련이 없는 다른 엔터티나 테이블도 조인할 수 있다.
const user = await createQueryBuilder("user")
.leftJoinAndSelect(Photo, "photo", "photo.userId = user.id")
.getMany()
const user = await createQueryBuilder("user")
.leftJoinAndSelect("photos", "photo", "photo.userId = user.id")
.getMany()
Joining and mapping functionality
User 엔터티에 profilePhoto를 추가하고 QueryBuilder를 사용하여 해당 속성에 원하는 데이터를 매핑할 수 있다.
export class User {
/// ...
profilePhoto: Photo
}
const user = await createQueryBuilder("user")
.leftJoinAndMapOne(
"user.profilePhoto",
"user.photos",
"photo",
"photo.isForProfile = TRUE",
)
.where("user.name = :name", { name: "Timber" })
.getOne()
이렇게 하면 Timber의 프로필 사진을 로드하고 이를 user.profilePhoto에 설정한다.
하나의 엔터티를 로드하고 매핑하려면 leftJoinAndMapOne을 사용
여러 엔터티를 로드하고 매핑하려면 leftJoinAndMapMany를 사용
Getting the generated query
QueryBuilder에서 생성된 SQL 쿼리를 얻고 싶을 땐 getSql을 사용
const sql = createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
.getSql()
debugging을 하려면 printSql 사용
const users = await createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
.printSql()
.getMany()
Getting raw results
select 쿼리 빌더를 사용하여 얻을 수 있는 결과 유형은 두 가지 -> 엔터티와 raw 결과이다.
대부분의 경우 데이터베이스에서 실제 엔터티를 선택해야 하는데, 예) 사용자.
이를 위해 getOne과 getMany를 사용하고 때로는 사용자 사진의 합과 같은 특정 데이터를 선택한다.
이러한 데이터는 엔터티가 아니라 raw 데이터라 한다.
raw 데이터를 얻으려면 getRawOne과 getRawMany을 사용
const { sum } = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("SUM(user.photosCount)", "sum")
.where("user.id = :id", { id: 1 })
.getRawOne()
const photosSums = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("user.id")
.addSelect("SUM(user.photosCount)", "sum")
.groupBy("user.id")
.getRawMany()
// result will be like this: [{ id: 1, sum: 25 }, { id: 2, sum: 13 }, ...]
Streaming result data
stream은 스트림을 반환합니다. 스트리밍은 raw 데이터를 반환하며, 엔터티 변환을 수동으로 처리해야한다.
const stream = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.stream()
Using pagination
대부분의 경우 애플리케이션을 개발할 때 페이지네이션 기능이 필요.
이 기능은 애플리케이션에 페이지네이션, 페이지 슬라이더 또는 무한 스크롤 구성 요소가 있는 경우에 사용된다.
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.take(10)
.getMany()
// 10 유저 정보 전달
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.skip(10)
.getMany()
// 첫 10 개 정보 제외하고 보여줌
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.photos", "photo")
.skip(5)
.take(10)
.getMany()
// 첫 5개 정보 스킵 후 10개 정보 보여줌
take와 skip은 limit와 offset을 사용하는 것처럼 보일 수 있지만, 실제로는 그렇지 않다.
join이나 서브쿼리와 같이 더 복잡한 쿼리가 있는 경우 limit와 offset이 예상대로 작동하지 않을 수 있다.
take와 skip을 사용하면 이러한 문제를 방지할 수 있다.
Set Lock
데이터베이스에서 트랜잭션 동안 사용자가 특정 엔티티 또는 특정 쿼리를 잠금 처리할 수 있도록 하는 기능을 제공함
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_read")
.getMany()
"Pessimistic locking"은 데이터베이스 트랜잭션에서 다른 트랜잭션이 해당 데이터를 변경하지 못하도록 데이터를 잠그는 방법 중 하나이다.
여러 사용자가 동시에 동일한 데이터를 수정하려는 경우 데이터 일관성과 무결성을 유지하기 위해 사용될 수 있다.
setLock("pessimistic_read")는 해당 쿼리로 선택된 데이터를 읽기 위해 피관측적 읽기 잠금을 설정하는 것.
다른 트랜잭션에서 해당 데이터를 수정하는 것을 막고, 읽기 작업을 보장하기 위해 사용될 수 있다
SetOnLock
행이 잠겨있을 때 어떻게 처리할지를 제어할 수 있다. 기본적으로 데이터베이스는 잠금을 기다림.
setOnLocked를 사용하여 이러한 동작을 제어할 수 있다.
기다리지 않을 때
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_write")
.setOnLocked("nowait")
.getMany()
행을 스킵할 때
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.setLock("pessimistic_write")
.setOnLocked("skip_locked")
.getMany()
Use custom index
특정 경우에 데이터베이스 서버가 사용할 인덱스를 제공할 수 있다. 이 기능은 MySQL에서만 지원
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.useIndex("my_index") // name of index
.getMany()
Max execution time
서버 충돌을 방지하기 위해 느린 쿼리를 제거할 수 있다.
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.maxExecutionTime(1000) // milliseconds.
.getMany()
Partial selection
특정 엔터티 속성만 선택하기
const users = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select(["user.id", "user.name"])
.getMany()
Using subqueries
subquries 를 쉽게 생성 가능하다.
const qb = await dataSource.getRepository(Post).createQueryBuilder("post")
const posts = qb
.where(
"post.title IN " +
qb
.subQuery()
.select("user.name")
.from(User, "user")
.where("user.registered = :registered")
.getQuery(),
)
.setParameter("registered", true)
.getMany()
더 정교한 방법
const posts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.where((qb) => {
const subQuery = qb
.subQuery()
.select("user.name")
.from(User, "user")
.where("user.registered = :registered")
.getQuery()
return "post.title IN " + subQuery
})
.setParameter("registered", true)
.getMany()
쿼리를 분리하여 할 수 있음
const userQb = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("user.name")
.where("user.registered = :registered", { registered: true })
const posts = await dataSource
.getRepository(Post)
.createQueryBuilder("post")
.where("post.title IN (" + userQb.getQuery() + ")")
.setParameters(userQb.getParameters())
.getMany()
From 절에서 subquery 생성 가능
const userQb = await dataSource
.getRepository(User)
.createQueryBuilder("user")
.select("user.name", "name")
.where("user.registered = :registered", { registered: true })
const posts = await dataSource
.createQueryBuilder()
.select("user.name", "name")
.from("(" + userQb.getQuery() + ")", "user")
.setParameters(userQb.getParameters())
.getRawMany()
더 정교한 방법
const posts = await dataSource
.createQueryBuilder()
.select("user.name", "name")
.from((subQuery) => {
return subQuery
.select("user.name", "name")
.from(User, "user")
.where("user.registered = :registered", { registered: true })
}, "user")
.getRawMany()
두 번째 from'으로서 subselects을 추가하려면 addFrom을 사용할 수 있다.
또한 SELECT 문에서도 subselects를 사용할 수 있다
const posts = await dataSource
.createQueryBuilder()
.select("post.id", "id")
.addSelect((subQuery) => {
return subQuery.select("user.name", "name").from(User, "user").limit(1)
}, "name")
.from(Post, "post")
.getRawMany()
Hidden Columns
조회하는 모델에 'select: false'로 설정된 열이 있는 경우, 해당 열에서 정보를 검색하려면 addSelect 함수를 사용해야 함.
Entity
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@Column({ select: false })
password: string
}
표준 find 또는 query를 사용하면 모델의 비밀번호 속성을 받지 못함 -> addSelect 써야함
const users = await dataSource
.getRepository(User)
.createQueryBuilder()
.select("user.id", "id")
.addSelect("user.password")
.getMany()
Querying Deleted rows
조회하는 모델에 @DeleteDateColumn 속성이 설정된 열이 있다면, 쿼리 빌더는 자동으로 '소프트 삭제된' 행을 쿼리함.
Entity
import { Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from "typeorm"
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@DeleteDateColumn()
deletedAt?: Date
}
표준 find 또는 query를 사용하면 해당 행에 값이 있는 행을 받지 못함 -> withDeleted 써야 함
const users = await dataSource
.getRepository(User)
.createQueryBuilder()
.select("user.id", "id")
.withDeleted()
.getMany()
'개발 공부' 카테고리의 다른 글
| Node.js (0) | 2024.07.15 |
|---|---|
| 동기(synchronous) 와 비동기(asynchronous) 함수 (0) | 2024.07.15 |
| TypeORM - QueryBuilder 1 (0) | 2023.11.20 |
| TypeORM (0) | 2023.11.17 |
| REST API와 GraphQL (0) | 2023.11.16 |