[ 살펴보기 ] TypeORM - Basics

[ 살펴보기 ] TypeORM - Basics

·

5 min read

TypeORM은 Javascript & Typescript 기반 ORM library이며 Active Record와 Data Mapper pattern을 지원한다. Core team에 따르면 Hibernate와 Entity Framework에 영향을 많이 받았다고 하며 상당 기간 동안 library가 관리가 되지 않는 듯 보였으나 최근 들어 다시끔 관리를 해나가려는 움직임이 보인다.

해당 포스트를 통해 TypeORM의 기본 사용법을 살펴보자. 다음 command line을 통해 기본 typeorm project를 생성한다. command를 실행할 때 --name option을 통해 project name과 --database option을 통해 원하는 database를 설정해준다.

npx typeorm init --name test-typeorm --database postgres

위의 command를 실행하여 생성되는 project 구조는 다음과 같다.

MyProject
├── src                
│   ├── entity           
│   │   └── User.ts       
│   ├── migration         
│   ├── data-source.ts   
│   └── index.ts          
├── .gitignore       
├── package.json         
├── README.md            
└── tsconfig.json

그리고 data-source.ts file을 사용하는 database 정보에 맞게 수정해준다.

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "test",
  password: "test",
  database: "test",
  synchronize: true,
  logging: false,
  entities: [User],
  migrations: [],
  subscribers: [],
});

data-source.ts 파일의 정보를 수정하고 npm run start command를 통해 application를 실행하면 database에 user라는 table이 생성되고 data row 하나가 추가된 것을 확인할 수 있다. 새로운 data는 index.ts 파일에 의해 추가되고 user table 생성은 data-source.ts 파일의 entities에 추가한 object에 의해 생성된다. ( synchronize option이 true일 떄 )

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

AppDataSource.initialize()
  .then(async () => {
    const user = new User();
    user.firstName = "Timber";
    user.lastName = "Saw";
    user.age = 25;
    await AppDataSource.manager.save(user);
    const users = await AppDataSource.manager.find(User);
    console.log("Loaded users: ", users);
  })
  .catch((error) => console.log(error));

Entity를 생성해서 database table을 생성하는 방법을 조금 더 자세히 살펴보자. TypeORM에서 entity는 database의 table과 mapping되는 class다. 만약 database에서 product table이 필요하다면 다음과 같이 entity class를 구성할 수 있다.

src/entity/Product.ts

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"

@Entity()
export class Product {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @Column()
    price: number
}

위의 예제에서 볼 수 있듯이 class에 @Entity decorator를 추가해 entity를 명시하고 table의 column이 될 각 property에 @Column, @PrimaryGeneratedColumn와 같이 목적에 맞는 decorator를 추가한다. table column의 type은 기본적으로 class property의 type을 통해 추론되며 명시적으로 설정할 수 도 있다. TypeORM에서 사용할 수 있는 decorator의 보다 자세한 내용은 별개의 포스트에서 살펴보도록 하자.

위의 예제에서 생성한 entity를 아래와 같이 data-source.ts파일에 추가해주자.

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";
import { Product } from "./entity/Product";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "postgres",
  password: "1238917",
  database: "postgres",
  synchronize: true,
  logging: false,
  entities: [User, Product],
  migrations: [],
  subscribers: [],
});

그리고 다시 index.ts file에서 다음과 같이 product table에 data를 추가하는 code를 추가해준다.

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

AppDataSource.initialize()
  .then(async () => {
    const user = new User();
    user.firstName = "Timber";
    user.lastName = "Saw";
    user.age = 25;

    const product = new Product();
    product.name = "test product";
    product.price = 1000;

    await AppDataSource.manager.save(user);
    await AppDataSource.manager.save(product);

    const users = await AppDataSource.manager.find(User);
    const products = await AppDataSource.manager.find(Product);
    console.log("Loaded users: ", users);
    console.log("Loaded products: ", products);
  })
  .catch((error) => console.log(error));

이제 npm run start command를 통해 application을 실행해주면 entity로 추가한 product table과 data가 추가되는 것을 확인할 수 있다. 위의 예제에서 data 추가 및 조회를 위해 EntityManager를 사용했지만 아래 예제와 같이 repository를 통해서도 같은 작업을 할 수 있다.

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

AppDataSource.initialize()
  .then(async () => {
    const user = new User();
    user.firstName = "Timber";
    user.lastName = "Saw";
    user.age = 25;

    const product = new Product();
    product.name = "test product";
    product.price = 1000;

    const userRepository = AppDataSource.getRepository(User);
    const productRepository = AppDataSource.getRepository(Product);

    await userRepository.save(user);
    await productRepository.save(product);

    const users = await userRepository.find();
    const products = await productRepository.find();
    console.log("Loaded users: ", users);
    console.log("Loaded products: ", products);

  })
  .catch((error) => console.log(error));

Read data with repository

Data를 조회할 때는 단순히 find method 뿐만 아니라 다음과 같이 다양한 조회 method를 사용할 수 있다.

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

AppDataSource.initialize()
  .then(async () => {

    const userRepository = AppDataSource.getRepository(User);

    const users = await userRepository.find();

    const oneUserById = await userRepository.findOneBy({id:1});

    const usersByCity = await userRepository.findBy({city:"seoul"});

    const [users, userCount] = await userRepository.findAndCount();

  })
  .catch((error) => console.log(error));

Update data with repository

기존 data를 update할 때는 다음과 같이 수정하고자 하는 data를 우선 조회하고 수정한다.

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

AppDataSource.initialize()
  .then(async () => {

    const userRepository = AppDataSource.getRepository(User);
    const oneUserById = await userRepository.findOneBy({id:1});

    oneUserById.name = "name updated";
    await userRepository.save(oneUserById);

  })
  .catch((error) => console.log(error));

Delete data with repository

데이터 삭제 역시 다음과 같이 삭제 하고자 하는 data를 우선 조회하고 삭제한다.

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

AppDataSource.initialize()
  .then(async () => {

    const userRepository = AppDataSource.getRepository(User);
    const oneUserById = await userRepository.findOneBy({id:1});
    await userRepository.remove(oneUserById);

  })
  .catch((error) => console.log(error));

QueryBuilder

다소 복잡한 query를 요청해야 할 때는 queryBuilder를 통해 query를 구성할 수도 있다. 예를 들어 User entity와 Photo entity가 다음과 같다고 가정해보자.

User.ts

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[]
}

Photo.ts

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
}

그리고 Jack이라는 이름을 가진 user의 photo를 모두 read하고 싶다면 다음과 같이 queryBuilder를 구성할 수 있다.

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

AppDataSource.initialize()
  .then(async () => {

    const result = AppDataSource.getRepository(User)
                                .createQueryBuilder("user")
                                .leftJoinAndSelect("user.photos", "photo")
                                .where("user.name = :name", { name: "Jack" })
                                .getOne();
  })
  .catch((error) => console.log(error));

만약 user 이름이 Jack인 photo가 모두 3개라면 위의 queryBuilder의 결과는 대략 다음과 같을 것이다.

{
    id: 1,
    name: "Jack",
    photos: [{
        id: 1,
        url: "test1.jpg"
    }, {
        id: 2,
        url: "test2.jpg"
    },{
        id: 3,
        url: "test3.jpg"
    }]
}

그리고 위의 queryBuilder를 SQL statement로 표현하면 다음과 같다.

SELECT user.*, photo.* FROM users user
LEFT JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Jack'

위의 예제를 통해 살펴본 예제는 typeorm이 제공하는 기능의 일부다. 이제 각각의 기능을 개별 포스트를 통해 보다 자세히 살펴보자.