多対多リレーション
多対多 (m-n) リレーションとは、リレーションの一方の側に0個以上のレコードが、もう一方の側に0個以上のレコードに接続できるリレーションを指します。
Prismaスキーマの構文と、基盤となるデータベースでの実装は、リレーショナルデータベースとMongoDBで異なります。
リレーショナルデータベース
リレーショナルデータベースでは、m-nリレーションは通常、リレーションテーブルを介してモデル化されます。Prismaスキーマでは、m-nリレーションは明示的または暗黙的のいずれかです。リレーションテーブル自体に追加のメタデータを保存する必要がない場合は、暗黙的な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対nリレーションと同じルールが適用されることに注意してください(Post
↔ CategoriesOnPosts
とCategory
↔ CategoriesOnPosts
はどちらも実際には1対nリレーションであるため)。これは、リレーションの一方の側に@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つのカテゴリ割り当て(categories
)が"New category"
という名前のカテゴリを参照するすべてのPost
レコードを返します。
const getPosts = await prisma.post.findMany({
where: {
categories: {
some: {
category: {
name: 'New Category',
},
},
},
},
})
以下のクエリは、少なくとも1つの関連するPost
レコードのタイトルに"Cool stuff"
という単語が含まれ、かつ、そのカテゴリがBobによって割り当てられたすべてのカテゴリを返します。
const getAssignments = await prisma.category.findMany({
where: {
posts: {
some: {
assignedBy: 'Bob',
post: {
title: {
contains: 'Cool stuff',
},
},
},
},
},
})
以下のクエリは、"Bob"
によって5つの投稿のいずれかに割り当てられたすべてのカテゴリ割り当て(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 Introspectionによって暗黙的な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リレーションとしてモデル化される」ことを意味します。
Prisma ORMが基盤となるデータベースにリレーションテーブルを自動的に生成する暗黙的なm-nリレーションを使用することをお勧めします。リレーションが作成された日付など、リレーションに追加データを保存する必要がある場合は、明示的な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
では参照アクションは許可されません。
MongoDBでは、リレーショナルデータベースで使用される暗黙的なm-nリレーションはサポートされていません。
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',
},
},
},
},
})