Skip to content

多对多

ManyToMany(to, through) 具有必需的参数 to 和可选的 through ,它采用目标和关系模型类。

Sqlalchemy 列和类型自动从目标模型中获取。

  • Sqlalchemy 列:目标模型主键列的类
  • 类型(用于 pydantic):目标模型的类型

定义模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from typing import List, Optional

import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Author(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="authors")

    id: int = ormar.Integer(primary_key=True)
    first_name: str = ormar.String(max_length=80)
    last_name: str = ormar.String(max_length=80)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=40)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts")

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories: Optional[List[Category]] = ormar.ManyToMany(Category)
    author: Optional[Author] = ormar.ForeignKey(Author)

创建样本数据:

1
2
3
guido = await Author.objects.create(first_name="Guido", last_name="Van Rossum")
post = await Post.objects.create(title="Hello, M2M", author=guido)
news = await Category.objects.create(name="News")

反向关系

外键字段会自动注册关系的反面。

默认情况下,它是子(源)模型名称 + s,如下面代码片段中的帖子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Category(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=40)


class Post(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories: Optional[List[Category]] = ormar.ManyToMany(Category)

# create some sample data
post = await Post.objects.create(title="Hello, M2M")
news = await Category.objects.create(name="News")
await post.categories.add(news)

# now you can query and access from both sides:
post_check = Post.objects.select_related("categories").get()
assert post_check.categories[0] == news

# query through auto registered reverse side
category_check = Category.objects.select_related("posts").get()
assert category_check.posts[0] == post

反向关系公开 API 来从父端管理相关对象。

相关名称

默认情况下,相关名称的生成方式与外键关系 (class.name.lower()+'s') 的生成方式相同,但您也可以通过提供相关名称参数来覆盖此名称,如下所示:

1
2
3
categories: Optional[Union[Category, List[Category]]] = ormar.ManyToMany(
        Category, through=PostCategory, related_name="new_categories"
    )

!!!警告当您向同一模型提供多个关系时,否则将无法再为您自动生成 related_name。因此,在这种情况下,您必须为除一个字段(一个可以是默认值并生成)之外的所有字段或所有相关字段提供 related_name。

跳过反向关系

如果您确定不想要反向关系,可以使用 ManyToMany 的skip_reverse=True 标志。

如果您在内部设置了skip_reverse标志,该字段仍然注册在关系的另一端,因此您可以:

  • 按反向模型中的相关模型字段进行过滤
  • order_by by 反向模型中的相关模型字段

但你不能:

  • 使用 related_name 从反向模型访问相关字段
  • 即使您从模型的反面选择相关,返回的模型也不会填充在反向实例中(不会阻止连接,因此您仍然可以对关系进行过滤和排序)
  • 该关系不会填充在 model_dump() 和 json() 中
  • 从字典或 json 填充时(也通过 fastapi),您无法传递嵌套的相关对象。根据 pydantic 配置中的额外设置,它将被忽略或引发错误。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Category(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="categories")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=40)


