Octoping의 블로그
article thumbnail

들어가기 앞서

해당 글은 Prisma의 공식 문서인 해당 글을 번역하였습니다.
https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm

 

Prisma vs TypeORM

Learn how Prisma compares to TypeORM.

www.prisma.io

 

TypeORM vs Prisma

PrismaTypeORM은 비슷한 문제를 해결하지만, 작동하는 방식은 꽤 다릅니다.

 

TypeORM은 테이블을 모델 클래스에 매핑하는 전통적인 ORM입니다. 이러한 모델 클래스는 SQL 마이그레이션을 생성하는 데 사용할 수 있습니다. 그런 다음 모델 클래스의 인스턴스는 런타임에 애플리케이션에 CRUD 쿼리를 위한 인터페이스를 제공합니다.

 

Prisma는 모델 인스턴스의 비대해짐, 비즈니스 로직과 DB 로직의 혼합, 타입 안전성의 부족, 지연 로딩으로 인한 예측 불가능한 쿼리 등 기존 ORM의 여러 문제를 완화하는 새로운 종류의 ORM입니다.

 

Prisma는 Prisma 스키마를 사용하여 선언적 방식으로 애플리케이션 모델을 정의합니다. 그런 다음 Prisma 마이그레이트는 Prisma 스키마에서 SQL 마이그레이션을 생성하고 데이터베이스에 대해 이를 실행할 수 있습니다. CRUD 쿼리는 가볍고 완전히 유형이 안전한 Node.js 및 TypeScript용 데이터베이스 클라이언트인 Prisma Client에서 제공합니다.

 

API 설계 & 추상화 단계

TypeORM과 Prisma는 서로 다른 추상화 단계에서 작동합니다. TypeORM은 API에서 SQL을 미러링하는 것에 가까운 반면, Prisma 클라이언트는 애플리케이션 개발자의 일반적인 작업을 염두에 두고 신중하게 설계된 더 높은 수준의 추상화를 제공합니다. Prisma의 API 설계는 올바른 일을 쉽게 하자는 아이디어에 크게 의존합니다.

 

Prisma Client는 더 높은 수준의 추상화에서 작동하지만, 기본 데이터베이스의 모든 기능을 노출하기 위해 노력하며 유스케이스에 필요한 경우 언제든지 원시 SQL로 드롭다운할 수 있습니다.

 

다음 섹션에서는 특정 시나리오에서 Prisma와 TypeORM의 API가 어떻게 다른지, 그리고 이러한 경우 Prisma의 API 설계의 근거가 무엇인지에 대한 몇 가지 예를 살펴봅니다.

 

필터링

TypeORMfind 메소드와 같이, 주로 SQL 연산자를 사용해서 리스트와 레코드들을 필터링합니다. 반면에 Prisma는 직관적으로 사용할 수 있는 더욱 일반적인 연산자들의 모음을 제공합니다.

 

또한 아래의 타입 안전성 섹션에서 설명할 것 처럼 TypeORM은 많은 시나리오에서 필터 쿼리를 사용할 때 타입 안전성을 잃는다는 점을 유의해야 합니다.

 

string 필터를 살펴보는 것은 TypeORM과 Prisma의 필터링 API가 어떻게 다른지 알 수 있는 좋은 예시입니다. TyprORM은 주로 SQL에서 제공하는 ILike 연산자를 기반으로 필터를 제공하는 반면, Prisma는 contains, startsWith, endsWith과 같이 개발자가 사용할 수 있는 보다 구체적인 연산자를 제공합니다.

 

// Prisma
const posts = await postRepository.find({
  where: {
    title: 'Hello World',
  },
})

// TypeORM
const posts = await postRepository.find({
  where: {
    title: ILike('Hello World'),
  },
})
// Prisma
const posts = await postRepository.find({
  where: {
    title: { contains: 'Hello World' },
  },
})

