Object Relational Mapping
객체지향 프로그래밍 언어와 데이터베이스 사이의 호환되지 않는 데이터를 변환해주는 시스템.
Typescript에서 사용하는 ORM
npm install typeorm @types/node mysql reflect-metadata
typeorm init --name <project_name> --database <database>
import 'reflect-metadata';
import { createConnection } from "typeorm";
import { Photo } from "./entity/Photo";
const Connection = async () => {
const connect = await createConnection({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "admin",
database: "test",
entities: [
Photo,
],
synchronize: true,
logging: false
});
return connect;
}
/* User Model */
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder('user')
.where('user.firstName = :firstName', { firstName })
.andWhere('user.lastName = :lastName', { lastName })
.getMany();
}
}
/* Main */
// save
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
await user.save();
// remove
await user.remove();
// load
const users = await User.find({ skip: 2, take: 5 });
const newUsers = await User.find({ isActive: true });
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });
const timber = await User.findByName('Timber', 'Saw');
/* User Model */
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
}
/* User Repository */
import { EntityRepository, Repository } from "typeorm";
import { User } from "../entity/User";
@EntityRepository()
export class UserRepository extends Repository<User> {
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder('user')
.where('user.firstName = :firstName', { firstName })
.andWhere('user.lastName = :lastName', { lastName })
.getMany();
}
}
/* Main */
import { User } from "../entity/User";
import { UserRepository } from "../repository/UserRepository";
// getRepository
const userRepository = connection.getRepository( User );
// save
const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.isActive = true;
await userRepository.save( user );
// remove
await userRepository.remove( user );
// load
const users = await userRepository.find({ skip: 2, take: 5 });
const newUsers = await userRepository.find({ isActive: true });
const timber = await userRepository.findOne({
firstName: "Timber",
lastName: "Saw"
});
const timber = await userRepository.findByName('Timber', 'Saw');
@Entity(options)
export class User {}
@Entity({
name: 'users',
engine: 'MyISAM',
database: 'example_dev',
schema: 'schema_with_best_tables',
synchronize: false,
orderBy: {
name: 'ASC',
id: 'DESC',
},
})
export class User {}
name | Table Name |
---|---|
database | 선택된 DB서버의 데이터베이스 이름 |
schema | Schema Name |
engine | 테이블 생성 중에 설정할 수 있는 DB엔진 이름 |
synchronize | false로 설정할 시 스키마 싱크를 건너뜁니다. |
orderBy | QueryBuilder와 find를 실행할 때 엔티티의 기본 순서를 지정합니다. |
// Abstract Class
abstract class Content {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
}
@Entity()
export class Photo extends Content {
@Column()
size: string;
}
@Entity()
export class Question extends Content {
@Column()
answersCount: number;
}
@Entity()
export class Post extends Content {
@Column()
viewCount: number;
}
@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
class Content {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
}
@ChildEntity()
export class Photo extends Content {
@Column()
size: string;
}
@ChildEntity()
export class Question extends Content {
@Column()
answersCount: number;
}
@ChildEntity()
export class Post extends Content {
@Column()
viewCount: number;
}
class Name {
@Column()
first: string;
@Column()
last: string;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: string;
@Column((type) => Name)
name: Name;
@Column()
isActive: boolean;
}
@ViewEntity({
expression: `
SELECT "post"."id" AS "id", "post"."name" AS "name", "category"."name" AS "categoryName"
FROM "post" "post"
LEFT JOIN "category" "category" ON "post"."categoryId" = "category"."id"
`
})
export class PostCategory {
@ViewColumn()
id: number;
@ViewColumn()
name: string;
@ViewColumn()
categoryName: string;
};
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 200, unique: true })
firstName: string;
@Column({ nullable: true })
lastName: string;
@Column({ default: false })
isActive: boolean;
}
type( ColumnType ) | js 원시타입들을 세분화해서 사용할 수 있다. |
---|---|
length( string | number ) | 최대 길이 설정 |
onUpdate( string ) | cascading을 하기 위한 옵션. ON UPDATE 트리거. |
nullable( boolean ) | false: NULL true : NOT NULL |
default( string ) | DEFAULT 값 |
unique( boolean ) | false: NOT UNIQUE true : UNIQUE |
enum( string[] | AnyEnum ) | |
enumName( string ) | |
transformer({ from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType }) |
Table간의 관계
JoinColumn()
을 사용한 필드는 FK(외래키)로 타겟 테이블에 등록된다.@JoinColumn()
은 반드시 한쪽 테이블에서만 사용해야 한다.@OneToOne()
을 사용해 관계를 나타낼 수 있다.@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: string;
@Column()
photo: string;
@OneToOne(() => User, (user) => user.profile)
user: User;
}
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne((type) => Profile, (profile) => profile.user)
@JoinColumn()
profile: Profile;
}
// using find* method
const userRepo = connection.getRepository(User);
const users = await userRepo.find({ relations: ['profile'] });
// using query builder
const users = await connection
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.getMany();
@OneToMany()
, @ManyToOne()
에서는 @JoinColumn()
을 생략할 수 있다.@OneToMany()
는 @ManyToOne()
이 없으면 안된다.@ManyToOne()
은 @OneToMany()
가 없어도 정의할 수 있다.@ManyToOne()
을 설정한 테이블에는 Relation ID
가 외래키를 가지고 있다.@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany((type) => Photo, (photo) => photo.user)
photos: Photo[];
}
@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@ManyToOne((type) => User, (user) => user.photos)
user: User;
}
// using find* method
const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ['photos'] });
// or from inverse side
const photoRepository = connection.getRepository(Photo);
const photos = await photoRepository.find({ relations: ['user'] });
// using query builder
const users = await connection
.getRepository(User)
.createQueryBuilder('user')
.leftJoinAndSelect('user.photos', 'photo')
.getMany();
// or from inverse side
const photos = await connection
.getRepository(Photo)
.createQueryBuilder('photo')
.leftJoinAndSelect('photo.user', 'user')
.getMany();
@ManyToMany()
관계에서는 @JoinTable()
이 반드시 필요하다.@JoinTable()
을 넣어주면 된다.@ManyToMany()
에서 옵션 cascade가 true인 경우, soft delete를 할 수 있습니다.@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
@Entity()
export class Question {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
text: string;
@ManyToMany(() => Category)
@JoinTable()
categories: Category[];
}
// using find* method
const questionRepository = connection.getRepository(Question);
const questions = await questionRepository.find({ relations: ['categories'] });
// using query builder
const questions = await connection
.getRepository(Question)
.createQueryBuilder('question')
.leftJoinAndSelect('question.categories', 'category')
.getMany();
1개의 테이블에서 부모-자식 관계를 나타낼 수 있는 패턴
- 상품 카테고리 ( 소, 중, 대 분류 )
- 사원 ( 사원, 관리자, 상위관리자 )
- 지역 ( 읍/면/동, 구/군, 시/도 )
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@ManyToOne((type) => Category, (category) => category.children)
parent: Category;
@OneToMany((type) => Category, (category) => category.parent)
children: Category[];
}
nested-set
이 들어간다.@Entity()
@Tree('nested-set')
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
materialized-path
가 들어간다.@Entity()
@Tree('materialized-path')
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
closure-table
이 들어간다.@Entity()
@Tree('closure-table')
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@TreeChildren()
children: Category[];
@TreeParent()
parent: Category;
}
@Entity()
export class Post {
@ManyToOne((type) => Category)
@JoinColumn({
name: 'category_id',
referencedColumnName: 'name',
})
category: Category;
}
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
@Entity()
export class Question {
@ManyToMany((type) => Category)
@JoinTable({
name: 'question_categories',
joinColumn: {
name: 'question',
referencedColumnName: 'id',
},
inverseJoinColumn: {
name: 'category',
referencedColumnName: 'id',
},
})
categories: Category[];
}
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}
// using many to one
@Entity()
export class Post {
@ManyToOne((type) => Category)
category: Category;
@RelationId((post: Post) => post.category)
categoryId: number;
}
// using many to many
@Entity()
export class Post {
@ManyToMany((type) => Category)
categories: Category[];
@RelationId((post: Post) => post.categories)
categoryIds: number[];
}
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(connection: Connection) {
connection.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>): Promise<any> | void {
console.log('User 테이블에 입력 전 : ', event.entity);
}
afterInsert(event: InsertEvent<User>): Promise<any> | void {
console.log('User 테이블에 입력 후 : ', event.entity);
}
}
// using with single column
@Entity()
export class User {
@Index()
@Column()
firstName: string;
@Index({ unique: true })
@Column()
lastName: string;
}
// using with entity
@Entity()
@Index(['firstName', 'lastName'], { unique: true })
export class User {
@Column()
firstName: string;
@Column()
lastName: string;
}
@Entity()
@Unique(['firstName', 'lastName'])
export class User {
@Column()
firstName: string;
@Column()
lastName: string;
}
@Entity()
@Check('"age" > 18')
export class User {
@Column()
firstName: string;
@Column()
firstName: string;
@Column()
age: number;
}
DB 내에서 하나의 그룹으로 처리해야하는 명령문을 모아 처리하는 작업의 단위.
- Transaction의 의미
- 여러 단계의 처리를 하나의 처리처럼 다루는 기능이다.
- 여러 개의 명령어의 집합이 정상적으로 처리되면 정상종료된다.
- 하나의 명령어라도 잘 못 되었다면 전체 취소 된다.
- 트랜잭션을 쓰는 이유는 데이터의 일관성을 유지하고, 안정적으로 데이터를 복구하기 위함이다.
- 격리성 수준 설정을 통해 트랜잭션이 열려있는 동안
외부에서 해당 데이터에 접근하지 못하도록 락을 걸 수 있다.
격리성 수준 | 설명 |
---|---|
Read Uncommitted | 트랜잭션에서 처리 중인 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 것을 허용 |
Read Committed | 트랜잭션이 커밋되어 확정된 데이터만 다른 트랜잭션이 읽도록 허용 |
Repeatable Read | 트랜잭션에서 삭제, 변경에 대해서 Undo 로그에 넣어두고, 앞서 발생한 트랜잭션에 대해서는 실제 데이터가 아닌 Undo 로그에 있는 백업데이터를 읽음. |
Serializable Read | 트랜잭션에서 쿼리를 두 번 이상 수행할 때, 첫 번째 쿼리에 있던 레코드가 사라지거나 값이 바뀌지 않음은 물론 새로운 레코드가 나타나지도 않도록 하는 설정. |
/* Decorator */
// using transaction manager
@Transaction({ isolation: 'SERIALIZABLE' })
save(@TransactionManager() manager: EntityManager, user: User) {
return manager.save(user)
}
// using transaction repository
@Transaction({ isolation: 'SERIALIZABLE' })
save(user: User, @TransactionRepository(User) userRepository: Repository<User>) {
return userRepository.save(user)
}
/**
* queryRunner
* - 격리성 수준 설정이 불가능
* - startTransaction: 트랜잭션 시작 Method
* - commitTransaction: 모든 변경사항을 커밋하는 Method
* - rollbackTransaction: 모든 변경사항을 되돌리는 Method
*/
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(user);
await queryRunner.manager.save(photos);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
@ManyToMany(
type => Category,
category => category.questions,
{ eager: true }
)
@ManyToMany(type => Question, question => question.categories)
questions: Promise<Question[]>;
const question = await connection.getRepository(Question).findOne(1);
const categories = await question.categories;
https://gmlwjd9405.github.io/2019/02/01/orm.html