Skip to main content

多对多关系

多对多 (m-n) 关系是指关系一侧的零个或多个记录可以连接到另一侧的零个或多个记录的关系。

¥Many-to-many (m-n) relations refer to relations where zero or more records on one side of the relation can be connected to zero or more records on the other side.

关系数据库MongoDB 之间的 Prisma 架构语法和底层数据库中的实现有所不同。

¥Prisma schema syntax and the implementation in the underlying database differs between relational databases and MongoDB.

关系数据库

¥Relational databases

在关系数据库中,m-n-关系通常通过 关系表 建模。在 Prisma 模式中,m-n-关系可以是 explicitimplicit。如果你不需要在关系表本身中存储任何其他元数据,我们建议使用 implicit m-n-relations。如果需要,你可以稍后迁移到 explicit m-n-关系。

¥In relational databases, m-n-relations are typically modelled via relation tables. m-n-relations can be either explicit or implicit in the Prisma schema. We recommend using implicit m-n-relations if you do not need to store any additional meta-data in the relation table itself. You can always migrate to an explicit m-n-relation later if needed.

显式多对多关系

¥Explicit many-to-many relations

在显式 m-n 关系中,关系表表示为 Prisma 模式中的模型,并且可以在查询中使用。显式的 m-n 关系定义了三个模型:

¥In an explicit m-n relation, the relation table is represented as a model in the Prisma schema and can be used in queries. Explicit m-n relations define three models:

  • 具有 m-n 关系的两个模型,例如 CategoryPost

    ¥Two models with m-n relation, such as Category and Post.

  • 代表 关系表 的一种模型,例如底层数据库中的 CategoriesOnPosts(有时也称为 JOIN、链接或数据透视表)。关系表模型的字段都是带注释的关系字段(postcategory)以及相应的关系标量字段(postIdcategoryId)。

    ¥One model that represents the relation table, such as CategoriesOnPosts (also sometimes called JOIN, link or pivot table) in the underlying database. The fields of a relation table model are both annotated relation fields (post and category) with a corresponding relation scalar field (postId and categoryId).

关系表 CategoriesOnPosts 连接相关的 PostCategory 记录。在此示例中,表示关系表的模型还定义了描述 Post/Category 关系的附加字段 - 谁分配了类别 (assignedBy),以及分配类别的时间 (assignedAt):

¥The relation table CategoriesOnPosts connects related Post and Category records. In this example, the model representing the relation table also defines additional fields that describe the Post/Category relationship - who assigned the category (assignedBy), and when the category was assigned (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 如下所示:

¥The underlying SQL looks like this:

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 属性进行注释。

¥Note that the same rules as for 1-n relations apply (because PostCategoriesOnPosts and CategoryCategoriesOnPosts are both in fact 1-n relations), which means one side of the relation needs to be annotated with the @relation attribute.

当你不需要向关系附加附加信息时,可以将 m-n-关系建模为 隐含的 m-n-关系。如果你不使用 Prisma Migrate,而是从 introspection 获取数据模型,你仍然可以按照 Prisma ORM 的 关系表的约定 使用隐式 m-n-关系。

¥When you don't need to attach additional information to the relation, you can model m-n-relations as implicit m-n-relations. If you're not using Prisma Migrate but obtain your data model from introspection, you can still make use of implicit m-n-relations by following Prisma ORM's conventions for relation tables.

查询显式多对多

¥Querying an explicit many-to-many

以下部分演示如何查询显式 m-n-关系。你可以直接查询关系模型(prisma.categoriesOnPosts(...)),也可以使用嵌套查询从 Post -> CategoriesOnPosts -> Category 或其他方式。

¥The following section demonstrates how to query an explicit m-n-relation. You can query the relation model directly (prisma.categoriesOnPosts(...)), or use nested queries to go from Post -> CategoriesOnPosts -> Category or the other way.

以下查询做了三件事:

¥The following query does three things:

  1. 创建 Post

    ¥Creates a Post

  2. 在关系表 CategoriesOnPosts 中创建一条新记录

    ¥Creates a new record in the relation table CategoriesOnPosts

  3. 创建与新创建的 Post 记录关联的新 Category

    ¥Creates a new Category that is associated with the newly created Post record

const createCategory = await prisma.post.create({
data: {
title: 'How to be Bob',
categories: {
create: [
{
assignedBy: 'Bob',
assignedAt: new Date(),
category: {
create: {
name: 'New category',
},
},
},
],
},
},
})

以下查询:

¥The following query:

  • 创建一个新的 Post

    ¥Creates a new Post

  • 在关系表 CategoriesOnPosts 中创建一条新记录

    ¥Creates a new record in the relation table CategoriesOnPosts

  • 将类别分配连接到现有类别(具有 ID 922

    ¥Connects the category assignment to existing categories (with IDs 9 and 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 记录。以下查询:

¥Sometimes you might not know if a Category record exists. If the Category record exists, you want to connect a new Post record to that category. If the Category record does not exist, you want to create the record first and then connect it to the new Post record. The following query:

  1. 创建一个新的 Post

    ¥Creates a new Post

  2. 在关系表 CategoriesOnPosts 中创建一条新记录

    ¥Creates a new record in the relation table CategoriesOnPosts

  3. 将类别分配连接到现有类别(ID 9),或者首先创建一个新类别(如果不存在)

    ¥Connects the category assignment to an existing category (with ID 9), or creates a new category first if it does not exist

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,
},
},
},
},
],
},
},
})