// TypeORM
const posts = await postRepository.find({
  where: {
    title: ILike('%Hello World%'),
  },
})
// Prisma
const posts = await postRepository.find({
  where: {
    title: { startsWith: 'Hello World' },
  },
})

// TypeORM
const posts = await postRepository.find({
  where: {
    title: ILike('Hello World%'),
  },
})
// Prisma
const posts = await postRepository.find({
  where: {
    title: { endsWith: 'Hello World' },
  },
})

// TypeORM
const posts = await postRepository.find({
  where: {
    title: ILike('%Hello World'),
  },
})

 

페이지네이션

TypeORM은 limit-offset 페이지네이션만 제공하는 반면, Prisma는 limit-offset과 커서 기반 모두에 대한 전용 API를 편리하게 제공합니다.

두 가지 접근 방식에 대한 자세한 내용은 Prisma 공식문서의 Pagination 섹션과 아래의 API 비교에서 찾아볼 수 있습니다.

 

연관관계

외래 키(FK)를 통해 연결된 레코드에서 작업하는 것은 SQL을 굉장히 복잡하게 만들 수 있습니다. Prisma의 가상 관계 필드라는 개념은 애플리케이션 개발자가 관련 데이터로 직관적이고 편리하게 작업할 수 있도록 해줍니다.

 

Prisma의 이런 접근 방식의 장점은 다음과 같습니다.

  • 유연한 API를 통한 연관관계 탐색 (문서)
  • 연결된 레코드를 CREATE / UPDATE 할 수 있는 중첩 쓰기 (문서)
  • 관련된 레코드에 대한 필터 적용 (문서)
  • JOIN에 대한 걱정 없이 중첩된 데이터를 쉽고 타입 안전하게 조회하기 (문서)
  • 모델과 그 관계를 기반으로 중첩된 Typescript 타이핑 생성 (문서)
  • 관계 필드를 통해 데이터 모델에서 직관적인 관계 모델링 (문서)
  • 관계 테이블 (Join, link, pivot, junction table) (다대다 관계)의 암시적 처리 (문서)

 

데이터 모델링과 마이그레이션

TypeORM은 클래스와 실험적인 Typescript 데코레이터를 사용해서 모델을 정의하는 반면, Prisma 모델은 Prisma 스키마를 이용해서 정의됩니다. 액티브 레코드 패턴을 사용할 때, TypeORM의 이런 접근은 종종 애플리케이션이 성장함에 따라 유지보수가 어려워지는 복잡한 모델 인스턴스로 이어집니다.

 

반면 Prisma는 액티브 레코드가 아니라 데이터 매퍼 패턴에 따라 Prisma 스키마에 정의된 모델의 데이터를 읽고 쓸 수 있도록 완전히 타입 안전한 맞춤형 API를 노출하는 경량의 데이터베이스 클라이언트를 생성합니다.

 

Prisma의 데이터 모델링 용 DSL은 간결하고 간단하고 직관적으로 사용할 수 있습니다. Visual Studio Code에서 데이터를 모델링할 때 자동완성, 빠른 수정, 정의로 이동 및 개발자 생산성을 높이는 등의 기능을 갖춘 확장 기능도 활용할 수 있습니다.

 

// Prisma
model User {
  id    Int     @id @default(autoincrement())
  name  String?
  email String  @unique
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  authorId  Int?
  author    User?   @relation(fields: [authorId], references: [id])
}
// TypeORM
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  OneToMany,
  ManyToOne,
} from 'typeorm'

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

  @Column({ nullable: true })
  name: string

  @Column({ unique: true })
  email: string

  @OneToMany((type) => Post, (post) => post.author)
  posts: Post[]
}

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

  @Column()
  title: string

  @Column({ nullable: true })
  content: string

  @Column({ default: false })
  published: boolean

  @ManyToOne((type) => User, (user) => user.posts)
  author: User
}

 

