リレーションクエリ
Prisma Clientの重要な機能は、2つ以上のモデル間のリレーションをクエリできることです。リレーションクエリには以下が含まれます。
- ネストされた読み取り(eager loadingとも呼ばれます)(
select
およびinclude
経由) - ネストされた書き込み (トランザクション保証付き)
- 関連レコードのフィルタリング
Prisma Clientには、リレーションをトラバースするためのfluent APIもあります。
ネストされた読み取り
ネストされた読み取りを使用すると、ユーザーとそのユーザーの投稿など、データベース内の複数のテーブルから関連データを読み取ることができます。以下のことができます。
include
を使用して、クエリのレスポンスにユーザーの投稿やプロフィールなどの関連レコードを含めます。- ネストされた
select
を使用して、関連レコードから特定のフィールドを含めます。include
の内側にselect
をネストすることもできます。
リレーションロード戦略(プレビュー)
バージョン 5.8.0 以降、PostgreSQLデータベースの場合、relationLoadStrategy
オプションを介して、リレーションクエリをどのように実行するか(つまり、どのようなロード戦略を適用するか)をクエリレベルごとに決定できます。
バージョン 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
が プレビュー から 一般提供 (GA) に移行すると、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 JOIN
と 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,
},
},
},
})
include
の内側に select
をネストすることもできます。次の例では、すべての 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はすべての変更をロールバックします。
- データモデルでサポートされている任意のレベルのネストをサポートします。
- モデルのcreateまたはupdateクエリを使用する場合、リレーションフィールドで使用できます。次のセクションでは、クエリごとに使用可能なネストされた書き込みオプションを示します。
関連レコードを作成する
レコードと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つのクエリでユーザー、複数の投稿、および投稿ごとの複数のコメントを作成できます。 * たとえば、has-oneリレーションで外部キーを手動で設定できます: { authorId: 9} |
1-nリレーションをサポート | ✔ | ✔ | たとえば、ユーザーと複数の投稿を作成できます(1人のユーザーが多数の投稿を持つ)。 |
m-nリレーションをサポート | ✔ | ✘ | たとえば、投稿と複数のカテゴリを作成できます(1つの投稿は複数のカテゴリを持つことができ、1つのカテゴリは複数の投稿を持つことができる)。 |
重複レコードのスキップをサポート | ✘ | ✔ | skipDuplicates クエリオプションを使用します。 |
ネストされた create
の使用
次のクエリでは、ネストされた create
を使用して作成します。
- 1人のユーザー
- 2つの投稿
- 1つの投稿カテゴリ
この例では、ネストされた 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
},
},
},
})
ネストされたcreate操作がデータベース内の複数のテーブルに一度に書き込む方法の視覚的な表現を以下に示します。
ネストされた createMany
の使用
次のクエリでは、ネストされた createMany
を使用して作成します。
- 1人のユーザー
- 2つの投稿
この例では、ネストされた 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[0].id
}, {
title: "Linux or macOS — what's better?",
categoryId: categories[1].id
},
{
title: "Who will win the next soccer championship?",
categoryId: categories[2].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 投稿が未公開 |
「未公開の Post レコードを1つも持たないすべての User のリストが必要です」 | 投稿の none が未公開 |
「未公開の Post レコードのみを持つすべての User のリストが必要です」 | every 投稿が未公開 |
たとえば、次のクエリは、次の基準を満たす 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を使用すると、関数呼び出しを介してモデルのリレーションをfluentにトラバースできます。最後の関数呼び出しがクエリ全体の戻り値の型を決定することに注意してください(それぞれの型アノテーションは、それを明確にするために以下のコードスニペットに追加されています)。
このクエリは、特定の 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 issue)を参照してください。
注意:
.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()