以下查询返回所有 Post 记录,其中至少一个 (some) 类别分配 (categories) 引用名为 "New category" 的类别:

¥The following query returns all Post records where at least one (some) category assignment (categories) refers to a category named "New category":

const getPosts = await prisma.post.findMany({
where: {
categories: {
some: {
category: {
name: 'New Category',
},
},
},
},
})

以下查询返回所有类别,其中至少一个 (some) 相关 Post 记录标题包含单词 "Cool stuff" 并且该类别是由 Bob 分配的。

¥The following query returns all categories where at least one (some) related Post record titles contain the words "Cool stuff" and the category was assigned by Bob.

const getAssignments = await prisma.category.findMany({
where: {
posts: {
some: {
assignedBy: 'Bob',
post: {
title: {
contains: 'Cool stuff',
},
},
},
},
},
})

以下查询获取 "Bob" 分配给 5 个帖子之一的所有类别分配 (CategoriesOnPosts) 记录:

¥The following query gets all category assignments (CategoriesOnPosts) records that were assigned by "Bob" to one of 5 posts:

const getAssignments = await prisma.categoriesOnPosts.findMany({
where: {
assignedBy: 'Bob',
post: {
id: {
in: [9, 4, 10, 12, 22],
},
},
},
})

隐式多对多关系

¥Implicit many-to-many relations

隐式 m-n 关系将关系字段定义为关系两侧的列表。尽管关系表存在于底层数据库中,但它由 Prisma ORM 管理,并且不体现在 Prisma 模式中。隐式关系表遵循 具体约定

¥Implicit m-n relations define relation fields as lists on both sides of the relation. Although the relation table exists in the underlying database, it is managed by Prisma ORM and does not manifest in the Prisma schema. Implicit relation tables follow a specific convention.

隐式 m-n-关系使得 m-n-关系的 Prisma 客户端 API 更简单一些(因为 嵌套写入 内部的嵌套级别少了一层)。

¥Implicit m-n-relations makes the Prisma Client API for m-n-relations a bit simpler (since you have one fewer level of nesting inside of nested writes).

在下面的示例中,PostCategory 之间存在一个隐式 m-n 关系:

¥In the example below, there's one implicit m-n-relation between Post and Category:

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

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

查询隐式多对多

¥Querying an implicit many-to-many

以下部分演示如何查询 隐式 m-n 关系。这些查询需要的嵌套少于 显式 m-n 查询

¥The following section demonstrates how to query an implicit m-n relation. The queries require less nesting than explicit m-n queries.

以下查询创建单个 Post 和多个 Category 记录:

¥The following query creates a single Post and multiple Category records:

const createPostAndCategory = await prisma.post.create({
data: {
title: 'How to become a butterfly',
categories: {
create: [{ name: 'Magic' }, { name: 'Butterflies' }],
},
},
})

以下查询创建单个 Category 和多个 Post 记录:

¥The following query creates a single Category and multiple Post records:

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 记录以及该帖子指定类别的列表:

¥The following query returns all Post records with a list of that post's assigned categories:

const getPostsAndCategories = await prisma.post.findMany({
include: {
categories: true,
},
})

定义隐式 m-n 关系的规则

¥Rules for defining an implicit m-n relation

隐式 m-n 关系:

¥Implicit m-n relations:

  • 使用特定的 关系表约定

    ¥Use a specific convention for relation tables

  • 不需要 @relation 属性,除非你需要 消除关系的歧义 具有名称,例如 @relation("MyRelation")@relation(name: "MyRelation")

    ¥Do not require the @relation attribute unless you need to disambiguate relations with a name, e.g. @relation("MyRelation") or @relation(name: "MyRelation").

  • 如果确实使用 @relation 属性,则不能使用 referencesfieldsonUpdateonDelete 参数。这是因为它们对于隐式 m-n-关系采用固定值并且无法更改。

    ¥If you do use the @relation attribute, you cannot use the references, fields, onUpdate or onDelete arguments. This is because these take a fixed value for implicit m-n-relations and cannot be changed.

  • 要求两个型号都具有一个 @id。意识到:

    ¥Require both models to have a single @id. Be aware that:

    • 你不能使用 多字段 ID

      ¥You cannot use a multi-field ID

    • 你不能使用 @unique 代替 @id

      ¥You cannot use a @unique in place of an @id

    info

    要使用这些功能之一,你必须使用 显式 m-n 代替

    ¥To use either of these features, you must use an explicit m-n instead.