마이그레이션은 TypeORM과 Prisma에서 비슷한 방식으로 작동합니다. 두 도구 모두 제공된 모델 정의를 기반으로 SQL 파일을 생성하고 데이터베이스에 대해 실행할 수 있는 CLI를 제공하는 접근 방식을 따릅니다. 마이그레이션이 실행되기 전에 SQL 파일을 수정할 수 있으므로 두 마이그레이션 시스템 모두에서 사용자 지정 데이터베이스 작업을 수행할 수 있습니다.

 

타입 안전성

TypeORM은 Node.js 생태계에서 TypeScript를 완전히 수용한 최초의 ORM 중 하나이며, 개발자가 데이터베이스 쿼리에 대해 일정 수준의 타입 안전성을 확보할 수 있도록 하는 데 큰 역할을 해왔습니다.

 

하지만 TypeORM의 타입 안전성 보장이 부족한 상황도 많습니다. 다음 섹션에서는 Prisma가 쿼리 결과의 타입에 대해 더 강력한 보장을 제공할 수 있는 시나리오를 설명합니다.

 

필드 조회

이 섹션에서는 쿼리에서 모델 필드의 하위 집합을 선택할 때 타입 안전성의 차이점에 대해 설명합니다.

 

TypeORM

TypeORM은 find, findByIds, findOne 등과 같은 메소드로 select 옵션을 제공합니다.

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
  where: { published: true },
  select: ['id', 'title'],
})

 

반환된 Post 배열의 각 객체는 런타임에 선택된 id 및 title 속성만 전달하지만 TypeScript 컴파일러는 이에 대해 전혀 알지 못합니다. 예를 들어 쿼리 후에 Post 엔티티에 정의된 다른 프로퍼티에 다음과 같이 접근할 수 있습니다.

const post = publishedPosts[0]

// The TypeScript compiler has no issue with this
if (post.content.length > 0) {  // TypeError: Cannot read property 'length' of undefined
  console.log(`This post has some content.`)
}

 

TypeScript 컴파일러는 반환된 객체의 Post 타입만 확인하지만 런타임에 이러한 객체가 실제로 가지고 있는 필드에 대해서는 알지 못합니다. 따라서 데이터베이스 쿼리에서 검색되지 않은 필드에 액세스하지 못하도록 보호할 수 없어 런타임 오류가 발생합니다.

 

Prisma

Prisma 클라이언트는 동일한 상황에서 완전한 타입 안전을 보장하고 데이터베이스에서 검색되지 않은 필드에 액세스하지 못하도록 보호합니다. 다음은 완전히 똑같은 예제입니다.

const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  select: {
    id: true,
    title: true,
  },
})

const post = publishedPosts[0]

// The TypeScript compiler will not allow this
if (post.content.length > 0) {
  console.log(`This post has some content.`)
}

 

이 예제에서 TypeScript 컴파일러는 컴파일 단계에서 에러를 발생시킵니다. Prisma 클라이언트가 이 쿼리에 대한 리턴 타입을 생성하기 때문입니다. 이 예제에서, publishedPost의 타입은 다음과 같습니다.

const publishedPosts: {
  id: number
  title: string
}[]

 

따라서 쿼리에서 검색되지 않은 모델의 속성에 실수로 접근하는 것은 불가능합니다.

 

관계 로딩

이 섹션에서는 쿼리에서 모델의 관계를 로드할 때 타입 안전성의 차이점에 대해 설명합니다. 기존 ORM에서는 이를 즉시 로딩(eager loading)이라고 부르기도 합니다.

 

TypeORM

TypeORM을 사용하면 find 메소드에 전달할 수 있는 relations 옵션을 통해 데이터베이스에서 관계를 '즉시 로딩' 할 수 있습니다.

 

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
  where: { published: true },
  relations: ['author'],
})

select와 다르게, TypeORM은 자동 완성을 제공하지 않으며 relations 옵션으로 전달하는 문자열에 대해 타입 안전성을 제공하지 않습니다. 즉, Typescript 컴파일러는 이런 관계를 쿼리할 때 발생하는 오타를 포착할 수 없습니다.

 

