🐸

Node.jsのORMであるPrismaとTypeORMを比較する

2022/08/31に公開

TypeORM

TypeORMはテーブルをモデルクラスにマッピングする従来のORM
このモデルクラスはSQLマイグレーションをするために使用される。

Prisma

Prismaは新しいORMでモデルインスタンスの肥大化、ビジネスとストレージロジックの混在や型の安全性や、レイジーローディングなどの従来のORMの多くの問題を軽減するためにできた
Typescript-friendly

APIの比較をケースで見ていく

1. フィルタリング

TypeORMではリストやレコードをフィルタリングするために主にSQL演算子を使用している一方で、Prismaは直感的に使用できる演算子(contains, startsWith, endsWithなど)を提供している
また、TypeORMでは多くのケースでフィルタクエリの型安全性を失っている。

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

2. ページング処理

TypeORMはリミットオフセットページネーションのみ提供しているが、Prismaは加えてカーソルページネーションにも対応したAPIを提供する。

  • オフセットページネーション
TypeORM
const postRepository = getRepository(Post)
const posts = await postRepository.find({
  skip: 5,
  take: 10,
})
Prisma
const cc = prisma.post.findMany({
  skip: 200,
  first: 20,
})
  • カーソルスタイルページネーション
Prisma
const page = prisma.post.findMany({
  before: {
    id: 242,
  },
  last: 20,
})

pagination方式についてはこちらに詳しく載っています

3. リレーション

Prismaはselectやincludeによる入れ子での読み込み、トランザクションを保証する入れ子による書き込み、関連レコードをフィルタリングする仕組みやN+1問題を解決するための仕組みとしてFluent APIが提供されている

TypeORM
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
  relations: ['posts'],
})
Prisma
const posts = await prisma.user.findUnique({
  where: {
    id: 2,
  },
  include: {
    post: true,
  },
})

Fluent APIで書いた場合

同一イベントループに発生した同じwhereとselectのパラメータを持つクエリを1つのクエリに最適化できる

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

また、Prismaでは取得するモデルだけでなく、そのモデルのリレーションでもフィルタリングができる。
TypeORMでは専用のAPIは提供していない。QueryBuilderを使用するか、手作業によりクエリを記述する必要がある。

Prismaでのリレーションフィルター

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

4. マイグレーション

  • Prismaのモデルはスキーマで定義されるが、TypeORMではモデルの定義に加えてデコレータにより制約を付与してくスタイル。
    スキーマ定義とエンティティを同一クラスで使いまわしているため定義が複雑になってしまう場合に煩雑になる。また、DBの型がデコレータに依存しているためクラスの型と異なるなど型安全性が担保されていない問題がある。

  • TypeORMではActiveRecordパターンとRepositoryパターンに対応しているところがメリット。
    初期リリースなど生産性の面ではActiveRecordパターンで実装するなど恩恵はあるが、徐々に処理が複雑になりActiveRecordで処理できない場合にRepositoryパターンに移行していく。

  • Prismaでは独自のDSLを起用していて、スキーマで定義されているモデルのデータを読み書きするためカスタマイズされた完全に型安全なAPIを公開する軽量なクライアントを生成できる。

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
}
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])
}

どちらも提供されたモデルに基づきSQLファイルを生成してそれをデータベースに対して実行するためのCLIを提供するというアプローチをとっている。どのようなカスタムデータベース操作でもどちらのマイグレーションシステムでも実行はできる。

PrismaClient素晴らしい

TypeORMには自動マイグレーションと手動でマイグレーションの二つがある。
@Entityの定義とDBの差分で自動でマイグレーションがかかることで初期の開発には向いている印象。

スキーマから生成されたPrisma Clientにより型付けされた結果を開発者が利用でき、オブジェクトで考えることができるなどTypescript ORMの中でも最も強力な型安全性を備えたものであるPrismaがこれから選定されていく印象を感じた。

参考

https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#type-safety

Discussion