リレーションクエリ
Prisma Clientの主要な機能は、2つ以上のモデル間のリレーションをクエリする機能です。リレーションクエリには以下が含まれます。
select
とinclude
を介したネストされた読み取り (イーガーローディングとも呼ばれます)- トランザクション保証付きのネストされた書き込み
- 関連レコードのフィルタリング
Prisma Clientには、リレーションを走査するためのFluent APIもあります。
ネストされた読み取り
ネストされた読み取りを使用すると、データベース内の複数のテーブルから関連データ(ユーザーとそのユーザーの投稿など)を読み取ることができます。以下を実行できます。
include
を使用して、クエリ応答にユーザーの投稿やプロフィールなどの関連レコードを含めます。- ネストされた
select
を使用して、関連レコードから特定のフィールドを含めます。select
をinclude
内にネストすることもできます。
リレーション読み込み戦略 (プレビュー)
バージョン5.8.0以降、PostgreSQLデータベースの場合、relationLoadStrategy
オプションを介して、Prisma Clientにリレーションクエリをどのように実行させるか (つまり、どのロード戦略を適用するか) をクエリレベルで決定できます。
バージョン5.10.0以降、この機能はMySQLでも利用できます。
relationLoadStrategy
オプションは現在プレビュー段階であるため、PrismaスキーマファイルでrelationJoins
プレビュー機能フラグを介して有効にする必要があります。
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}
このフラグを追加した後、Prisma Clientを再生成するためにprisma generate
を再度実行する必要があります。relationJoins
機能は現在、PostgreSQL、CockroachDB、およびMySQLで利用可能です。
Prisma Clientはリレーションに対して2つのロード戦略をサポートしています。
join
(デフォルト): データベースレベルのLATERAL JOIN
(PostgreSQL) または相関サブクエリ (MySQL) を使用し、単一のクエリでデータベースからすべてのデータを取得します。query
: データベースに複数のクエリ (テーブルごとに1つ) を送信し、アプリケーションレベルでそれらを結合します。
これら2つのオプションのもう1つの重要な違いは、join
戦略がデータベースレベルでJSON集約を使用することです。これは、Prisma Clientによって返されるJSON構造をデータベース内で既に作成するため、アプリケーションレベルでの計算リソースを節約します。
注意:
relationLoadStrategy
がプレビューから一般提供に移行すると、すべてのリレーションクエリでjoin
が普遍的にデフォルトになります。
例
relationLoadStrategy
オプションは、include
またはselect
をサポートする任意のクエリのトップレベルで使用できます。
以下はinclude
の例です。
const users = await prisma.user.findMany({
relationLoadStrategy: 'join', // or 'query'
include: {
posts: true,
},
})
そして、こちらはselect
の別の例です。
const users = await prisma.user.findMany({
relationLoadStrategy: 'join', // or 'query'
select: {
posts: true,
},
})
どのロード戦略を使用すべきか?
join
戦略 (デフォルト) はほとんどのシナリオでより効果的です。PostgreSQLでは、LATERAL JOINs
とJSON集約の組み合わせを使用して、結果セットの冗長性を減らし、クエリ結果を期待されるJSON構造に変換する作業をデータベースサーバーに委譲します。MySQLでは、相関サブクエリを使用して単一のクエリで結果を取得します。- データセットとクエリの特性によっては、
query
の方がパフォーマンスが向上するエッジケースがあるかもしれません。これらの状況を特定するために、データベースクエリのプロファイリングを推奨します。 - データベースサーバーのリソースを節約し、データのマージと変換の重い処理をアプリケーションサーバーで行いたい場合は、
query
を使用してください。これはスケーリングが容易な場合があります。
リレーションを含める
次の例では、単一のユーザーとそのユーザーの投稿を返します。
const user = await prisma.user.findFirst({
include: {
posts: true,
},
})
特定のリレーションのすべてのフィールドを含める
次の例では、投稿とその投稿の作成者を返します。
const post = await prisma.post.findFirst({
include: {
author: true,
},
})
深くネストされたリレーションを含める
include
オプションをネストして、リレーションのリレーションを含めることができます。次の例では、ユーザーの投稿、および各投稿のカテゴリを返します。
const user = await prisma.user.findFirst({
include: {
posts: {
include: {
categories: true,
},
},
},
})
含まれるリレーションの特定のフィールドを選択する
ネストされたselect
を使用して、リレーションのフィールドのサブセットを選択して返すことができます。たとえば、次のクエリは、ユーザーのname
と各関連投稿のtitle
を返します。
const user = await prisma.user.findFirst({
select: {
name: true,
posts: {
select: {
title: true,
},
},
},
})
select
をinclude
の中にネストすることもできます。次の例は、すべてのUser
フィールドと各投稿のtitle
フィールドを返します。
const user = await prisma.user.findFirst({
include: {
posts: {
select: {
title: true,
},
},
},
})
select
とinclude
を同じレベルで使用することはできません。つまり、ユーザーの投稿をinclude
し、各投稿のタイトルをselect
する場合でも、ユーザーのemail
のみをselect
することはできません。
// The following query returns an exception
const user = await prisma.user.findFirst({
select: { // This won't work!
email: true
}
include: { // This won't work!
posts: {
select: {
title: true
}
}
},
})
代わりに、ネストされたselect
オプションを使用してください。
const user = await prisma.user.findFirst({
select: {
// This will work!
email: true,
posts: {
select: {
title: true,
},
},
},
})
リレーション数
3.0.1以降では、フィールドと共にリレーションのカウントをinclude
またはselect
することができます。たとえば、ユーザーの投稿数などです。
const relationCount = await prisma.user.findMany({
include: {
_count: {
select: { posts: true },
},
},
})
リレーションのリストをフィルタリングする
select
またはinclude
を使用して関連データのサブセットを返す場合、select
またはinclude
内でリレーションのリストをフィルタリングおよびソートできます。
たとえば、次のクエリは、ユーザーに関連付けられた未公開の投稿のタイトルリストを返します。
const result = await prisma.user.findFirst({
select: {
posts: {
where: {
published: false,
},
orderBy: {
title: 'asc',
},
select: {
title: true,
},
},
},
})
同じクエリをinclude
を使って次のように書くこともできます。
const result = await prisma.user.findFirst({
include: {
posts: {
where: {
published: false,
},
orderBy: {
title: 'asc',
},
},
},
})
ネストされた書き込み
ネストされた書き込みを使用すると、リレーショナルデータを単一のトランザクションでデータベースに書き込むことができます。
ネストされた書き込み
- 単一のPrisma Clientクエリ内で、複数のテーブルにわたるデータの作成、更新、または削除に対してトランザクション保証を提供します。クエリの一部でも失敗した場合 (たとえば、ユーザーの作成は成功したが、投稿の作成が失敗した場合)、Prisma Clientはすべての変更をロールバックします。
- データモデルがサポートする任意のレベルのネストをサポートします。
- モデルの作成または更新クエリを使用する際に、リレーションフィールドで利用できます。以下のセクションでは、クエリごとに利用可能なネストされた書き込みオプションを示します。
関連レコードを作成する
レコードと1つ以上の関連レコードを同時に作成できます。次のクエリは、User
レコードと2つの関連Post
レコードを作成します。
const result = await prisma.user.create({
data: {
email: 'elsa@prisma.io',
name: 'Elsa Prisma',
posts: {
create: [
{ title: 'How to make an omelette' },
{ title: 'How to eat an omelette' },
],
},
},
include: {
posts: true, // Include all posts in the returned object
},
})
単一のレコードと複数の関連レコードを作成する
単一のレコードと複数の関連レコード (例: 複数の投稿を持つユーザー) を作成または更新するには、次の2つの方法があります。
- ネストされた
create
クエリを使用する - ネストされた
createMany
クエリを使用する
ほとんどの場合、skipDuplicates
クエリオプションが必要でない限り、ネストされたcreate
が推奨されます。以下に、2つのオプションの違いを説明する簡単な表を示します。
機能 | create | createMany | 備考 |
---|---|---|---|
追加のリレーションのネストをサポート | ✔ | ✘ * | 例えば、ユーザー、複数の投稿、各投稿ごとの複数のコメントを1つのクエリで作成できます。 * 1対1リレーションでは、外部キーを手動で設定できます(例: { authorId: 9} )。 |
1対多リレーションをサポート | ✔ | ✔ | たとえば、ユーザーと複数の投稿を作成できます(1人のユーザーは多くの投稿を持つ)。 |
多対多リレーションをサポート | ✔ | ✘ | たとえば、投稿と複数のカテゴリを作成できます (1つの投稿は多くのカテゴリを持つことができ、1つのカテゴリは多くの投稿を持つことができます)。 |
重複レコードのスキップをサポート | ✘ | ✔ | skipDuplicates クエリオプションを使用してください。 |
ネストされたcreate
の使用
次のクエリは、ネストされたcreate
を使用して以下を作成します。
- 一人のユーザー
- 二つの投稿
- 一つの投稿カテゴリ
この例では、返されるデータにすべての投稿と投稿カテゴリを含めるために、ネストされたinclude
も使用しています。
const result = await prisma.user.create({
data: {
email: 'yvette@prisma.io',
name: 'Yvette',
posts: {
create: [
{
title: 'How to make an omelette',
categories: {
create: {
name: 'Easy cooking',
},
},
},
{ title: 'How to eat an omelette' },
],
},
},
include: {
// Include posts
posts: {
include: {
categories: true, // Include post categories
},
},
},
})
以下は、ネストされた作成操作がデータベース内の複数のテーブルに一度に書き込む方法の視覚的表現です。
ネストされたcreateMany
の使用
次のクエリは、ネストされたcreateMany
を使用して以下を作成します。
- 一人のユーザー
- 二つの投稿
この例では、返されるデータにすべての投稿を含めるために、ネストされたinclude
も使用しています。
const result = await prisma.user.create({
data: {
email: 'saanvi@prisma.io',
posts: {
createMany: {
data: [{ title: 'My first post' }, { title: 'My second post' }],
},
},
},
include: {
posts: true,
},
})
強調表示されたクエリ内にさらにcreate
またはcreateMany
をネストすることはできません。つまり、ユーザー、投稿、投稿カテゴリを同時に作成することはできません。
回避策として、まず接続されるレコードを作成するクエリを送信し、次に実際のレコードを作成することができます。例えば、
const categories = await prisma.category.createManyAndReturn({
data: [
{ name: 'Fun', },
{ name: 'Technology', },
{ name: 'Sports', }
],
select: {
id: true
}
});
const posts = await prisma.post.createManyAndReturn({
data: [{
title: "Funniest moments in 2024",
categoryId: categories.filter(category => category.name === 'Fun')!.id
}, {
title: "Linux or macOS — what's better?",
categoryId: categories.filter(category => category.name === 'Technology')!.id
},
{
title: "Who will win the next soccer championship?",
categoryId: categories.filter(category => category.name === 'Sports')!.id
}]
});
すべてのレコードを単一のデータベースクエリで作成したい場合は、$transaction
または型安全な生SQLの使用を検討してください。
複数のレコードと複数の関連レコードを作成する
createMany()
またはcreateManyAndReturn()
クエリではリレーションにアクセスできないため、単一のネストされた書き込みで複数のユーザーと複数の投稿を作成することはできません。以下は不可能です。
const createMany = await prisma.user.createMany({
data: [
{
name: 'Yewande',
email: 'yewande@prisma.io',
posts: {
// Not possible to create posts!
},
},
{
name: 'Noor',
email: 'noor@prisma.io',
posts: {
// Not possible to create posts!
},
},
],
})
複数のレコードを接続する
次のクエリは、新しいUser
レコードを作成し(create
)、そのレコードを(connect
)既存の3つの投稿に接続します。
const result = await prisma.user.create({
data: {
email: 'vlad@prisma.io',
posts: {
connect: [{ id: 8 }, { id: 9 }, { id: 10 }],
},
},
include: {
posts: true, // Include all posts in the returned object
},
})
注: 投稿レコードのいずれかが見つからない場合、Prisma Clientは例外をスローします:
connect: [{ id: 8 }, { id: 9 }, { id: 10 }]
単一のレコードを接続する
既存のレコードを新規または既存のユーザーにconnect
することができます。以下のクエリは、既存の投稿(id: 11
)を既存のユーザー(id: 9
)に接続します。
const result = await prisma.user.update({
where: {
id: 9,
},
data: {
posts: {
connect: {
id: 11,
},
},
},
include: {
posts: true,
},
})
レコードを接続または作成する
関連レコードが既に存在する場合とそうでない場合がある場合は、connectOrCreate
を使用して関連レコードを接続します。
- メールアドレス
viola@prisma.io
のUser
を接続する、または - ユーザーがまだ存在しない場合は、メールアドレス
viola@prisma.io
で新しいUser
を作成する
const result = await prisma.post.create({
data: {
title: 'How to make croissants',
author: {
connectOrCreate: {
where: {
email: 'viola@prisma.io',
},
create: {
email: 'viola@prisma.io',
name: 'Viola',
},
},
},
},
include: {
author: true,
},
})
関連レコードの切断
レコードのリストから1つをdisconnect
(たとえば、特定のブログ投稿)するには、切断するレコードのIDまたは一意の識別子を指定します。
const result = await prisma.user.update({
where: {
id: 16,
},
data: {
posts: {
disconnect: [{ id: 12 }, { id: 19 }],
},
},
include: {
posts: true,
},
})
1つのレコードをdisconnect
(たとえば、投稿の作成者)するには、disconnect: true
を使用します。
const result = await prisma.post.update({
where: {
id: 23,
},
data: {
author: {
disconnect: true,
},
},
include: {
author: true,
},
})
すべての関連レコードを切断する
1対多のリレーション(ユーザーが多くの投稿を持つ)において、すべての関連レコードをdisconnect
するには、示されているようにリレーションを空のリストにset
します。
const result = await prisma.user.update({
where: {
id: 16,
},
data: {
posts: {
set: [],
},
},
include: {
posts: true,
},
})
すべての関連レコードを削除する
すべての関連Post
レコードを削除する
const result = await prisma.user.update({
where: {
id: 11,
},
data: {
posts: {
deleteMany: {},
},
},
include: {
posts: true,
},
})
特定の関連レコードを削除する
未公開の投稿をすべて削除してユーザーを更新
const result = await prisma.user.update({
where: {
id: 11,
},
data: {
posts: {
deleteMany: {
published: false,
},
},
},
include: {
posts: true,
},
})
特定の投稿を削除してユーザーを更新
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
deleteMany: [{ id: 7 }],
},
},
include: {
posts: true,
},
})
すべての関連レコードを更新 (またはフィルタリング)
ネストされたupdateMany
を使用して、特定のユーザーのすべての関連レコードを更新できます。次のクエリは、特定のユーザーのすべての投稿を未公開にします。
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
updateMany: {
where: {
published: true,
},
data: {
published: false,
},
},
},
},
include: {
posts: true,
},
})
特定の関連レコードを更新する
const result = await prisma.user.update({
where: {
id: 6,
},
data: {
posts: {
update: {
where: {
id: 9,
},
data: {
title: 'My updated title',
},
},
},
},
include: {
posts: true,
},
})
関連レコードを更新または作成する
次のクエリは、ネストされたupsert
を使用して、"bob@prisma.io"
というユーザーが存在すれば更新し、存在しなければ作成します。
const result = await prisma.post.update({
where: {
id: 6,
},
data: {
author: {
upsert: {
create: {
email: 'bob@prisma.io',
name: 'Bob the New User',
},
update: {
email: 'bob@prisma.io',
name: 'Bob the existing user',
},
},
},
},
include: {
author: true,
},
})
既存のレコードに新しい関連レコードを追加する
update
内にcreate
またはcreateMany
をネストして、既存のレコードに新しい関連レコードを追加できます。次のクエリは、id
が9のユーザーに2つの投稿を追加します。
const result = await prisma.user.update({
where: {
id: 9,
},
data: {
posts: {
createMany: {
data: [{ title: 'My first post' }, { title: 'My second post' }],
},
},
},
include: {
posts: true,
},
})
リレーションフィルタ
"多"リレーションでフィルタリングする
Prisma Clientは、リレーションの「多」側で関連レコードのプロパティによってレコードをフィルタリングするために、some
、every
、およびnone
オプションを提供します。例えば、投稿のプロパティに基づいてユーザーをフィルタリングします。
例えば
要件 | 使用するクエリオプション |
---|---|
「未公開のPost レコードを少なくとも1つ持つすべてのUser のリストが欲しい」 | some posts are unpublished (いくつかの投稿が未公開) |
「未公開のPost レコードを持たないすべてのUser のリストが欲しい」 | none of the posts are unpublished (投稿のどれも未公開ではない) |
「未公開のPost レコードのみを持つすべてのUser のリストが欲しい」 | every post is unpublished (すべての投稿が未公開) |
たとえば、次のクエリは、以下の基準を満たすUser
を返します。
- 閲覧数が100を超える投稿がない
- すべての投稿のいいね数が50以下
const users = await prisma.user.findMany({
where: {
posts: {
none: {
views: {
gt: 100,
},
},
every: {
likes: {
lte: 50,
},
},
},
},
include: {
posts: true,
},
})
"一"リレーションでフィルタリングする
Prisma Clientは、リレーションの「一」側で関連レコードのプロパティに基づいてレコードをフィルタリングするために、is
とisNot
オプションを提供します。たとえば、投稿の作成者のプロパティに基づいて投稿をフィルタリングします。
たとえば、次のクエリは、以下の基準を満たすPost
レコードを返します。
- 作成者の名前がBobではない
- 作成者が40歳より年上
const users = await prisma.post.findMany({
where: {
author: {
isNot: {
name: 'Bob',
},
is: {
age: {
gt: 40,
},
},
},
},
include: {
author: true,
},
})
"多"レコードの不在でフィルタリングする
たとえば、次のクエリはnone
を使用して、投稿がゼロのすべてのユーザーを返します。
const usersWithZeroPosts = await prisma.user.findMany({
where: {
posts: {
none: {},
},
},
include: {
posts: true,
},
})
"一"リレーションの不在でフィルタリングする
次のクエリは、作成者リレーションを持たないすべての投稿を返します。
const postsWithNoAuthor = await prisma.post.findMany({
where: {
author: null, // or author: { }
},
include: {
author: true,
},
})
関連レコードの存在でフィルタリングする
次のクエリは、少なくとも1つの投稿を持つすべてのユーザーを返します。
const usersWithSomePosts = await prisma.user.findMany({
where: {
posts: {
some: {},
},
},
include: {
posts: true,
},
})
Fluent API
Fluent APIを使用すると、関数呼び出しを介してモデルのリレーションを流れるようにたどることができます。最後の関数呼び出しがクエリ全体の戻り型を決定することに注意してください(そのことを明確にするために、以下のコードスニペットにはそれぞれの型アノテーションが追加されています)。
このクエリは、特定のUser
によるすべてのPost
レコードを返します。
const postsByUser: Post[] = await prisma.user
.findUnique({ where: { email: 'alice@prisma.io' } })
.posts()
これは、以下のfindMany
クエリと同等です。
const postsByUser = await prisma.post.findMany({
where: {
author: {
email: 'alice@prisma.io',
},
},
})
クエリの主な違いは、Fluent API呼び出しが2つの個別のデータベースクエリに変換されるのに対し、もう1つは単一のクエリのみを生成することです(詳細については、このGitHubイシューを参照)。
注:
.findUnique({ where: { email: 'alice@prisma.io' } }).posts()
クエリはPrisma ClientのPrisma dataloaderによって自動的にバッチ処理されるという事実を利用して、GraphQLリゾルバでのN+1問題を回避できます。
このリクエストは、特定の投稿によるすべてのカテゴリを返します。
const categoriesOfPost: Category[] = await prisma.post
.findUnique({ where: { id: 1 } })
.categories()
好きなだけクエリを連結できることに注意してください。この例では、連結はProfile
から始まり、User
を経由してPost
に続きます。
const posts: Post[] = await prisma.profile
.findUnique({ where: { id: 1 } })
.user()
.posts()
連結の唯一の要件は、前の関数呼び出しが単一のオブジェクト(たとえば、findUnique
クエリまたはprofile.user()
のような「一対一のリレーション」によって返されるもの)のみを返す必要があることです。
findMany
が単一のオブジェクトではなくリストを返すため、次のクエリは不可能です。
// This query is illegal
const posts = await prisma.user.findMany().posts()