const publishedPosts: Post[] = await postRepository.find({
  where: { published: true },
  // this query would lead to a runtime error because of a typo
  relations: ['authors'],
})

// UnhandledPromiseRejectionWarning: Error: Relation "authors" was not found; please check if it is correct and really exists in your entity.

 

Prisma

Prisma는 이런 실수로부터 사용자를 보호하여 런타임에 애플리케이션에서 발생할 수 있는 모든 종류의 오류를 제거합니다. include를 사용하여 Prisma 클라이언트 쿼리에서 관계를 로드할 때 자동 완성 기능을 활용하여 쿼리를 지정할 수 있을 뿐만 아니라 쿼리 결과도 올바르게 입력됩니다.

 

const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  include: { author: true },
})

// published Post의 타입도 다음과 같이 생성됩니다!
const publishedPosts: (Post & {
  author: User
})[]

 

참고로, Prisma 클라이언트가 Prisma 모델에 대해 생성하는 User 및 Post 타입은 다음과 같습니다.

// Generated by Prisma
export type User = {
  id: number
  name: string | null
  email: string
}

// Generated by Prisma
export type Post = {
  id: number
  title: string
  content: string | null
  published: boolean
  authorId: number | null
}

 

필터링

이 섹션에서는 where를 사용하여 레코드 목록을 필터링할 때 타입 안전성의 차이점에 대해 설명합니다.

 

TypeORM

TypeORM에서는 특정 기준에 따라 반환된 레코드 목록을 필터링하기 위해 where 옵션을 find 메서드에 전달할 수 있습니다. 이러한 기준은 모델의 프로퍼티와 관련하여 정의할 수 있습니다.

 

operator를 사용해서 타입 안전성 느슨하게 하기

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
  where: {
    published: true,
    title: ILike('Hello World'),
    views: MoreThan(0),
  },
})

이 코드는 제대로 실행되고 런타임에 유효한 쿼리를 생성합니다. 그러나 where 옵션은 다양한 시나리오에서 실제로 타입 안전하지 않습니다. 특정 타입(ILike는 문자열, MoreThan은 숫자)에만 작동하는 ILike 또는 MoreThan과 같은 FindOperator를 사용하는 경우 모델 필드에 올바른 타입을 제공한다는 보장을 잃게 됩니다.

 

예를 들어 MoreThan 연산자에 문자열을 제공할 수 있습니다. TypeScript 컴파일러는 에러를 감지하지 못하며 애플리케이션은 런타임에 실패합니다.

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
  where: {
    published: true,
    title: ILike('Hello World'),
    views: MoreThan('test'),
  },
})

// error: error: invalid input syntax for type integer: "test"

 

존재하지 않는 프로퍼티 특정하기

또한 TypeScript 컴파일러를 사용하면 모델에 존재하지 않는 프로퍼티를 where 옵션에 지정할 수 있으므로 런타임 오류가 다시 발생할 수 있습니다.

const publishedPosts: Post[] = await postRepository.find({
  where: {
    published: true,
    title: ILike('Hello World'),
    viewCount: 1,
  },
})

// EntityColumnNotFound: No entity column "viewCount" was found.

 

Prisma

TypeORM에서 타입 안전성 측면에서 문제가 되는 두 가지 필터링 시나리오는 Prisma에서 완전히 타입 안전 방식으로 처리됩니다.

 

타입 안전성이 보장된 연산자 사용

Prisma를 사용하면 TypeScript 컴파일러가 필드별로 연산자를 올바르게 사용하도록 강제합니다.

const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: 'Hello World' },
    views: { gt: 0 },
  },
})

 

Prisma 클라이언트에서는 위에 표시된 것과 동일한 문제가 있는 쿼리를 지정할 수 없습니다.

const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: 'Hello World' },
    views: { gt: 'test' }, // Caught by the TypeScript compiler
  },
})

 

