TypeORM Start

ClassBinu·2024년 5월 18일

F-lab

목록 보기
24/65

멘토님의 한 마디

TypeORM 공식 문서 안 보셨죠?

질문의 수준에서 티가 난 듯 😂

공식문서 읽기 시작!
https://typeorm.io/

기본 개념

  • DataSource
  • Entity
  • Relations
  • Entity Manager and Repository
  • Query Builder

빠른 시작

이건 테이블은 생성되지만, 컬럼은 생성 안 됨.

import { Entity } from "typeorm"

@Entity()
export class Photo {
    id: number
    name: string
    description: string
    filename: string
    views: number
    isPublished: boolean
}

이렇게 해야지 필드와 컬럼이 매칭된다.

import { Entity, Column } from "typeorm"

@Entity()
export class Photo {
    @Column()
    id: number

    @Column()
    name: string

    @Column()
    description: string

    @Column()
    filename: string

    @Column()
    views: number

    @Column()
    isPublished: boolean
}

기본키는 반드시 있어야 한다고 한다. 예전에 멘토님께 질문했던 내용에 대한 답을 공식문서에서 찾았음.

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

@Entity()
export class Photo {
    @PrimaryColumn()
    id: number

    @Column()
    name: string

    @Column()
    description: string

    @Column()
    filename: string

    @Column()
    views: number

    @Column()
    isPublished: boolean
}

TypeORM을 사용하면, 개발자는 객체 지향적인 방식으로 데이터베이스 스키마를 정의하고 관리할 수 있으며, SQL 쿼리를 직접 작성하는 번거로움 없이 데이터베이스 작업을 수행할 수 있음.

엔터티 관리자

EntityManager는 앱의 모든 엔터티를 조작할 수 있다!

import { Photo } from "./entity/Photo"
import { AppDataSource } from "./index"

const savedPhotos = await AppDataSource.manager.find(Photo)
console.log("All photos from the db: ", savedPhotos)

리포지토리

리포지토리는 특정 엔터티의 자체 저장소
엔터티 매니저로 앱을 명시해서 사용할 수 있지만, 하나의 테이블은 자신만으 레포지토리를 가질 수 있고 이걸 사용하면 해당 엔터티 조작이 더 편리해진다.

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

const photoRepository = AppDataSource.getRepository(Photo)

await photoRepository.save(photo)
console.log("Photo has been saved")

const savedPhotos = await photoRepository.find()
console.log("All photos from the db: ", savedPhotos)

One to One

@OneToOne(() => Photo)
    @JoinColumn()
    photo: Photo
  • 언어 특성으로 클래스를 직접 사용하지 않고 클래스를 반환하는 함수를 사용함.
  • JoinColumn()은 Owner side에서 가지고 있음.

외래키를 가지고 있는 쪽이 owner side

이런 식으로 양 쪽에 다 명시해 주면 양방향으로 관계를 맺을 수 있다.
Owner side에서는 외래키를 가지고 있음. 다른 한 쪽은 관계를 참조만 함.
반대는 Inverse side

@Entity()
// 첫 번째 파라미터 () => Photo는 관계의 대상 엔터티 타입을 지정
// 두 번째 파라미터 (photo) => photo.metadata는 관계의 반대쪽에서 현재 엔터티를 참조하는 프로퍼티를 지정
export class PhotoMetadata {
    /* ... other columns */

    @OneToOne(() => Photo, (photo) => photo.metadata)
    @JoinColumn()
    photo: Photo
}

@Entity()
export class Photo {
    /* ... other columns */

    @OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo)
    metadata: PhotoMetadata
}

근데 ESM 쓰고 있으면 순환 참조 문제 있음

이렇게 바꿔야 함

//
@OneToOne(() => Photo, (photo) => photo.metadata)
@JoinColumn()
photo: Relation<Photo> // 여기

//
@OneToOne(() => PhotoMetadata, (photoMetadata) => photoMetadata.photo)
metadata: Relation<PhotoMetadata> // 여기

조인 테이블 불러오기

방법1. find

const photoRepository = AppDataSource.getRepository(Photo)
const photos = await photoRepository.find({
    relations: {
        metadata: true,
    },
})

간단한게는 find 메서드로 해도 됨. 근데 복잡한 쿼리는 쿼리 빌더 쓰면 됨.

TypeORM에서 relations: { metadata: true } 옵션
특정 엔터티를 조회할 때 해당 엔터티와 관계가 설정된 다른 엔터티들도 함께 로드하도록 지정하는 설정

방법2. QueryBuilder

const photos = await AppDataSource.getRepository(Photo)
    .createQueryBuilder("photo")
    .innerJoinAndSelect("photo.metadata", "metadata")
    .getMany()

casecade

하나의 객체가 저장될 때 연관된 다른 객체도 자동적으로 저장되게 할 수 있음!

@OneToOne(() => PhotoMetadata, (metadata) => metadata.photo, {
        cascade: true,
    })
    metadata: PhotoMetadata

이렇게 하면 사진과 메타데이터 관계에서 메타테이터는 리포지토리로 save를 호출하지 않고,
photo.metadata에 넣어놓고, Photo만 저장하면 casecade된 메타데이터도 저장됨.

만약 이렇게 안하면 메타데이터를 저장해서 FK를 받고, 그걸 Photo에 넣어주는 번거로움이 있음.

Many to one / one to many

한 명의 작가는 다수의 사진을 가질 수 있다.

//
@Entity()
export class Author {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @OneToMany(() => Photo, (photo) => photo.author) // note: we will create author property in the Photo class below
    photos: Photo[]
}

// 항상 ManyToOne이 Owner Side가 됨
@Entity()
export class Photo {
    /* ... other columns */

    @ManyToOne(() => Author, (author) => author.photos)
    author: Author
}

OneToMany 관계가 설정된 엔터티는 반드시 ManyToOne 관계가 설정된 엔터티와 연결되어야 합니다. 이는 OneToMany가 단순히 관계를 맺고 있는 엔터티를 참조하는 목록을 유지하기 때문입니다. 실제 외래 키 관리와 관계의 정의는 ManyToOne 쪽에서 이루어집니다.

Many to Many

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

    @Column()
    name: string

    @ManyToMany(() => Photo, (photo) => photo.albums)
    @JoinTable()
    photos: Photo[]
}

// inverse side
export class Photo {
    // ... other columns

    @ManyToMany(() => Album, (album) => album.photos)
    albums: Album[]
}

헐. M2M의 접합 테이블을 보고 그 동안 의문이 완전 해소됨.
정규화를 해야 하는 게 어떻게 필드를 배열로 관리하지? 했는데 접합 테이블에서 로우를 하나씩 쌓아가는 거였음.

쿼리 빌더

복잡한 쿼리를 SQL로 짜지 않고 빌더로 프로그래밍적으로 생성할 수 있음.

const photos = await AppDataSource.getRepository(Photo)
    .createQueryBuilder("photo") // first argument is an alias. Alias is what you are selecting - photos. You must specify it.
    .innerJoinAndSelect("photo.metadata", "metadata")
    .leftJoinAndSelect("photo.albums", "album")
    .where("photo.isPublished = true")
    .andWhere("(photo.name = :photoName OR photo.name = :bearName)")
    .orderBy("photo.id", "DESC")
    .skip(5)
    .take(10)
    .setParameters({ photoName: "My", bearName: "Mishka" })
    .getMany()

0개의 댓글