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

多対多リレーション

多対多 (m-n) リレーションとは、リレーションの一方の側に0個以上のレコードが、もう一方の側に0個以上のレコードに接続できるリレーションを指します。

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

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

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

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

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

  • CategoryPostのようなm-nリレーションを持つ2つのモデル。
  • 基盤となるデータベースのリレーションテーブルを表す1つのモデル(CategoriesOnPostsなど。JOINlinkpivotテーブルとも呼ばれる)。リレーションテーブルモデルのフィールドは、対応するリレーションスカラーフィールド(postIdcategoryId)を持つ注釈付きリレーションフィールド(postcategory)の両方です。

リレーションテーブル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リレーションと同じルールが適用されることに注意してください(PostCategoriesOnPostsCategoryCategoriesOnPostsはどちらも実際には1対nリレーションであるため)。これは、リレーションの一方の側に@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つのカテゴリ割り当て(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つ減るため)。

以下の例では、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が必要です。以下の点に注意してください。

    • 複数フィールドIDを使用することはできません。
    • @idの代わりに@uniqueを使用することはできません。
    情報

    これらの機能のいずれかを使用するには、代わりに明示的な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 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")
}

リレーションテーブル

リレーションテーブル(JOINlink、または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リレーションを以下のルールで検証します。

  • リレーションの両側のフィールドはリスト型である必要があります(上記の例では、categoriesCategory[]型、postsPost[]型です)。
  • @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',
},
},
},
},
})
© . All rights reserved.