メインコンテンツへスキップ

TypeORM

このページではPrisma ORMとTypeORMを比較します。TypeORMからPrisma ORMへ移行する方法を知りたい場合は、このガイドをご覧ください。

TypeORM vs Prisma ORM

Prisma ORMとTypeORMは類似の問題を解決しますが、その動作方法は大きく異なります。

TypeORMは、テーブルモデルクラスにマッピングする従来のORMです。これらのモデルクラスは、SQLマイグレーションを生成するために使用できます。モデルクラスのインスタンスは、実行時にアプリケーションへのCRUDクエリのためのインターフェースを提供します。

Prisma ORMは、肥大化したモデルインスタンス、ビジネスロジックとストレージロジックの混在、型安全性の欠如、遅延ロードなどによって引き起こされる予測不能なクエリといった、従来のORMの多くの問題を軽減する新しい種類のORMです。

Prismaスキーマを使用して、アプリケーションモデルを宣言的に定義します。Prisma Migrateは、PrismaスキーマからSQLマイグレーションを生成し、それをデータベースに対して実行することを可能にします。CRUDクエリは、Node.jsおよびTypeScript用の軽量で完全に型安全なデータベースクライアントであるPrisma Clientによって提供されます。

API設計と抽象度

TypeORMとPrisma ORMは異なる抽象度で動作します。TypeORMはそのAPIにおいてSQLをミラーリングすることに近く、一方でPrisma Clientは、アプリケーション開発者の一般的なタスクを念頭に置いて慎重に設計された、より高レベルの抽象化を提供します。Prisma ORMのAPI設計は、「正しいことを簡単にする」という考え方に大きく傾倒しています。

Prisma Clientはより高レベルの抽象度で動作しますが、基盤となるデータベースの全機能を公開するように努めており、ユースケースで必要に応じていつでも生のSQLにドロップダウンできます。

以下のセクションでは、Prisma ORMとTypeORMのAPIが特定のシナリオでどのように異なるか、そしてこれらのケースにおけるPrisma ORMのAPI設計の根拠が何かをいくつかの例で検証します。

フィルタリング

TypeORMは主に、findメソッドなどを使用して、リストやレコードのフィルタリングにSQL演算子に依存しています。一方、Prisma ORMは、より汎用的な演算子セットを提供し、直感的に使用できます。また、後述の型安全性のセクション以下で説明するように、TypeORMは多くのシナリオでフィルタークエリの型安全性を失うことにも注意が必要です。

TypeORMとPrisma ORMの両方のフィルタリングAPIがどのように異なるかを示す良い例は、stringフィルターを見ることです。TypeORMは主にSQLから直接来るILike演算子に基づくフィルターを提供しますが、Prisma ORMは開発者が使用できるより具体的な演算子を提供します。例えば、containsstartsWithendsWithなどです。

Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: 'Hello World',
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('Hello World'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { contains: 'Hello World' },
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('%Hello World%'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { startsWith: 'Hello World' },
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('Hello World%'),
},
})
Prisma ORM
const posts = await prisma.post.findMany({
where: {
title: { endsWith: 'Hello World' },
},
})
TypeORM
const posts = await postRepository.find({
where: {
title: ILike('%Hello World'),
},
})

ページネーション

TypeORMはリミット・オフセットによるページネーションのみを提供しますが、Prisma ORMはリミット・オフセットだけでなく、カーソルベースのページネーションにも便利な専用APIを提供します。両方のアプローチについては、ドキュメントのページネーションセクションまたは以下のAPI比較で詳しく学ぶことができます。

リレーション