class Post(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories: Optional[List[Category]] = ormar.ManyToMany(Category, skip_reverse=True)

# create some sample data
post = await Post.objects.create(title="Hello, M2M")
news = await Category.objects.create(name="News")
await post.categories.add(news)

assert post.categories[0] == news  # ok
assert news.posts  # Attribute error!

# but still can use in order_by
categories = (
    await Category.objects.select_related("posts").order_by("posts__title").all()
)
assert categories[0].first_name == "Test"

# note that posts are not populated for author even if explicitly
# included in select_related - note no posts in model_dump()
assert news.model_dump(exclude={"id"}) == {"name": "News"}

# still can filter through fields of related model
categories = await Category.objects.filter(posts__title="Hello, M2M").all()
assert categories[0].name == "News"
assert len(categories) == 1

通过模型

或者,如果您想添加其他字段,您可以显式创建并传递模型类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id = ormar.Integer(primary_key=True)
    name = ormar.String(max_length=40)


class PostCategory(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts_x_categories")

    id: int = ormar.Integer(primary_key=True)
    sort_order: int = ormar.Integer(nullable=True)
    param_name: str = ormar.String(default="Name", max_length=200)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories = ormar.ManyToMany(Category, through=PostCategory)

!!!警告请注意,即使您不通过模型提供,它也会自动为您创建,并且仍然必须包含在 alembic 迁移的示例中。

!!!tip 请注意,如果您想自定义Through 模型名称或该模型的数据库表名称,则需要提供Through 模型。

如果您不提供“通过”字段,系统将为您生成该字段。

默认命名约定是:

  • 对于类名,它是两个类名(父级+其他)的联合,因此在上面的示例中它将是 PostCategory
  • 对于表名,它类似,但在类小写名称末尾有下划​​线和 s,在上面的示例中将是 posts_categorys

通过关系名称自定义

默认情况下,通过模型关系名称默认为小写的相关模型名称。

所以在这样的例子中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
... # course declaration omitted
class Student(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    courses = ormar.ManyToMany(Course)

# will produce default Through model like follows (example simplified)
class StudentCourse(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="students_courses")

    id: int = ormar.Integer(primary_key=True)
    student = ormar.ForeignKey(Student) # default name
    course = ormar.ForeignKey(Course)  # default name

要自定义 Through 模型中的字段/关系名称,现在您可以使用 ManyToMany 的新参数:

  • through_relation_name - 通向声明 ManyToMany 的模型的字段名称
  • through_reverse_relation_name - 通向 ManyToMany 通向的模型的字段名称

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
... # course declaration omitted
base_ormar_config = ormar.OrmarConfig(
    database=databases.Database("sqlite:///db.sqlite"),
    metadata=sqlalchemy.MetaData(),
)


class Student(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100)
    courses = ormar.ManyToMany(Course,
                               through_relation_name="student_id",
                               through_reverse_relation_name="course_id")

# will produce Through model like follows (example simplified)
class StudentCourse(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="student_courses")

    id: int = ormar.Integer(primary_key=True)
    student_id = ormar.ForeignKey(Student) # set by through_relation_name
    course_id = ormar.ForeignKey(Course)  # set by through_reverse_relation_name

!!!注意 请注意,禁止在 Through 模型中显式声明关系,因此即使您提供自己的自定义 Through 模型,您也无法更改那里的名称,并且需要使用相同的 through_relation_name 和 through_reverse_relation_name 参数。

穿过田野

通过字段会自动添加到关系的反面。

暴露的字段通过类名命名为小写。

公开的字段显式没有加载任何关系,因为该关系已填充在 ManyToMany 字段中,因此仅当在 Through 模型上提供其他字段时它才有用。

在示例模型设置中,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import databases
import ormar
import sqlalchemy

DATABASE_URL = "sqlite:///test.db"

ormar_base_config = ormar.OrmarConfig(
    database=databases.Database(DATABASE_URL), metadata=sqlalchemy.MetaData()
)


class Category(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="categories")

    id = ormar.Integer(primary_key=True)
    name = ormar.String(max_length=40)


class PostCategory(ormar.Model):
    ormar_config = ormar_base_config.copy(tablename="posts_x_categories")

    id: int = ormar.Integer(primary_key=True)
    sort_order: int = ormar.Integer(nullable=True)
    param_name: str = ormar.String(default="Name", max_length=200)


class Post(ormar.Model):
    ormar_config = ormar_base_config.copy()

    id: int = ormar.Integer(primary_key=True)
    title: str = ormar.String(max_length=200)
    categories = ormar.ManyToMany(Category, through=PostCategory)

through 字段可以在大多数 QuerySet 操作中用作普通模型字段。

请注意,通过字段仅附加到查询的相关端,因此:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
post = await Post.objects.select_related("categories").get()
# source model has no through field
assert post.postcategory is None
# related models have through field
assert post.categories[0].postcategory is not None

# same is applicable for reversed query
category = await Category.objects.select_related("posts").get()
assert category.postcategory is None
assert category.posts[0].postcategory is not None

通过字段可用于过滤数据。

1
2
3
4
5
post = (
        await Post.objects.select_related("categories")
        .filter(postcategory__sort_order__gt=1)
        .get()
        )

!!!tip 请注意,尽管实际实例未填充在源模型中,但在查询、order by 语句等中,您可以从两侧通过模型进行访问。所以下面的查询具有完全相同的效果(注意通过类别访问)

1
2
3
4
5
6
7
```python
post = (
    await Post.objects.select_related("categories")
    .filter(categories__postcategory__sort_order__gt=1)
    .get()
    )
```

通过模型可以按顺序使用查询。

1
2
3
4
5
post = (
        await Post.objects.select_related("categories")
        .order_by("-postcategory__sort_order")
        .get()
    )

您还可以使用 fields 和 except_fields 以正常的 QuerySet 方式选择列的子集。

1
2
3
4
5
post2 = (
        await Post.objects.select_related("categories")
        .exclude_fields("postcategory__param_name")
        .get()
        )

!!!警告注意,由于 through fields 显式地使所有关系字段无效,因为关系填充在 ManyToMany 字段中,因此在从数据库重新加载字段之前,不应使用 save() 和 update() 等标准模型方法。

如果您想就地修改通过字段,请记住从数据库重新加载它。否则,您会将关系设置为“无”,从而有效地使该字段变得无用!

1
2
3
4
# always reload the field before modification
await post2.categories[0].postcategory.load()
# only then update the field
await post2.categories[0].postcategory.update(sort_order=3)

请注意,重新加载模型实际上会将关系重新加载为 pk_only 模型(仅设置主键),因此它们没有完全填充,但足以在更新时保留关系。

!!!警告如果您使用 ie fastapi,通过字段部分加载的相关模型可能会导致 pydantic 验证错误(这是默认情况下不填充它们的主要原因)。因此,您要么需要在响应中排除相关字段,要么完全加载相关模型。在上面的例子中,这意味着: python await post2.categories[0].postcategory.post.load() await post2.categories[0].postcategory.category.load() 或者,您可以使用 load_all(): python await post2.categories[0].postcategory.load_all()

首选更新方式是通过 queryset 代理 update() 方法

1
2
# filter the desired related model with through field and update only through field params
await post2.categories.filter(name='Test category').update(postcategory={"sort_order": 3})

关系法

添加

添加(项目:模型,**kwargs)

允许您将模型添加到多对多关系。

1
2
3
4
# Add a category to a post.
await post.categories.add(news)
# or from the other end:
await news.posts.add(post)

!!!警告 在所有非 None 情况下,相关模型的主键值必须存在于数据库中。

1
Otherwise an IntegrityError will be raised by your database driver library.

如果您使用带有附加字段的 Through 模型声明模型,则可以在将子模型添加到关系期间填充它们。

为此,请将带有字段名称和值的关键字参数传递给 add() 调用。

请注意,这仅适用于多对多关系。

1
2
3
4
post = await Post(title="Test post").save()
category = await Category(name="Test category").save()
# apart from model pass arguments referencing through model fields
await post.categories.add(category, sort_order=1, param_name='test')

消除

相关型号一一删除。

还删除数据库中的关系。

1
await news.posts.remove(post)

清除

一次调用删除所有相关模型。

还删除数据库中的关系。

等待 news.posts.clear()

查询集代理

反向关系公开了 QuerysetProxy API,它允许您像发出普通查询一样查询相关模型。

要了解 QuerySet 的哪些方法可用,请阅读下面的 querysetproxy