관리 메뉴

공부 기록장 💻

[NestJS/TypeORM] DataSource, Database Connection for TypORM 본문

# Tech Studies/NestJS

[NestJS/TypeORM] DataSource, Database Connection for TypORM

dream_for 2022. 10. 26. 23:01

- 공식문서 ( https://typeorm.io/data-source) 를 정리하며, Data Source를 이해해보자

DataSource란?

- 개발 환경 내에서 데이터베이스와 상호작용 하기 위해서는, Datasource를 먼저 설정해야 한다. TypeORM의 DataSource는 DB connection 설정을 유지하고, 사용하고 있는 RDBMS에 의지하여 connection pool 또는 초기 db 연결 상태를 초기 db connection을 구축한다.
- 초기 connection 또는 connection pool을 구축하기 위해서는, DataSrouce 객체의 initialize 메서드를 호출해야 한다.
- 연결 해제는 destroy 메서드로 실행한다.
- 일반적으로, 어플리케이션 부트스트랩에서 Datasource의 initialize 메서드를 호출하고, DB와 관련한 일을 모두 완료한 후에 destroy 한다. 그러나 실제로는, 백엔드 서버는 실행을 계속 유지하므로, DataSource를 destroy하는 경우는 존재하지 않는다.


DataSource 전역 변수로 생성 후 설정(setup)

- new DataSource 를 호출하여 생성자를 실행시킴으로써 초기화해야 한다. 전역 변수로 설정하도록 해야 한다.
- AppDataSource라는 이름으로 전역 DataSource 변수를 생성하여, 즉 전역으로 export할 수 있도록 만들어 어플리케이션 사이에서 이 인스턴스를 사용할 수 있게 된다.

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)
    })


- DataSource는 DataSourceOptions를 허용하는데, 사용하는 db가 무엇이느냐에 따라 옵션들을 다르게 적용하게 된다. (port 번호가 우선은 다르기 때문)
- Mysql과 Postgres 를 모두 사용하는 경우 다음과 같이 따로 DataSource 객체를 만들어주자.

import { DataSource } from "typeorm"

const MysqlDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",
    entities: [
        // ....
    ],
})

const PostgresDataSource = new DataSource({
    type: "postgres",
    host: "localhost",
    port: 5432,
    username: "test",
    password: "test",
    database: "test",
    entities: [
        // ....
    ],
})

DataSource 사용하기

- DataSource 정의를 한 후에, 앱 어디에서든 다음과 같이 사용할 수 있다.
- DataSource 객체를 사용하기 위해, manager 또는 getRepository() 옵션을 사용할 수 있는데 아래는 manager 옵션의 find() 메서드를 통해 모든 유저 정보를 가져오도록 하는 예시로 작성되었다.

import { AppDataSource } from "./app-data-source"
import { User } from "../entity/User"

export class UserController {
    @Get("/users")
    getAll() {
        return AppDataSource.manager.find(User)
    }
}

.

DataSourceOptions

- DataSourceOptions란, 새로운 DataSource 객체를 만든 후 필요한 설정이다. 서로 다른 RDBMS는 각자의 특수한 옵션들을 가지고 있다.

일반적인 data source options

- type: RDBMS type을 설정해야 하며, 이 옵션 설정은 필수적이다. 정확히 어떤 db 엔진을 사용할지 구체화해야 한다 (myslq, postgres, sqlite, maridb 등)
- extra: 기초 db driver가 있는 경우 설정하는 옵션
- entities: data source로 사용할 entity 또는 entity shcema를 설정한다. entity classes, entity schema classes 모두 허용된다. directory path로도 설정할 수 있으며, glob 패턴을 지원한다. (entites: [Post, Category, "entity/*.js", "modules/**/entity/*.js"] 와 같이 설정할 수 있다.)
- subscribers
- migrations
- logging: logging이 허용되는 경우 true로 설정 가능
- logger: logging의 목적 설정
- maxQueryExecutionTime: query가 주어진 최대 실행 시간을 넘어서면 (milliseconds 단위), logger가 이 쿼리를 로그하게 된다.
- poolSize: 활성화된 연결의 개수를 설정
- namingStrategy: db의 테이블과 컬럼의 이름을 정하는 전략 설정
- entityPrefix
- entitySkipConstructor
- dropSchema: data source가 초기화 될때마다 해당 스키마를 삭제한다 (production level에선 사용하지 않음. debug나 develop 단계에서 유용하게 사용됨)
- synchronize: 데이터베이스 스키마가 어플리케이션이 실행될 때마다 매번 자동적으로 생성되도록 설정. (마찬가지로 production level에선 사용하지 않는다.)

mysql, mariadb datasource 옵션

- url: 연결이 수행될 connection url
- host
- port: db 포트번호 (mysql default: 3306)
- username: db 유저
- password: db 비밀번호
- database: db 이름
- charset: 연결을 위한 charset 설정 (MySQL의 SQL레벨에서, utf8_neneral_ci을 비롯하여 collation 이라 부른다. default 값은 UTF8_GENERAL_CI로 설정)
- timezone: mysql에 설정할 timezone (default 값은 local)

예시로는 다음과 같이 작성할 수 있다.

{
    host: "localhost",
    port: 3306,
    username: "test",
    password: "test",
    database: "test",
    logging: true,
    synchronize: true,
    entities: [
        "entity/*.js"
    ],
    subscribers: [
        "subscriber/*.js"
    ],
    entitySchemas: [
        "schema/*.json"
    ],
    migrations: [
        "migration/*.js"
    ],
    cli: {
        entitiesDir: "entity",
        migrationsDir: "migration",
        subscribersDir: "subscriber"
    }
}