外部キーで接続されたレコードの操作は、SQLでは非常に複雑になることがあります。Prisma ORMの仮想リレーションフィールドの概念は、アプリケーション開発者が関連データと直感的かつ便利に作業できる方法を提供します。Prisma ORMのアプローチの利点には以下のようなものがあります。

  • fluent APIによるリレーションシップのトラバース(ドキュメント
  • 接続されたレコードの更新/作成を可能にするネストされた書き込み(ドキュメント
  • 関連レコードへのフィルターの適用(ドキュメント
  • JOINを気にせずにネストされたデータを簡単かつ型安全にクエリ(ドキュメント
  • モデルとそのリレーションに基づいたネストされたTypeScript型定義の作成(ドキュメント
  • リレーションフィールドを介したデータモデルにおけるリレーションの直感的なモデリング(ドキュメント
  • リレーションテーブル(JOIN、リンク、ピボット、結合テーブルとも呼ばれる)の暗黙的な処理(ドキュメント

データモデリングとマイグレーション

PrismaモデルはPrismaスキーマで定義されますが、TypeORMはモデル定義にクラスと実験的なTypeScriptデコレータを使用します。Active Record ORMパターンでは、TypeORMのアプローチは、アプリケーションが成長するにつれて保守が困難になる複雑なモデルインスタンスにつながることがよくあります。

一方、Prisma ORMは、Active RecordパターンではなくDataMapper ORMパターンに従い、Prismaスキーマで定義されたモデルのデータを読み書きするための、調整され完全に型安全なAPIを公開する軽量なデータベースクライアントを生成します。

Prisma ORMのデータモデリング用DSLは、シンプルで直感的に使用できます。VS Codeでデータをモデリングする際、オートコンプリート、クイックフィックス、定義へのジャンプなどの機能を持つPrisma ORMの強力なVS Code拡張機能をさらに活用でき、開発者の生産性を向上させます。

Prisma ORM
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 ORMで同様の方法で動作します。どちらのツールも、提供されたモデル定義に基づいてSQLファイルを生成し、それらをデータベースに対して実行するためのCLIを提供します。SQLファイルは、マイグレーションが実行される前に変更できるため、どちらのマイグレーションシステムでもカスタムデータベース操作を実行できます。

型安全

TypeORMは、Node.jsエコシステムでTypeScriptを完全に採用した最初のORMの1つであり、開発者がデータベースクエリにある程度の型安全性を得る上で素晴らしい仕事をしました。

しかし、TypeORMの型安全性の保証が不十分な状況が数多く存在します。以下のセクションでは、Prisma ORMがクエリ結果の型に対してより強力な保証を提供できるシナリオを説明します。

フィールドの選択

このセクションでは、クエリでモデルのフィールドのサブセットを選択する際の型安全性の違いについて説明します。

TypeORM

TypeORMは、findメソッド(例: find, findByIds, findOne, ...)に対してselectオプションを提供します。例えば、

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

返されるpublishedPosts配列の各オブジェクトは、実行時に選択されたidtitleプロパティのみを保持しますが、TypeScriptコンパイラはこのことを認識していません。そのため、クエリ後もPostエンティティに定義されている他のプロパティにアクセスできてしまいます。例えば、

const post = publishedPosts[0]

// The TypeScript compiler has no issue with this
if (post.content.length > 0) {
console.log(`This post has some content.`)
}

このコードは実行時にエラーになります。

TypeError: Cannot read property 'length' of undefined

TypeScriptコンパイラは返されたオブジェクトのPost型しか認識せず、それらのオブジェクトが実行時に実際に保持するフィールドについては知りません。そのため、データベースクエリで取得されていないフィールドにアクセスすることを防ぐことができず、実行時エラーが発生します。

Prisma ORM

Prisma Clientは、同じ状況で完全な型安全性を保証し、データベースから取得されていないフィールドへのアクセスからあなたを保護します。

Prisma Clientクエリを使った同じ例を考えてみましょう。

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コンパイラはコンパイル時にすでに次のエラーをスローします。

[ERROR] 14:03:39 ⨯ Unable to compile TypeScript:
src/index.ts:36:12 - error TS2339: Property 'content' does not exist on type '{ id: number; title: string; }'.

42 if (post.content.length > 0) {

これは、Prisma Clientがクエリの戻り値をその場で生成するためです。この場合、publishedPostsは次のように型付けされます。

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

したがって、クエリで取得されなかったモデルのプロパティに誤ってアクセスすることは不可能です。

リレーションの読み込み

このセクションでは、クエリでモデルのリレーションを読み込む際の型安全性の違いについて説明します。従来のORMでは、これはEager Loadingと呼ばれることがあります。

TypeORM

TypeORMは、findメソッドに渡すことができるrelationsオプションを介して、データベースからリレーションをEager Loadすることを可能にします。

この例を考えてみましょう

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 ORM

Prisma ORMは、このような間違いからあなたを保護し、アプリケーションの実行時に発生しうるエラーの全クラスを排除します。Prisma Clientクエリでリレーションを読み込むためにincludeを使用する場合、クエリを指定するためにオートコンプリートを活用できるだけでなく、クエリの結果も適切に型付けされます。

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

繰り返しますが、publishedPostsの型はオンザフライで生成され、次のようになります。

const publishedPosts: (Post & {
author: User
})[]

参考までに、Prisma ClientがあなたのPrismaモデル用に生成するUserPostの型は以下のようになります。

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

フィルタリング

このセクションでは、whereを使用してレコードのリストをフィルタリングする際の型安全性の違いについて説明します。

TypeORM

TypeORMでは、findメソッドにwhereオプションを渡すことで、特定の条件に従って返されるレコードのリストをフィルタリングできます。これらの条件は、モデルのプロパティに関して定義できます。

演算子を使用した型安全性の喪失

この例を考えてみましょう

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

このコードは適切に実行され、実行時に有効なクエリを生成します。しかし、whereオプションはさまざまなシナリオで実際には型安全ではありません。特定の型に対してのみ機能するFindOperator(例: ILikeは文字列に、MoreThanは数値に機能)を使用する場合、モデルのフィールドに正しい型を提供するという保証を失います。

例えば、MoreThan演算子に文字列を提供することができます。TypeScriptコンパイラは文句を言わず、アプリケーションは実行時のみに失敗します。

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

上記のコードは、TypeScriptコンパイラがあなたのために捕捉しない実行時エラーになります。

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 ORM

TypeORMで型安全性の観点から問題となる両方のフィルタリングシナリオは、Prisma ORMによって完全に型安全な方法でカバーされています。

演算子の型安全な使用法

Prisma ORMでは、TypeScriptコンパイラがフィールドごとに演算子の正しい使用法を強制します。

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

Prisma Clientで上記のような問題のあるクエリを指定することは許可されません。

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

TypeScriptコンパイラがこれを検出し、アプリの実行時エラーからあなたを保護するために、次のエラーをスローします。

[ERROR] 16:13:50 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ gt: string; }' is not assignable to type 'number | IntNullableFilter'.
Type '{ gt: string; }' is not assignable to type 'IntNullableFilter'.
Types of property 'gt' are incompatible.
Type 'string' is not assignable to type 'number'.

42 views: { gt: "test" }
モデルプロパティとしてのフィルターの型安全な定義

TypeORMでは、モデルのフィールドにマッピングされないプロパティをwhereオプションに指定できます。上記の例では、viewCountでフィルタリングすると、フィールドの実際の名前がviewsであるため、実行時エラーが発生しました。

Prisma ORMでは、TypeScriptコンパイラはモデルに存在しないwhere内のプロパティを参照することを許可しません。

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

繰り返しますが、TypeScriptコンパイラはあなたの間違いからあなたを保護するために、以下のメッセージで警告します。

[ERROR] 16:16:16 ⨯ Unable to compile TypeScript:
src/index.ts:39:5 - error TS2322: Type '{ published: boolean; title: { contains: string; }; viewCount: { gt: number; }; }' is not assignable to type 'PostWhereInput'.
Object literal may only specify known properties, and 'viewCount' does not exist in type 'PostWhereInput'.

42 viewCount: { gt: 0 }

新規レコードの作成

このセクションでは、新規レコードを作成する際の型安全性の違いについて説明します。

TypeORM

TypeORMでは、データベースに新しいレコードを作成する方法が主に2つあります。insertsaveです。どちらのメソッドも、必須フィールドが提供されていない場合に実行時エラーにつながる可能性のあるデータを開発者が送信することを許可します。

この例を考えてみましょう

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

TypeORMでレコード作成にsaveまたはinsertを使用しても、必須フィールドの値の提供を忘れると、次の実行時エラーが発生します。

QueryFailedError: null value in column "email" of relation "user" violates not-null constraint

emailフィールドはUserエンティティ上で必須として定義されています(これはデータベースのNOT NULL制約によって強制されます)。

Prisma ORM

Prisma ORMは、モデルのすべての必須フィールドの値を送信することを強制することで、このような間違いからあなたを保護します。

例えば、必須のemailフィールドが欠落している新しいUserを作成しようとすると、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 ORM

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

TypeORM

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

単一オブジェクトの選択されたスカラー値の取得

Prisma ORM

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 ORM

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

: selectpost配列を含むuserオブジェクトを返しますが、fluent APIはpost配列のみを返します。

TypeORM

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

具体的な値によるフィルタリング

Prisma ORM

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

TypeORM

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

その他のフィルター条件

Prisma ORM

Prisma ORMは、現代のアプリケーション開発で一般的に使用される多くの追加フィルターを生成します。

TypeORM

TypeORMは、より複雑な比較を作成するために使用できる組み込みの演算子を提供します。

リレーションフィルター

Prisma ORM

Prisma ORMを使用すると、取得されるリストのモデルだけでなく、そのモデルのリレーションに適用される条件に基づいてリストをフィルタリングできます。

例えば、次のクエリはタイトルに「Hello」を含む1つ以上の投稿を持つユーザーを返します。

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

TypeORM

TypeORMはリレーションフィルター専用のAPIを提供していません。QueryBuilderを使用するか、クエリを手動で記述することで、同様の機能を得ることができます。

ページネーション

Prisma ORM

カーソル方式ページネーション

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

オフセットページネーション

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 ORM

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

TypeORM

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

オブジェクトの更新

Prisma ORM

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 ORM

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

TypeORM

const userRepository = getRepository(User)
await userRepository.delete(id)

一括更新

Prisma ORM

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

TypeORM

クエリビルダーを使用してデータベースのエンティティを更新できます

一括削除

Prisma ORM

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

TypeORM

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

トランザクション

Prisma ORM

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

Prismaとつながりましょう

以下のコミュニティと連携して、Prismaの旅を続けましょう。 私たちの活発なコミュニティに参加しましょう。情報収集、イベント参加、他の開発者との協力ができます。

  • Xでフォローしてください お知らせ、ライブイベント、役立つヒントをお届けします。
  • Discordに参加する 質問したり、コミュニティと話したり、会話を通じてアクティブなサポートを受けたりできます。
  • YouTubeを購読する チュートリアル、デモ、ストリームを視聴できます。
  • GitHubで関わる リポジトリにスターを付けたり、問題を報告したり、問題に貢献したりしてください。
私たちは皆様の関与を心から歓迎し、コミュニティの一員となることを楽しみにしています!

© . All rights reserved.