Node.jsのORMであるPrismaとTypeORMを比較する
TypeORM
TypeORMはテーブルをモデルクラスにマッピングする従来のORM
このモデルクラスはSQLマイグレーションをするために使用される。
Prisma
Prismaは新しいORMでモデルインスタンスの肥大化、ビジネスとストレージロジックの混在や型の安全性や、レイジーローディングなどの従来のORMの多くの問題を軽減するためにできた
Typescript-friendly
APIの比較をケースで見ていく
1. フィルタリング
TypeORMではリストやレコードをフィルタリングするために主にSQL演算子を使用している一方で、Prismaは直感的に使用できる演算子(contains, startsWith, endsWithなど)を提供している
また、TypeORMでは多くのケースでフィルタクエリの型安全性を失っている。
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
const posts = await postRepository.find({
where: {
title: { contains: 'Hello World' },
},
})
2. ページング処理
TypeORMはリミットオフセットページネーションのみ提供しているが、Prismaは加えてカーソルページネーションにも対応したAPIを提供する。
- オフセットページネーション
const postRepository = getRepository(Post)
const posts = await postRepository.find({
skip: 5,
take: 10,
})
const cc = prisma.post.findMany({
skip: 200,
first: 20,
})
- カーソルスタイルページネーション
const page = prisma.post.findMany({
before: {
id: 242,
},
last: 20,
})
pagination方式についてはこちらに詳しく載っています
3. リレーション
Prismaはselectやincludeによる入れ子での読み込み、トランザクションを保証する入れ子による書き込み、関連レコードをフィルタリングする仕組みやN+1問題を解決するための仕組みとしてFluent APIが提供されている
const userRepository = getRepository(User)
const user = await userRepository.findOne(id, {
relations: ['posts'],
})
const posts = await prisma.user.findUnique({
where: {
id: 2,
},
include: {
post: true,
},
})
Fluent APIで書いた場合
同一イベントループに発生した同じwhereとselectのパラメータを持つクエリを1つのクエリに最適化できる
const posts = await prisma.user
.findUnique({
where: {
id: 2,
},
})
.post()
また、Prismaでは取得するモデルだけでなく、そのモデルのリレーションでもフィルタリングができる。
TypeORMでは専用のAPIは提供していない。QueryBuilderを使用するか、手作業によりクエリを記述する必要がある。
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を公開する軽量なクライアントを生成できる。
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
}
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がこれから選定されていく印象を感じた。
参考
Discussion