데이터베이스와 상호작용은 DataSource
를 이용한다.
한개의 DataSource는 내가 연결한 하나의 DataBase가 되기 때문에 하나의 애플리케이션에서 여러개의 DataSource를 컨트롤하여 여러 DataBase를 제어 할 수도있다.
새로운 데이터소스 인스턴스를 생성 할려면 다음과 같이 생성자를 초기화 한다.
import { DataSource } from "typeorm"
const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "test",
password: "test",
database: "test",
})
AppDataSource.initialize()
.then(() => {
console.log("Data Source has been initialized!")
})
.catch((err) => {
console.error("Error during Data Source initialization", err)
})
글로벌하게 사용할 때가 많으니 싱글톤인스턴스로 전역적으로 사용 할 수 있게 만들어주는게 좋다.
필요한 만큼 다음과 같이 여러 데이터소스를 정의할 수도 있다.
데이터소스를 정의하는데 쓰이는 옵션은 여기에서 확인 할 수 있다.
NestJS에서는 다음과 같이 정의한다.
import { AccountEntities } from '@app/account/account.module';
import { FileEntities } from '@app/file/file.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
export const dataBaseConfig: TypeOrmModuleAsyncOptions = {
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mariadb',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
synchronize: configService.get<boolean>('DB_SYNCHRONIZE'),
// ADD ENTITIES
entities: [...AccountEntities, ...FileEntities],
}),
inject: [ConfigService],
};
app.module.ts
...
@Module({
imports: [
// env
ConfigModule.forRoot({
envFilePath:
process.env.NODE_ENV === 'prod'
? ['src/config/env/.prod.env']
: ['src/config/env/.dev.env'],
}),
// database
TypeOrmModule.forRootAsync(dataBaseConfig),
],
})
export class AppModule {}
안정성을 위해 비동기로 처리하는걸 추천한다.
서로 다른 데이터베이스에 연결된 데이터 소스를 이용할려면 다음과 같이 DataSource를 각각 정의해주면 된다.
express
import { DataSource } from "typeorm"
export static const db1DataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "db1",
entities: [__dirname + "/entity/*{.js,.ts}"],
synchronize: true,
})
export static const db2DataSource = new DataSource({
type: "postgresql",
host: "localhost",
port: 5432,
username: "root",
password: "admin",
database: "db2",
entities: [__dirname + "/entity/*{.js,.ts}"],
synchronize: true,
})
nestjs
import { AccountEntities } from '@app/account/account.module';
import { FileEntities } from '@app/file/file.module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
export const dataBaseConfig: TypeOrmModuleAsyncOptions = {
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mariadb',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
synchronize: configService.get<boolean>('DB_SYNCHRONIZE'),
// ADD ENTITIES
entities: [...AccountEntities],
}),
inject: [ConfigService],
};
export const dataBase2Config: TypeOrmModuleAsyncOptions = {
imports: [ConfigModule],
name:'DataSource2',
useFactory: (configService: ConfigService) => ({
type: 'postgresql',
host: configService.get<string>('DB2_HOST'),
port: configService.get<number>('DB2_PORT'),
username: configService.get<string>('DB2_USER'),
password: configService.get<string>('DB2_PASSWORD'),
database: configService.get<string>('DB2_DATABASE'),
synchronize: configService.get<boolean>('DB2_SYNCHRONIZE'),
// ADD ENTITIES
entities: [ ...FileEntities],
}),
inject: [ConfigService],
};
NestJS는 DI구분을 위해 2번째요소 부턴 name을 지정해주어야 한고 다음과 같이 사용한다.
export class SecretService extends TypeOrmQueryService<SecretEntity> {
constructor(
//첫번째 dataSource
private readonly datasource: DataSource,
//두번째 dataSource
@Inject('dataSource2') private readonly datasource2:DataSource,
) {
super(repository);
}
}
단일 데이터소스에서 여러 데이터베이스를 사용하려면 엔티티별로 이름을 지정해주면 된다.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity({ database: "secondDB" })
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity({ database: "thirdDB" })
export class Photo {
@PrimaryGeneratedColumn()
id: number
@Column()
url: string
}
User테이블은 secondDB에 생성되고 Photo는 thirdDB에 생성된다.
default는 우리가 datasource를 설정 할 때 작성한 DB에 생성된다.
다른 데이터베이스에서 데이터를 선택할려면 엔티티모델을 제공하면 된다.
const users = await dataSource
.createQueryBuilder()
.select()
.from(User, "user")
.addFrom(Photo, "photo")
.andWhere("photo.userId = user.id")
.getMany()
이 코드는 다음 SQL쿼리를 생성한다. (DB에 유형에 따라 다름)
SELECT * FROM "secondDB"."user" "user", "thirdDB"."photo" "photo"
WHERE "photo"."userId" = "user"."id"
엔티티 대신 테이블 경로를 지정할 수도있지만 mysql, mssql에서만 지원된다.
const users = await dataSource
.createQueryBuilder()
.select()
.from("secondDB.user", "user")
.addFrom("thirdDB.photo", "photo")
.andWhere("photo.userId = user.id")
.getMany()
여러 스키마를 사용하려면 shema
를 각 앤티티에 설정하기만 하면 된다.
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity({ schema: "secondSchema" })
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity({ schema: "thirdSchema" })
export class Photo {
@PrimaryGeneratedColumn()
id: number
@Column()
url: string
}
User엔티티는 secondShema내부에서 생성되고, photo앤티티는 thirdSchema에 생성된다.
TypeOrm을 사용하면 간단하게 DB Replication을 구현 할 수 있다.
{
type: "mysql",
logging: true,
replication: {
master: {
host: "server1",
port: 3306,
username: "test",
password: "test",
database: "test"
},
slaves: [{
host: "server2",
port: 3306,
username: "test",
password: "test",
database: "test"
}, {
host: "server3",
port: 3306,
username: "test",
password: "test",
database: "test"
}]
}
}
모든 스키마 업데이트 및 쓰기 작업은 master
서버를 사용하여 수행된다. find 또는 select로 수행되는(읽기) 쿼리는 레플리카에서 작업하는 걸 추천한다. 인스턴스 지정은 다음과 같이 할 수 있다.
const masterQueryRunner = dataSource.createQueryRunner("master")
try {
const postsFromMaster = await dataSource
.createQueryBuilder(Post, "post")
.setQueryRunner(masterQueryRunner)
.getMany()
} finally {
await masterQueryRunner.release()
}
마스터 인스턴스는 모든 일반적인 쓰기/읽기 상황에서 사용하면 된다.
읽기 작업은 레플리카에서 하는건 최적화를 위한 추천일 뿐이지 꼭 그래야되는건 아니다.
const slaveQueryRunner = dataSource.createQueryRunner("slave")
try {
const userFromSlave = await slaveQueryRunner.query(
"SELECT * FROM users WHERE id = $1",
[userId],
slaveQueryRunner,
)
} finally {
return slaveQueryRunner.release()
}
위 처럼 로우 쿼리를 이용한 조회나 find매서드처럼 읽기(read)작업의 경우 레플리카에서 수행하는 걸 추천한다.
{
replication: {
master: {
host: "server1",
port: 3306,
username: "test",
password: "test",
database: "test"
},
slaves: [{
host: "server2",
port: 3306,
username: "test",
password: "test",
database: "test"
}, {
host: "server3",
port: 3306,
username: "test",
password: "test",
database: "test"
}],
/**
* true면 연결이 실패 될 때 PoolCluster에서 다시 연결을 시도함
(Default: true)
*/
canRetry: true,
/**
* 연결이 실패하면 노드의 error카운트가 증가하는데
* errorCount가 removeNodeErrorCount보다 클 경우 노드를 제거함
* 즉 기본값 기준 5번 에러가 발생하면 노드 없앰
* (Default : 5)
*/
removeNodeErrorCount: 5,
/**
* 연결이 실해 할 경우 다시 연결을 위한 시간을 정함
* 0일 경우 바로 노드가 제거됌
*/
restoreNodeTimeout: 0,
/**
* 슬레이브 선택 방법을 결정함
* RR: 교대로 하나를 선택함 (Round-Robin).
* RANDOM: 랜덤으로 노드를 선택함
* ORDER: 사용 가능한 가장 앞 노드를 선택함
*/
selector: "RR"
}
}