Typescript

[Typescript] typeORM

sian han 2022. 12. 13. 15:37

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 없이 존재할 수 없다.

 

 

 

 

 

github : https://github.com/HAN-SEOHYUN/typeORM-study