모델 속성으로 필터의 타입 안전 정의

TypeORM을 사용하면 where 옵션에 모델 필드에 매핑되지 않는 속성을 지정할 수 있습니다. 따라서 위의 예에서 viewCount에 대해 필터링하면 필드가 실제로는 views라고 호출되기 때문에 런타임 오류가 발생했습니다.

 

Prisma를 사용하면 TypeScript 컴파일러는 모델에 존재하지 않는 where 내부의 프로퍼티를 참조할 수 없습니다.

const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: 'Hello World' },
    viewCount: { gt: 0 }, // Caught by the TypeScript compiler
  },
})

 

새 레코드 생성하기

이 섹션에서는 새 레코드를 생성할 때 타입 안전성의 차이점에 대해 설명합니다.

 

TypeORM

TypeORM을 사용하면 데이터베이스에 새 레코드를 생성하는 두 가지 주요 방법, 즉 insertsave가 있습니다. 두 가지 방법 모두 개발자가 필수 필드를 제공하지 않을 경우 런타임 오류가 발생할 수 있는 데이터를 제출할 수 있습니다.

이 예시를 살펴보겠습니다.

const userRepository = getManager().getRepository(User)
const newUser = new User()
newUser.name = 'Alice'
userRepository.save(newUser)

 

TypeORM으로 레코드를 생성할 때 save 또는 insert를 사용하든 관계없이 필수 필드에 대한 값을 제공하는 것을 잊어버리면 런타임 오류가 발생합니다.

 

Prisma

Prisma는 모델의 모든 필수 필드에 대한 값을 제출하도록 강제하여 이러한 종류의 실수를 방지합니다.

예를 들어, 필수 이메일 필드가 누락된 새 사용자를 생성하려는 다음 시도는 TypeScript 컴파일러에 의해 포착될 수 있습니다.

const newUser = await prisma.user.create({
  data: {
    name: 'Alice',
  },
})


// [ERROR] 10:39:07 ⨯ Unable to compile TypeScript:
// src/index.ts:39:5 - error TS2741: Property 'email' is missing in type '{ name: string; }' but required in type 'UserCreateInput'.

 

API 비교

단일 오브젝트 가져오기

Prisma

const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
})

TypeORM

const userRepository = getRepository(User)
const user = await userRepository.findOne(id)

 

Fetching selected scalars of single objects

Prisma

const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
  select: {
    name: true,
  },
})

TypeORM

const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
  select: ['id', 'email'],
})

 

관계 가져오기

Prisma

// using include
const posts = await prisma.user.findUnique({
  where: {
    id: 2,
  },
  include: {
    post: true,
  },
})

// fluent api
const posts = await prisma.user
  .findUnique({
    where: {
      id: 2,
    },
  })
  .post()

참고: selectpost 배열을 포함한 user를 반환하지만, fluent API는 post배열만 반환합니다.

 

TypeORM

// Using `relations`
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
  relations: ['posts'],
})

// Using JOIN
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
  join: {
    alias: 'user',
    leftJoinAndSelect: {
      posts: 'user.posts',
    },
  },
})

// Eager relations
const userRepository = getRepository(User)
const user = await userRepository.findOne(id)

 

Filtering for concrete values

Prisma

const posts = await prisma.post.findMany({
  where: {
    title: {
      contains: 'Hello',
    },
  },
})

TypeORM

const userRepository = getRepository(User)
const users = await userRepository.find({
  where: {
    name: 'Alice',
  },
})

 

Other filter criteria

Prisma

Prisma는 최신 애플리케이션 개발에서 일반적으로 사용되는 많은 추가 필터를 생성합니다.

TypeORM

TypeORM은 보다 복잡한 비교를 생성하는 데 사용할 수 있는 내장 연산자를 제공합니다.

 

Relation filters

Prisma

Prisma를 사용하면 검색되는 목록의 모델뿐만 아니라 해당 모델의 관계에 적용되는 기준에 따라 목록을 필터링할 수 있습니다.

