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이 제공하는 기능의 일부다. 이제 각각의 기능을 개별 포스트를 통해 보다 자세히 살펴보자.