多対多リレーション
多対多(m-n)リレーションとは、リレーションの一方の側のゼロ以上のレコードが、もう一方の側のゼロ以上のレコードに接続できるリレーションのことです。
Prismaスキーマの構文と、基盤となるデータベースでの実装は、リレーショナルデータベースとMongoDBで異なります。
リレーショナルデータベース
リレーショナルデータベースでは、m-nリレーションは通常、リレーションテーブルを介してモデル化されます。m-nリレーションは、Prismaスキーマで明示的または暗黙的にすることができます。リレーションテーブル自体に追加のメタデータを保存する必要がない場合は、暗黙的なm-nリレーションを使用することをお勧めします。必要に応じて、後で明示的なm-nリレーションにいつでも移行できます。
明示的な多対多リレーション
明示的なm-nリレーションでは、リレーションテーブルはPrismaスキーマのモデルとして表現され、クエリで使用できます。明示的なm-nリレーションは3つのモデルを定義します。
Category
やPost
など、m-nリレーションを持つ2つのモデル。- 基盤となるデータベースのリレーションテーブルを表す1つのモデル(
CategoriesOnPosts
など。JOIN、link、pivotテーブルとも呼ばれます)。リレーションテーブルモデルのフィールドは、対応するリレーションスカラフィールド(postId
およびcategoryId
)を持つ、注釈付きリレーションフィールド(post
およびcategory
)の両方です。
リレーションテーブルCategoriesOnPosts
は、関連するPost
レコードとCategory
レコードを接続します。この例では、リレーションテーブルを表すモデルは、Post
/Category
リレーションシップを記述する追加のフィールドも定義します。誰がカテゴリを割り当てたか(assignedBy
)、およびカテゴリがいつ割り当てられたか(assignedAt
)です。
model Post {
id Int @id @default(autoincrement())
title String
categories CategoriesOnPosts[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts CategoriesOnPosts[]
}
model CategoriesOnPosts {
post Post @relation(fields: [postId], references: [id])
postId Int // relation scalar field (used in the `@relation` attribute above)
category Category @relation(fields: [categoryId], references: [id])
categoryId Int // relation scalar field (used in the `@relation` attribute above)
assignedAt DateTime @default(now())
assignedBy String
@@id([postId, categoryId])
}
基盤となるSQLは次のようになります。
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- Relation table + indexes --
CREATE TABLE "CategoriesOnPosts" (
"postId" INTEGER NOT NULL,
"categoryId" INTEGER NOT NULL,
"assignedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CategoriesOnPosts_pkey" PRIMARY KEY ("postId","categoryId")
);
ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
1対多リレーションと同じルールが適用されることに注意してください(Post
↔ CategoriesOnPosts
とCategory
↔ CategoriesOnPosts
はどちらも実際には1対多リレーションであるため)。つまり、リレーションの一方の側は@relation
属性で注釈を付ける必要があります。
リレーションに追加情報を付加する必要がない場合は、m-nリレーションを暗黙的なm-nリレーションとしてモデル化できます。Prisma Migrateを使用せず、イントロスペクションからデータモデルを取得している場合でも、Prisma ORMのリレーションテーブルの規約に従うことで、暗黙的なm-nリレーションを利用できます。
明示的な多対多のクエリ
次のセクションでは、明示的なm-nリレーションをクエリする方法を示します。リレーションモデルを直接クエリするか(prisma.categoriesOnPosts(...)
)、ネストされたクエリを使用してPost
-> CategoriesOnPosts
-> Category
またはその逆方向に移動できます。
次のクエリは3つのことを行います。
Post
を作成します- リレーションテーブル
CategoriesOnPosts
に新しいレコードを作成します - 新しく作成された
Post
レコードに関連付けられた新しいCategory
を作成します
const createCategory = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
create: {
name: 'New category',
},
},
},
],
},
},
})
次のクエリ
- 新しい
Post
を作成します - リレーションテーブル
CategoriesOnPosts
に新しいレコードを作成します - カテゴリの割り当てを既存のカテゴリ(ID
9
および22
)に接続します
const assignCategories = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connect: {
id: 9,
},
},
},
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connect: {
id: 22,
},
},
},
],
},
},
})
Category
レコードが存在するかどうか不明な場合があります。Category
レコードが存在する場合は、新しいPost
レコードをそのカテゴリに接続します。Category
レコードが存在しない場合は、最初にレコードを作成してから、新しいPost
レコードに接続します。次のクエリ
- 新しい
Post
を作成します - リレーションテーブル
CategoriesOnPosts
に新しいレコードを作成します - カテゴリの割り当てを既存のカテゴリ(ID
9
)に接続するか、存在しない場合は最初に新しいカテゴリを作成します
const assignCategories = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
connectOrCreate: {
where: {
id: 9,
},
create: {
name: 'New Category',
id: 9,
},
},
},
},
],
},
},
})
次のクエリは、少なくとも1つ(some
)のカテゴリ割り当て(categories
)が"New category"
という名前のカテゴリを参照するすべてのPost
レコードを返します。
const getPosts = await prisma.post.findMany({
where: {
categories: {
some: {
category: {
name: 'New Category',
},
},
},
},
})
次のクエリは、少なくとも1つ(some
)の関連するPost
レコードのタイトルに"Cool stuff"
という単語が含まれており、かつカテゴリがBobによって割り当てられたすべてのカテゴリを返します。
const getAssignments = await prisma.category.findMany({
where: {
posts: {
some: {
assignedBy: 'Bob',
post: {
title: {
contains: 'Cool stuff',
},
},
},
},
},
})
次のクエリは、5つの投稿のいずれかに"Bob"
によって割り当てられたすべてのカテゴリ割り当て(CategoriesOnPosts
)レコードを取得します。
const getAssignments = await prisma.categoriesOnPosts.findMany({
where: {
assignedBy: 'Bob',
post: {
id: {
in: [9, 4, 10, 12, 22],
},
},
},
})
暗黙的な多対多リレーション
暗黙的なm-nリレーションは、リレーションフィールドをリレーションの両側でリストとして定義します。リレーションテーブルは基盤となるデータベースに存在しますが、Prisma ORMによって管理され、Prismaスキーマには現れません。暗黙的なリレーションテーブルは、特定の規約に従います。
暗黙的なm-nリレーションにより、m-nリレーションのPrisma Client APIが少し単純になります(ネストされた書き込み内でネストのレベルが1つ少なくなるため)。
以下の例では、Post
とCategory
の間に1つの暗黙的なm-nリレーションがあります。
- リレーショナルデータベース
- MongoDB
model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
categoryIDs String[] @db.ObjectId
categories Category[] @relation(fields: [categoryIDs], references: [id])
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
postIDs String[] @db.ObjectId
posts Post[] @relation(fields: [postIDs], references: [id])
}
暗黙的な多対多のクエリ
次のセクションでは、暗黙的なm-nリレーションをクエリする方法を示します。クエリは、明示的なm-nクエリよりもネストが少なくて済みます。
次のクエリは、単一のPost
と複数のCategory
レコードを作成します。
const createPostAndCategory = await prisma.post.create({
data: {
title: 'How to become a butterfly',
categories: {
create: [{ name: 'Magic' }, { name: 'Butterflies' }],
},
},
})
次のクエリは、単一のCategory
と複数のPost
レコードを作成します。
const createCategoryAndPosts = await prisma.category.create({
data: {
name: 'Stories',
posts: {
create: [
{ title: 'That one time with the stuff' },
{ title: 'The story of planet Earth' },
],
},
},
})
次のクエリは、その投稿に割り当てられたカテゴリのリストを含むすべてのPost
レコードを返します。
const getPostsAndCategories = await prisma.post.findMany({
include: {
categories: true,
},
})
暗黙的なm-nリレーションを定義するためのルール
暗黙的なm-nリレーション
-
リレーションテーブルの特定の規約を使用します
-
名前(例:
@relation("MyRelation")
または@relation(name: "MyRelation")
)でリレーションを曖昧にする必要がある場合を除き、@relation
属性は不要です。 -
@relation
属性を使用する場合は、references
、fields
、onUpdate
、またはonDelete
引数を使用できません。これは、これらが暗黙的なm-nリレーションに対して固定値を持ち、変更できないためです。 -
両方のモデルに単一の
@id
が必要です。次の点に注意してください。- 複数フィールドIDは使用できません
@id
の代わりに@unique
を使用することはできません
情報これらの機能のいずれかを使用するには、代わりに明示的なm-nを使用する必要があります。
暗黙的なm-nリレーションにおけるリレーションテーブルの規約
イントロスペクションからデータモデルを取得する場合でも、Prisma ORMのリレーションテーブルの規約に従うことで、暗黙的なm-nリレーションを使用できます。次の例では、Post
とCategory
という2つのモデルの暗黙的なm-nリレーションを取得するためのリレーションテーブルを作成すると仮定しています。
リレーションテーブル
リレーションテーブルをイントロスペクションによって暗黙的なm-nリレーションとして認識させるには、名前が次の正確な構造に従う必要があります。
- アンダースコア
_
で始まる必要があります - 次に、アルファベット順の最初のモデルの名前(この場合は
Category
) - 次に、リレーションシップ(この場合は
To
) - 次に、アルファベット順の2番目のモデルの名前(この場合は
Post
)
例では、正しいテーブル名は_CategoryToPost
です。
Prismaスキーマファイルで暗黙的なm-nリレーションを自分で作成する場合は、リレーションの名前を設定して、別の名前を付けることができます。これにより、データベースのリレーションテーブルに付けられる名前が変更されます。たとえば、"MyRelation"
という名前のリレーションの場合、対応するテーブルは_MyRelation
と呼ばれます。
マルチスキーマ
暗黙的な多対多リレーションシップが複数のデータベーススキーマにまたがる場合(multiSchema
プレビュー機能を使用)、リレーションテーブル(上記の例では_CategoryToPost
という名前)は、アルファベット順の最初のモデル(この場合はCategory
)と同じデータベーススキーマに存在する必要があります。
カラム
暗黙的なm-nリレーションのリレーションテーブルには、正確に2つのカラムが必要です。
Category
を指す外部キーカラム(A
と呼ばれる)Post
を指す外部キーカラム(B
と呼ばれる)
カラムはA
とB
という名前である必要があり、A
はアルファベット順で最初にくるモデルを指し、B
はアルファベット順で最後にくるモデルを指します。
インデックス
さらに、次のものが必要です。
-
両方の外部キーカラムに定義された一意のインデックス
CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A" int4_ops,"B" int4_ops);
-
Bに定義された非一意のインデックス
CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B" int4_ops);
例
これは、Prismaイントロスペクションによって暗黙的なm-nリレーションとして認識されるインデックスを含む3つのテーブルを作成するサンプルSQLステートメント(PostgreSQLダイアレクト)です。
CREATE TABLE "_CategoryToPost" (
"A" integer NOT NULL REFERENCES "Category"(id) ,
"B" integer NOT NULL REFERENCES "Post"(id)
);
CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A" int4_ops,"B" int4_ops);
CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B" int4_ops);
CREATE TABLE "Category" (
id integer SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
id integer SERIAL PRIMARY KEY
);
また、異なるリレーションシップ名を使用することで、2つのテーブル間に複数の多対多リレーションを定義できます。この例は、このような場合にPrismaイントロスペクションがどのように機能するかを示しています。
CREATE TABLE IF NOT EXISTS "User" (
"id" SERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS "Video" (
"id" SERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS "_UserLikedVideos" (
"A" SERIAL NOT NULL,
"B" SERIAL NOT NULL,
CONSTRAINT "_UserLikedVideos_A_fkey" FOREIGN KEY ("A") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_UserLikedVideos_B_fkey" FOREIGN KEY ("B") REFERENCES "Video" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS "_UserDislikedVideos" (
"A" SERIAL NOT NULL,
"B" SERIAL NOT NULL,
CONSTRAINT "_UserDislikedVideos_A_fkey" FOREIGN KEY ("A") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "_UserDislikedVideos_B_fkey" FOREIGN KEY ("B") REFERENCES "Video" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "_UserLikedVideos_AB_unique" ON "_UserLikedVideos"("A", "B");
CREATE INDEX "_UserLikedVideos_B_index" ON "_UserLikedVideos"("B");
CREATE UNIQUE INDEX "_UserDislikedVideos_AB_unique" ON "_UserDislikedVideos"("A", "B");
CREATE INDEX "_UserDislikedVideos_B_index" ON "_UserDislikedVideos"("B");
このデータベースでprisma db pull
を実行すると、Prisma CLIはイントロスペクションを通じて次のスキーマを生成します。
model User {
id Int @id @default(autoincrement())
Video_UserDislikedVideos Video[] @relation("UserDislikedVideos")
Video_UserLikedVideos Video[] @relation("UserLikedVideos")
}
model Video {
id Int @id @default(autoincrement())
User_UserDislikedVideos User[] @relation("UserDislikedVideos")
User_UserLikedVideos User[] @relation("UserLikedVideos")
}
暗黙的な多対多リレーションにおけるリレーションテーブルの名前の設定
Prisma Migrateを使用すると、@relation
属性を使用してPrisma ORMによって管理されるリレーションテーブルの名前を設定できます。たとえば、リレーションテーブルの名前をデフォルト名の_CategoryToPost
ではなく_MyRelationTable
にしたい場合は、次のように指定できます。
model Post {
id Int @id @default(autoincrement())
categories Category[] @relation("MyRelationTable")
}
model Category {
id Int @id @default(autoincrement())
posts Post[] @relation("MyRelationTable")
}
リレーションテーブル
リレーションテーブル(JOIN、link、またはpivotテーブルとも呼ばれます)は、2つ以上の他のテーブルを接続し、それらの間にリレーションを作成します。リレーションテーブルの作成は、SQLで異なるエンティティ間のリレーションシップを表すための一般的なデータモデリングプラクティスです。本質的に、これは「1つのm-nリレーションがデータベースで2つの1-nリレーションとしてモデル化される」ことを意味します。
暗黙的なm-nリレーションを使用することをお勧めします。Prisma ORMは、基盤となるデータベースでリレーションテーブルを自動的に生成します。明示的なm-nリレーションは、リレーションが作成された日付など、リレーションに追加データを保存する必要がある場合に使用する必要があります。
MongoDB
MongoDBでは、m-nリレーションは次のように表現されます。
- 両側のリレーションフィールド。それぞれに
@relation
属性があり、必須のfields
およびreferences
引数があります。 - 各側の参照IDのスカラーリスト。型は反対側のIDフィールドと一致します。
次の例は、投稿とカテゴリ間のm-nリレーションを示しています。
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
categoryIDs String[] @db.ObjectId
categories Category[] @relation(fields: [categoryIDs], references: [id])
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
postIDs String[] @db.ObjectId
posts Post[] @relation(fields: [postIDs], references: [id])
}
Prisma ORMは、次のルールでMongoDBのm-nリレーションを検証します。
- リレーションの両側のフィールドはリスト型である必要があります(上記の例では、
categories
の型はCategory[]
、posts
の型はPost[]
です)。 @relation
属性は、両側でfields
およびreferences
引数を定義する必要があります。fields
引数は、リスト型のスカラーフィールドを1つだけ定義する必要があります。references
引数は、スカラーフィールドを1つだけ定義する必要があります。このスカラーフィールドは、参照先のモデルに存在する必要があり、fields
引数のスカラーフィールドと同じ型である必要がありますが、単数形(リストではない)である必要があります。references
が指すスカラーフィールドには、@id
属性が必要です。@relation
では参照アクションは許可されていません。
リレーショナルデータベースで使用される暗黙的なm-nリレーションは、MongoDBではサポートされていません。
MongoDB多対多リレーションのクエリ
このセクションでは、上記のスキーマ例を使用して、MongoDBでm-nリレーションをクエリする方法を示します。
次のクエリは、特定のカテゴリIDに一致する投稿を検索します。
const newId1 = new ObjectId()
const newId2 = new ObjectId()
const posts = await prisma.post.findMany({
where: {
categoryIDs: {
hasSome: [newId1.toHexString(), newId2.toHexString()],
},
},
})
次のクエリは、カテゴリ名に文字列'Servers'
が含まれている投稿を検索します。
const posts = await prisma.post.findMany({
where: {
categories: {
some: {
name: {
contains: 'Servers',
},
},
},
},
})