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

TypeORM

このページでは、Prisma ORMとTypeORMを比較します。TypeORMからPrisma ORMへの移行方法については、こちらのガイドをご覧ください。

TypeORM vs Prisma ORM

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

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

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

Prisma 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は主に、リストまたはレコードをフィルタリングするためにSQL演算子に依存しています。たとえば、findメソッドなどです。一方、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はlimit-offsetページネーションのみを提供しますが、Prisma ORMはlimit-offsetとカーソルベースの両方専用のAPIを便利に提供します。両方のアプローチの詳細については、ドキュメントのページネーションセクションまたはAPI比較の下記をご覧ください。

リレーション

外部キーを介して接続されたレコードの操作は、SQLで非常に複雑になる可能性があります。Prisma ORMの仮想リレーションフィールドの概念により、アプリケーション開発者は関連データを直感的かつ便利に操作できます。Prisma ORMのアプローチの利点の一部は次のとおりです。

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

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

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

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

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メソッド(例:findfindByIdsfindOne、...)にselectオプションを提供します。例:

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

返されたpublishedPosts配列の各オブジェクトは、実行時に選択されたidおよびtitleプロパティのみを保持しますが、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モデル用に生成するUser型とPost型は次のようになります

// 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オプションは、さまざまなシナリオで実際には型安全ではありません。特定の型(ILikeは文字列、MoreThanは数値で動作)でのみ機能するFindOperatorILikeMoreThanなど)を使用すると、モデルのフィールドに正しい型を提供する保証が失われます。

たとえば、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の旅を続けましょう アクティブなコミュニティ。最新情報を入手し、参加し、他の開発者と協力しましょう

皆様のご参加を心よりお待ちしており、コミュニティの一員としてお迎えできることを楽しみにしています!