다수의 data source, db, schema, 복제 설정

다수의 데이터베이스, 그리고 한 개의 data soruce

- user 엔티티는 secondDB에서, Photo 엔티티는 thirdDB에서 생성된 테이블이라고 하면, 다음과 같이 작성할 수 있다.

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
}


- 만약 서로 다른 데이터베이스로부터 데이터를 조회하고자 하면, createqueryBuilder() 을 이용해 다음과 같이 where 조건으로 엔티티의 pk, fk값을 연결해주면 된다.

const users = await dataSource
    .createQueryBuilder()
    .select()
    .from(User, "user")
    .addFrom(Photo, "photo")
    .andWhere("photo.userId = user.id")
    .getMany() // userId is not a foreign key since its cross-database request

- 위의 쿼리 생성 문은, 실제 SQL 쿼리문으로 바꿔보자면 다음과 같다.

SELECT * FROM "secondDB"."user" "user", "thirdDB"."photo" "photo"
    WHERE "photo"."userId" = "user"."id"


다수의 스키마, 그리고 한 개의 data soruce

- 다수의 스키마(서로 다른 테이블)와 한 개의 data source(한 개의 데이터베이스 사용)을 사용하는 경우, 아래와 같이 Entity의 옵션으로 schema의 이름을 설정해주면 된다.

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
}


복제 (Replication)

- TypeORM을 이용해 DB의 복제본을 다음과같이 replication 옵션으로 master과 slaves로 분리할 수 있다.

{
  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,

    /**
    	연결 실패시, 노드의 errorCount가 증가한다.
        removeNodeErrorCount값보다 증가하면, PoolCluster에서 node를 제거한다. (default 값:5)
     */
    removeNodeErrorCount: 5,

    /**
    	연결 실패 시, 다른 새로운 연결이 시도되기 전 count할 milliseconds 시간을 구체화한다.
        0 으로 설정 시, 노드는 재사용될 일 없이 완전히 제거 됨 (default: 0)
     */
     restoreNodeTimeout: 0,

    /**
    	어떻게 slaves 가 선택될지를 결정함
        	- RR: Round-robin 방식으로 선택
            - RANDOM: 랜덤에 의해 선택
            - ORDER: 실행 가능한 첫번째 노드를 조건없이 선택 
     */
    selector: "RR"
  }
}

여기서 잠시 Replication이 무엇이냐 하면,

(https://jung-story.tistory.com/118 참고)

데이터베이스를 용도에 따라 Master과 Slave로 분리해 복제본을 만드는 것이다.
DB 데이터를 물리적으로 복사해 다른 곳에 넣어두는 기술을 말하는데, Master DB에서 데이터 변경이 일어난 경우(1) Master DB에 반영하고(2), 변경 이력을 Binary Log로 저장하고(3), 관련 이벤트를 Slave DB들에게 넘겨(4) Slave IO Thread에서 이벤트를 캐치하면(5) Binary Log를 Slave DB 각각의 Relay Log에 저장하며(6), Slave SQL Thread에서 Relay Log를 읽어(7) Slave DB까지도 업데이트를 하는(8).. 메커니즘에 바탕한 복제를 뜻한다.


- 일반적으로 Master & Slave구조를 이용해 DB에 대한 트래픽 분산을 위해 MySQL Replication을 통해 트래픽 집중 문제를 해결한다고 한다. Master에게는 데이터 동시성이 아주 높게 요구되는 트랜잭션 (Insert, Update, Delete) 을 담당하고, Slave에게는 읽기 전용으로 설정하여, 데이터를 가져오게 된다.

DataSource API

- 기본적인 옵션과 메서드들

const isInitialized: boolean = dataSource.isInitialized // dataSource, 초기 연결 초기화 되었는지
const driver: Driver = dataSource.driver // datasource의 db driver 설정

const manager: EntityManager = dataSource.manager // entity manager 사용
// you can call manager methods, for example find:
const users = await manager.find()

await dataSource.initialize() // data source 초기화, db에 연결 pool 오픈
await dataSource.destroy() // datasource 제거 및 모든 db 연결 해제
await dataSource.synchronize() // db schema 동시성
await dataSource.dropDatabase() // db drop 후 모든 데이터 삭제
await dataSource.runMigrations() // migration 실행

const repository = dataSource.getRepository(User) // table의 repository 가져오기
// now you can call repository methods, for example find:
const users = await repository.find()


- transaction() : 다수 db 요청에 대한 하나의 transaction 생성 (EntityManager 특별한 객체와 함께 수행)

await dataSource.transaction(async (manager) => {
    // NOTE: you must perform all database operations using given manager instance
    // its a special instance of EntityManager working with this transaction
    // and don't forget to await things here
})


- query() : raw query 실행

const rawData = await dataSource.query(`SELECT * FROM USERS`)


- createQueryBuilder() : 쿼리를 만드는데 사용 (예시: User 테이블로부터 name이 John인 데이터 모두 가져오기)

const users = await dataSource
    .createQueryBuilder()
    .select()
    .from(User, "user")
    .where("user.name = :name", { name: "John" })
    .getMany()


- createQueryRunner() : 하나의 db dataSource를 다루고 관리하기 위한 query runner

const queryRunner = dataSource.createQueryRunner()

// you can use its methods only after you call connect
// which performs real database connection
await queryRunner.connect()

// .. now you can work with query runner and call its methods

// very important - don't forget to release query runner once you finished working with it
await queryRunner.release()


728x90
반응형
Comments