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

多対多リレーション

多対多(m-n)リレーションとは、リレーションの一方の側のゼロ以上のレコードが、もう一方の側のゼロ以上のレコードに接続できるリレーションのことです。

Prismaスキーマの構文と、基盤となるデータベースでの実装は、リレーショナルデータベースMongoDBで異なります。

リレーショナルデータベース

リレーショナルデータベースでは、m-nリレーションは通常、リレーションテーブルを介してモデル化されます。m-nリレーションは、Prismaスキーマで明示的または暗黙的にすることができます。リレーションテーブル自体に追加のメタデータを保存する必要がない場合は、暗黙的なm-nリレーションを使用することをお勧めします。必要に応じて、後で明示的なm-nリレーションにいつでも移行できます。

明示的な多対多リレーション

明示的なm-nリレーションでは、リレーションテーブルはPrismaスキーマのモデルとして表現され、クエリで使用できます。明示的なm-nリレーションは3つのモデルを定義します。

  • CategoryPostなど、m-nリレーションを持つ2つのモデル。
  • 基盤となるデータベースのリレーションテーブルを表す1つのモデル(CategoriesOnPostsなど。JOINlinkpivotテーブルとも呼ばれます)。リレーションテーブルモデルのフィールドは、対応するリレーションスカラフィールド(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対多リレーションと同じルールが適用されることに注意してください(PostCategoriesOnPostsCategoryCategoriesOnPostsはどちらも実際には1対多リレーションであるため)。つまり、リレーションの一方の側は@relation属性で注釈を付ける必要があります。

リレーションに追加情報を付加する必要がない場合は、m-nリレーションを暗黙的なm-nリレーションとしてモデル化できます。Prisma Migrateを使用せず、イントロスペクションからデータモデルを取得している場合でも、Prisma ORMのリレーションテーブルの規約に従うことで、暗黙的なm-nリレーションを利用できます。

明示的な多対多のクエリ

次のセクションでは、明示的なm-nリレーションをクエリする方法を示します。リレーションモデルを直接クエリするか(prisma.categoriesOnPosts(...))、ネストされたクエリを使用してPost -> CategoriesOnPosts -> Categoryまたはその逆方向に移動できます。

次のクエリは3つのことを行います。

  1. Postを作成します
  2. リレーションテーブルCategoriesOnPostsに新しいレコードを作成します
  3. 新しく作成された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レコードに接続します。次のクエリ

  1. 新しいPostを作成します
  2. リレーションテーブルCategoriesOnPostsに新しいレコードを作成します
  3. カテゴリの割り当てを既存のカテゴリ(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つ少なくなるため)。

以下の例では、PostCategoryの間に1つの暗黙的なm-nリレーションがあります。

model Post {
id Int @id @default(autoincrement())
title String
categories Category[]
}

model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}

暗黙的な多対多のクエリ

次のセクションでは、暗黙的な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属性を使用する場合は、referencesfieldsonUpdate、またはonDelete引数を使用できません。これは、これらが暗黙的なm-nリレーションに対して固定値を持ち、変更できないためです。

  • 両方のモデルに単一の@idが必要です。次の点に注意してください。

    情報

    これらの機能のいずれかを使用するには、代わりに明示的なm-nを使用する必要があります。

暗黙的なm-nリレーションにおけるリレーションテーブルの規約

イントロスペクションからデータモデルを取得する場合でも、Prisma ORMのリレーションテーブルの規約に従うことで、暗黙的なm-nリレーションを使用できます。次の例では、PostCategoryという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と呼ばれる)

カラムはABという名前である必要があり、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")
}

リレーションテーブル

リレーションテーブル(JOINlink、または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',
},
},
},
},
})