隐式 m-n 关系中关系表的约定

¥Conventions for relation tables in implicit m-n relations

如果你从 introspection 获取数据模型,你仍然可以按照 Prisma ORM 的 关系表的约定 使用隐式 m-n-关系。以下示例假设你要创建一个关系表来获取名为 PostCategory 的两个模型的隐式 m-n-关系。

¥If you obtain your data model from introspection, you can still use implicit m-n-relations by following Prisma ORM's conventions for relation tables. The following example assumes you want to create a relation table to get an implicit m-n-relation for two models called Post and Category.

关系表

¥Relation table

如果你希望通过内省将关系表选取为隐式 m-n-关系,则名称必须遵循以下精确结构:

¥If you want a relation table to be picked up by introspection as an implicit m-n-relation, the name must follow this exact structure:

  • 必须以下划线 _ 开头

    ¥It must start with an underscore _

  • 然后是按字母顺序排列的第一个模型的名称(在本例中为 Category

    ¥Then the name of the first model in alphabetical order (in this case Category)

  • 然后是关系(本例为 To

    ¥Then the relationship (in this case To)

  • 然后是按字母顺序排列的第二个模型的名称(在本例中为 Post

    ¥Then the name of the second model in alphabetical order (in this case Post)

在示例中,正确的表名称是 _CategoryToPost

¥In the example, the correct table name is _CategoryToPost.

当你自己在 Prisma 架构文件中创建隐式 m-n-关系时,你可以 配置关系 以获得不同的名称。这将更改数据库中关系表的名称。例如,对于名为 "MyRelation" 的关系,相应的表将称为 _MyRelation

¥When creating an implicit m-n-relation yourself in the Prisma schema file, you can configure the relation to have a different name. This will change the name given to the relation table in the database. For example, for a relation named "MyRelation" the corresponding table will be called _MyRelation.

多模式

¥Multi-schema

如果你的隐式多对多关系跨越多个数据库模式(使用 multiSchema 预览功能),则关系表(其名称直接在上面定义,在示例 _CategoryToPost 中)必须与第一个模型按字母顺序出现在同一数据库模式中 (在本例中为 Category)。

¥If your implicit many-to-many relationship spans multiple database schemas (using the multiSchema preview feature), the relation table (with the name defined directly above, in the example _CategoryToPost) must be present in the same database schema as the first model in alphabetical order (in this case Category).

¥Columns

隐式 m-n-关系的关系表必须恰好有两列:

¥A relation table for an implicit m-n-relation must have exactly two columns:

  • 指向 Category 的外键列(称为 A

    ¥A foreign key column that points to Category called A

  • 指向 Post 的外键列(称为 B

    ¥A foreign key column that points to Post called B

这些列必须称为 AB,其中 A 指向字母表中第一个的模型,B 指向字母表中最后一个的模型。

¥The columns must be called A and B where A points to the model that comes first in the alphabet and B points to the model which comes last in the alphabet.

索引

¥Indexes

还必须有:

¥There further must be:

  • 在两个外键列上定义的唯一索引:

    ¥A unique index defined on both foreign key columns:

    CREATE UNIQUE INDEX "_CategoryToPost_AB_unique" ON "_CategoryToPost"("A" int4_ops,"B" int4_ops);
  • B 上定义的非唯一索引:

    ¥A non-unique index defined on B:

    CREATE INDEX "_CategoryToPost_B_index" ON "_CategoryToPost"("B" int4_ops);
示例

¥Example

这是一个示例 SQL 语句,它将创建三个表,包括由 Prisma Introspection 作为隐式 m-n-关系拾取的索引(在 PostgreSQL 方言中):

¥This is a sample SQL statement that would create the three tables including indexes (in PostgreSQL dialect) that are picked up as a implicit m-n-relation by Prisma Introspection:

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
);

你可以使用不同的关系名称在两个表之间定义多个多对多关系。这个例子展示了 Prisma 内省在这种情况下是如何工作的:

¥And you can define multiple many-to-many relations between two tables by using the different relationship name. This example shows how the Prisma introspection works under such case:

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 将通过内省生成以下架构:

¥If you run prisma db pull on this database, the Prisma CLI will generate the following schema through introspection:

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")
}

配置隐式多对多关系中的关系表名称

¥Configuring the name of the relation table in implicit many-to-many relations

使用 Prisma Migrate 时,你可以使用 @relation 属性配置 Prisma ORM 管理的关系表的名称。例如,如果你希望关系表名为 _MyRelationTable 而不是默认名称 _CategoryToPost,则可以如下指定:

¥When using Prisma Migrate, you can configure the name of the relation table that's managed by Prisma ORM using the @relation attribute. For example, if you want the relation table to be called _MyRelationTable instead of the default name _CategoryToPost, you can specify it as follows:

model Post {
id Int @id @default(autoincrement())
categories Category[] @relation("MyRelationTable")
}

model Category {
id Int @id @default(autoincrement())
posts Post[] @relation("MyRelationTable")
}

关系表

¥Relation tables

关系表(有时也称为 JOIN、链接或数据透视表)连接两个或多个其他表,从而在它们之间创建关系。创建关系表是 SQL 中常见的数据建模实践,用于表示不同实体之间的关系。本质上它意味着 "数据库中的一个 m-n 关系被建模为两个 1-n 关系"。

¥A relation table (also sometimes called a JOIN, link or pivot table) connects two or more other tables and therefore creates a relation between them. Creating relation tables is a common data modelling practice in SQL to represent relationships between different entities. In essence it means that "one m-n relation is modeled as two 1-n relations in the database".

我们建议使用 implicit m-n-relations,其中 Prisma ORM 自动在底层数据库中生成关系表。当你需要在关系中存储附加数据(例如创建关系的日期)时,应使用 显式的 m-n-relations。

¥We recommend using implicit m-n-relations, where Prisma ORM automatically generates the relation table in the underlying database. Explicit m-n-relations should be used when you need to store additional data in the relations, such as the date the relation was created.

MongoDB

在 MongoDB 中,m-n-关系表示为:

¥In MongoDB, m-n-relations are represented by:

  • 双方的关系字段,每个字段都有一个 @relation 属性,带有强制 fieldsreferences 参数

    ¥relation fields on both sides, that each have a @relation attribute, with mandatory fields and references arguments

  • 每侧引用 ID 的标量列表,其类型与另一侧的 ID 字段匹配

    ¥a scalar list of referenced IDs on each side, with a type that matches the ID field on the other side

以下示例演示了帖子和类别之间的 m-n 关系:

¥The following example demonstrates a m-n-relation between posts and categories:

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-关系:

¥Prisma ORM validates m-n-relations in MongoDB with the following rules:

  • 关系两侧的字段必须具有列表类型(在上面的示例中,categories 的类型为 Category[]posts 的类型为 Post[]

    ¥The fields on both sides of the relation must have a list type (in the example above, categories have a type of Category[] and posts have a type of Post[])

  • @relation 属性必须在两侧定义 fieldsreferences 参数

    ¥The @relation attribute must define fields and references arguments on both sides

  • fields 参数只能定义一个标量字段,该字段必须是列表类型

    ¥The fields argument must have only one scalar field defined, which must be of a list type

  • references 参数必须仅定义一个标量字段。该标量字段必须存在于引用模型上,并且必须与 fields 参数中的标量字段具有相同类型,但为单数(无列表)

    ¥The references argument must have only one scalar field defined. This scalar field must exist on the referenced model and must be of the same type as the scalar field in the fields argument, but singular (no list)

  • references 指向的标量域必须具有 @id 属性

    ¥The scalar field to which references points must have the @id attribute

  • @relation 中不允许有 参考行动

    ¥No referential actions are allowed in @relation

MongoDB 不支持隐式 m-n-关系 用于关系数据库

¥The implicit m-n-relations used in relational databases are not supported on MongoDB.

查询 MongoDB 多对多关系

¥Querying MongoDB many-to-many relations

本节演示如何使用上面的示例架构在 MongoDB 中查询 m-n-关系。

¥This section demonstrates how to query m-n-relations in MongoDB, using the example schema above.

以下查询查找具有特定匹配类别 ID 的帖子:

¥The following query finds posts with specific matching category IDs:

const newId1 = new ObjectId()
const newId2 = new ObjectId()

const posts = await prisma.post.findMany({
where: {
categoryIDs: {
hasSome: [newId1.toHexString(), newId2.toHexString()],
},
},
})

以下查询查找类别名称包含字符串 'Servers' 的帖子:

¥The following query finds posts where the category name contains the string 'Servers':

const posts = await prisma.post.findMany({
where: {
categories: {
some: {
name: {
contains: 'Servers',
},
},
},
},
})