예를 들어 다음 쿼리는 제목에 'Hello'가 포함된 글이 하나 이상 있는 사용자를 반환합니다.

const posts = await prisma.user.findMany({
  where: {
    Post: {
      some: {
        title: {
          contains: 'Hello',
        },
      },
    },
  },
})

 

TypeORM

TypeORM은 관계 필터를 위한 전용 API를 제공하지 않습니다. 쿼리 빌더를 사용하거나 직접 쿼리를 작성하여 유사한 기능을 얻을 수 있습니다.

 

Pagination

Prisma

Cursor-style 페이지네이션

const page = await prisma.post.findMany({
  before: {
    id: 242,
  },
  last: 20,
})

Offset 페이지네이션

const cc = await prisma.post.findMany({
  skip: 200,
  first: 20,
})

 

TypeORM

const postRepository = getRepository(Post)
const posts = await postRepository.find({
  skip: 5,
  take: 10,
})

 

객체 생성

Prisma

const user = await prisma.user.create({
  data: {
    email: 'alice@prisma.io',
  },
})

TypeORM

// Using `save`
const user = new User()
user.name = 'Alice'
user.email = 'alice@prisma.io'
await user.save()

// Using `create`
const userRepository = getRepository(User)
const user = await userRepository.create({
  name: 'Alice',
  email: 'alice@prisma.io',
})
await user.save()

// Using `insert`
const userRepository = getRepository(User)
await userRepository.insert({
  name: 'Alice',
  email: 'alice@prisma.io',
})

 

객체 업데이트

Prisma

const user = await prisma.user.update({
  data: {
    name: 'Alicia',
  },
  where: {
    id: 2,
  },
})

TypeORM

const userRepository = getRepository(User)
const updatedUser = await userRepository.update(id, {
  name: 'James',
  email: 'james@prisma.io',
})

 

객체 삭제

Prisma

const deletedUser = await prisma.user.delete({
  where: {
    id: 10,
  },
})

TypeORM

// using `delete`
const userRepository = getRepository(User)
await userRepository.delete(id)

// using `remove`
const userRepository = getRepository(User)
const deletedUser = await userRepository.remove(user)

 

Batch 업데이트

Prisma

const user = await prisma.user.updateMany({
  data: {
    name: 'Published author!',
  },
  where: {
    Post: {
      some: {
        published: true,
      },
    },
  },
})

 

TypeORM

데이터베이스의 엔티티들을 업데이트하기 위해 쿼리 빌더를 사용할 수 있습니다.

 

Batch 삭제

Prisma

const users = await prisma.user.deleteMany({
  where: {
    id: {
      in: [1, 2, 6, 6, 22, 21, 25],
    },
  },
})

TypeORM

// using `delete`
const userRepository = getRepository(User)
await userRepository.delete([id1, id2, id3])

// using `remove`
const userRepository = getRepository(User)
const deleteUsers = await userRepository.remove([user1, user2, user3])

 

트랜잭션

Prisma

const user = await prisma.user.create({
  data: {
    email: 'bob.rufus@prisma.io',
    name: 'Bob Rufus',
    Post: {
      create: [
        { title: 'Working at Prisma' },
        { title: 'All about databases' },
      ],
    },
  },
})

 

TypeORM

await getConnection().$transaction(async (transactionalEntityManager) => {
  const user = getRepository(User).create({
    name: 'Bob',
    email: 'bob@prisma.io',
  })
  const post1 = getRepository(Post).create({
    title: 'Join us for GraphQL Conf in 2019',
  })
  const post2 = getRepository(Post).create({
    title: 'Subscribe to GraphQL Weekly for GraphQL news',
  })
  user.posts = [post1, post2]
  await transactionalEntityManager.save(post1)
  await transactionalEntityManager.save(post2)
  await transactionalEntityManager.save(user)
})
profile

Octoping의 블로그

@Octoping

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!