TypeORM은 NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo 및 Electron 플랫폼에서 실행할 수 있고 TypeScript 및 JavaScript(ES5, ES6, ES7, ES8)와 함께 사용할 수 있는 ORM입니다 [ 공식문서]
▶ ORM 이란 ?
Object Relational Mapping (객체 관계 매핑)
: 데이터베이스를 사용하는 서비스를 객체지향적으로 구현하는데 큰 도움을 주는 도구이다
- 관계형 데이터베이스와 객체지향 프로그래밍언어의 중간에서 패러다임 일치를 시켜주기위한 기술이다
- 개발자는 객체지향적으로 프로그래밍을하고, ORM 이 관계형데이터베이스에 맞게 SQL을 대신 생성해서 실행한다
- ORM 을 통해 개발자는 더이상 SQL 에 종속적인 개발을 하지 않아도 된다.
▶ DB 연동하기
data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
import {Address} from './entity/Address';
import {User} from './entity/User';
export const AppDataSource = new DataSource({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "root",
password: "{password}",
database: "typeorm_study_db",
entities: [Address, User],
synchronize: true,
logging: true,
migrations: [],
subscribers: [],
});
index.ts
import { AppDataSource } from "./data-source";
import express from "express";
import userRouter from './routes/userRoutes'
AppDataSource.initialize().then(async () => {
// create express app
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// run app
app.listen(3000);
}).catch(error => console.log(error))
데이터베이스와의 초기 연결을 초기화하고, 모든 엔티티를 등록하고, 데이터베이스 스키마를 동기화하기 위해 initialize() 호출.
createConnection 은 구 DB 연결방식이므로 DataSource 사용 권장
▶ ORM 을 통해 테이블 생성하기
- @Entity : 데코레이터와 모델을 통해서 테이블을 생성할 수 있다.
- @Column : 데코레이터를 통해 컬럼 추가가 가능하다.
- @PrimaryColumn : 각 엔티티에는 PK 가 반드시 존재해야 한다.
PK를 시퀀스에 의해 자동생성된 값으로 설정하고 싶다면 @PrimaryGeneratedColumn 를 사용한다.
import {Entity, PrimaryGeneratedColumn, Column} from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn()
id : number;
@Column()
name : string;
@Column()
age :number;
}
▷ 컬럼의 자료형 지정하기
문자열은 기본적으로 varchar(255) 로 매핑된다. 자료형별 기본 매핑 값은 데이터베이스의 유형에 따라 다르다.
대표적으로 @Column 데코레이터의 / 매개변수로 컬럼의 자료형을 입력해 지정할 수 있다.
@Column("int")
@Column({ type: "int" })
@Column("varchar", { length: 200 })
@Column({ type: "int", width: 200 })
대표적인 자료형 지정 방법만 설명했으나
공식문서 에서 enum 유형, set 유형, simple-array 유형, simple-json 유형 등
기타 자료형을 지정하는 예시를 찾아볼 수 있다.
▶ DB 저장하기
▷ EntityManager 사용
공식문서예제.ts
import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"
const photo = new Photo()
photo.name = "Me and Bears"
photo.description = "I am near polar bears"
photo.filename = "photo-with-bears.jpg"
photo.views = 1
photo.isPublished = true
await AppDataSource.manager.save(photo) // EntityManager API 사용
console.log("Photo has been saved. Photo id is", photo.id)
새로운 객체를 생성하고 데이터를 매핑한다.
EntityManager 의 save() 메서드는 객체를 저장하고 auto_increment 된 pk(id : number) 를 리턴한다.
이렇듯 EntityManager API 를 통해 DB 정보를 관리할 수 있다.
▷ Repository 사용
각 엔티티는 엔티티관련 작업을 처리하는 Repository 를 가지고 있다.
EntityManager 는 모든 엔티티를 처리하고, Repository 는 단일 엔티티를 처리한다.
(둘은 같은 작업을 하는 것 같은데, 둘의 차이점이 뭔지 궁금함)
const userRepository = AppDataSource.getRepository(User);
다음과 같이 EntityManager 을 통해 Repository 에 접근할 수 있다.
UserController.ts
export async function saveUser (req: Request, res: Response){
await userRepository
.save(req.body) //DB저장
.then((user) => {
res.send(user); //저장된 정보 response
})
.catch((err) => console.log(err));
}
▶ 조회하기
▷ Id 로 조회하기
UserController.ts
export async function getUserById (req: Request, res: Response){
let inputId = parseInt(req.params.id);
await userRepository
.findOne({where: {id: inputId}})
.then((user) =>{
res.send(user);
console.log(user);
})
.catch((err) =>console.log(err));
}
▷ 전체 리스트 조회하기
UserController.ts
//get 전체 User
export async function getAllUser(req: Request, res:Response) {
await userRepository
.find()
.then((user) =>{
res.send(user);
console.log(user);
})
.catch((err) => console.log(err));
}
기타 Repository 의 find+@ 메서드
const allPhotos = await photoRepository.find()//find()
const firstPhoto = await photoRepository.findOneBy({ //findOneBy
id: 1,
})
const allViewedPhotos = await photoRepository.findBy({ views: 1 }) //findBy()
▶ 조건에 알맞은 데이터 update
▷ save() 메서드 사용 (공식문서 예제)
1. EntityManager 를 통해 객체의 Repository 에 접근
const photoRepository = AppDataSource.getRepository(Photo)
2. Repository 의 findOneBuy() 메서드를 이용해 조건에 알맞은 데이터 가져와서 변수 (photoToUpdate) 에 담기
const photoToUpdate = await photoRepository.findOneBy({
id: 1,
})
3. update 할 프로퍼티를 set
photoToUpdate.name = "Me, my friends and polar bears"
4. DB 에 저장
await photoRepository.save(photoToUpdate)
save() - DB 에 존재할 경우 insert, 이미 존재하면 update 가 됨.
Q. save() 메서드는 어떻게 존재여부를 체크해서 insert or update 쿼리를 날릴지 결정하는걸까 ?
A. pk 를 통해서 insert / update 를 결정하게된다.
Repository 의 findBy() 메서드를 통해 id 가 1인 데이터는 photoToupdate 변수에 매핑된다.
3번 과정을 통해 photoToUpdate 는 name 프로퍼티만 변경되었고, id 는 여전히 1의 값을 갖고있다.
save(photoToUpdate) 를 했을 때 EntityManger 는 DB에 id 가 1인 데이터가 존재하는지 확인하고,
이미 존재하니 update 쿼리를 날리게 되는 것이다.
▷ QueryBuilder 사용
UserController.ts
export async function updateUser(req: Request, res:Response) {
await userRepository
.createQueryBuilder()
.update(User)
.set(req.body)
.where({id: req.params.id})
.execute();
}
▶ 조건에 알맞은 데이터 remove
const photoRepository = AppDataSource.getRepository(Photo) //Repository 접근
const photoToRemove = await photoRepository.findOneBy({ // findById() 를 통해 id 가 1인 데이터 가져오기
id: 1,
})
await photoRepository.remove(photoToRemove) // 삭제
※ 연관관계
데코레이터(@)를 사용하여 엔티티 간의 연관관계를 맺을 수 있다.
▶ @OneToOne (단방향 / 양방향)
Address.ts
import {Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne} from 'typeorm'
import { User } from './User'
@Entity()
export class Address {
@PrimaryGeneratedColumn()
id: number
@Column()
detailAddress: string
@OneToOne(()=> User)//type() => Photo 는 관계를 맺고자 하는 엔티티의 클래스를 반환하는 함수이다. 가독성을 높이기 위해 () => Photo 형식을 사용했다.
@JoinColumn()
user : User //fk : user_id
}
@OneToOne 은 일대일 연관관계를 맺을 때 사용한다.
@joinColumn 데코레이션을 통해 관계의 소유자임을 명시할 수 있다.
(관계는 한쪽에서만 소유 가능. 소유자 측에서 @joinColumn 사용가능)
소유자는은 귀속된 엔티티의 PK 를 FK 로 갖게 된다.
ex ) Address 테이블 - 한명의 유저는 한개의 주소를 갖을 수 있는 경우
관계의 주인 Address 가 User의 pk 값을 fk 로 갖고 있는 것을 확인할 수 있다.
이렇게 @OneToOne 연관관계를 맺으면
관계의 소유자 Address 는 User 의 PK를 알게되어 User 의 정보를 알 수 있으나,
User 는 Address 에 접근할 수 있는 방법이 없다. (단방향매핑)
Address Repository 를 통해 findOneBy() 메서드를 사용하여 User_id 로 조회한 데이터를 들고오면 해결되겠지만,
User 에서 Address 에 바로 접근해서 데이터를 가져와야하는 상황이 생겼다고 가정해보자.
이 문제를 해결하기 위해 우선 두 엔티티의 연관관계를 양방향으로 바꿔주어야 한다.
코드를 다음과 같이 수정해보자.
관계의 소유자 Address.ts
@Entity()
export class Address {
/* ... other columns */
@OneToOne(() => User, (user) => user.address)
@JoinColumn()
user: User
}
- @OneToOne("user") 와 같이 간단히 문자열을 사용할 수 있다. 그러나 리팩토링을 더 쉽게 하기 위해 함수 접근 방식을 사용했다.
- 양방향 매핑에서도 역시 @joinColumn 데코레이터는 관계의 소유자가 되는 쪽에서만 사용할 수 있다.
관계의 귀속자 User.ts
@Entity()
export class User {
/* ... other columns */
@OneToOne(() => Address, (address) => address.user)
address: Address
}
양방향 매핑을 해서 User 에서도, Address 에서도 서로를 조회 할 수 있게되었다.
QueryBuilder 를 사용해 양쪽에서 조인쿼리를 날려 조회가 되는지 확인해보자.
▷ 조회
AddressController.ts
- Address 에서 User 접근
export async function getJoinedAddress(req: Request, res:Response) {
const address = await AppDataSource
.getRepository(Address)
.createQueryBuilder("address")
.leftJoinAndSelect("address.user", "user")
.getMany()
res.send(address);
}
UserController.ts
- User 에서 Address 접근
export async function getJoinedUser(req: Request, res:Response) {
const user = await AppDataSource
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.address", "address")
.getMany()
.then((user) => {
res.send(user);
})
.catch((err) => console.log(err));
}
양방향 매핑을 통해 서로를 조회가 되는 것을 확인할 수 있다.
▷ 저장
여기서 궁금증이 생겼는데 클래스를 멤버변수로 갖고있는 엔티티를 어떻게 DB 에 저장하느냐는 것이다.
공식문서에서 아래와 같은 방법을 찾을 수 있었다
const profile = new Profile()
profile.gender = "male"
profile.photo = "me.jpg"
await dataSource.manager.save(profile)
const user = new User()
user.name = "Joe Smith"
user.profile = profile
await dataSource.manager.save(user)
벗 이건 한번에 저장하는거고.. Profile 을 저장한 후 한참 나중에 User 를 저장하고싶으면 어쩌지 ? 라는 의문
여러가지를 시도해봤으나 해결이 안되서 JPA 사용했을 때의 경험을 더듬어 다음 코드를 작성했다.
AddressController.ts
export async function saveAddress(req:Request, res: Response){
//Address 객체생성
const address = new Address();
//get User 데이터
await userRepository
.findOne({where: {id: req.body.userId}})
.then((user) =>{
//set Address
if(user) address.user = user;
address.detailAddress = req.body.detailAddress;
})
.catch((err) =>console.log(err));
//save Address
await addressRepository
.save(address)
.then((address)=>{
res.send(address);
})
.catch((err)=> console.error(err));
}
의도한대로 등록이 된 것을 확인할 수 있다. 이게되네 .. 벗 썩 마음에 들지 않는다.
(User정보를 포함하고 있는) Address 를 등록하는 더 좋은 방법이 분명 있을거다.
▶ @OneToMany / @ManyToOne
한명의 사용자가 여러개의 주소를 가질 수 있다고 가정했을 때 ( 집, 회사, 학교 etc .. ) 다음과 같이 변경 가능하다.
관계의 소유자 Address.ts
@Entity()
export class Address {
/* ... other columns */
@ManyToOne(() => User, (user) => user.address)
user: User
}
@ManyToOne 데코레이션은 언제나 관계의 소유자 측에서 사용된다.
(@ManyToOne 이 이미 관계의 주인을 명시해줬기 때문에 @JoinColumn 생략이 가능하다)
@ManyToOne 을 사용하는 클래스가 관련 개채의 ID 를 저장한다는 의미이다.
관계의 귀속자 User.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@OneToMany(() => Address, (address) => address.user)
addresses: Address[]
}
@OneToMany 는 다른 쪽 엔티티의 @ManyToOne 없이 존재할 수 없다.
'Typescript' 카테고리의 다른 글
[Typescript] 이펙티브 타입스크립트 2장 아이템13 (0) | 2023.03.05 |
---|---|
[Typescript] 이펙티브 타입스크립트 2장 아이템10-12 (0) | 2023.02.27 |
[Typescript] 클래스 / 제네릭 / 유틸리티 타입 (0) | 2022.12.03 |
[Typescript] 리터럴 타입 / 유니온 타입 / 교차 타입 (0) | 2022.12.03 |
[Typescript] Type / interface 함수정의 / 함수 type 정의 (0) | 2022.12.03 |