DRF多对多关系使用笔记

在开发博客过程中,遇到了多对多关系,记录下踩的坑。

Django关系:

一对一:

典型的例子有一个学生对应一张身份证, 一张身份证对应一个学生。 又或者Dj文档的例子, 一个地点(Place)对应一个餐厅(Restaurant)。在Dj中,一对一关系使用OneToOneField来关联, 关联后,地点表可以查询餐厅表的数据,餐厅表也可以查询地点表的数据。

一对多:

典型的例子有一个班级拥有多个学生, 一个学生只能属于某个班级。 又或者Dj文档的例子, 一个作者(Reporter)可以写多篇文章(Article),而一篇文章只属于一个作者。在Dj中,一对多关系使用ForeignKey来关联。

多对多:

典型的例子是一个老师可以教多个班级, 一个班级可以被多个老师教。又或者Dj文档的例子, 一个出版社(Publication)可以发布多篇文章(Article),而一篇文章可以被多个出版社发布。在Dj中,多对多关系使用ManyToManyField来关联。

遇到的问题

现在开发的这个博客,分类与文章之间是多对多的关系

一篇文章可以所属多个分类

一个分类下有多篇文章

先了解下多对多这种关系,在Django中实现多对多,实际上是为我们新建一个表(关系表),在这个关系表中至少要有两个外键,分别连接着两个表。

一上来,先不忙码代码。我实际遇到的问题就是:当新建一篇文章的时候,如何为这篇文章设置分类

上方就是我们新建文章的表单,当POST提交后,数据能够入库,但是,关系表中却没有这篇文章与分类表的关系

换个说法就是,我们需要将这篇文章的所属分类一同提交

好的,有了这个思路之后,我们就可以知道多对多字段是写在 文章模型类 还是 分类模型类,随着文章内容一起提交,那么我们就理所应当的写在 文章模型类

代码编写

文章模型类: content/models.py

class ContentsModel(models.Model):
    title = models.CharField(max_length=30, verbose_name="标题")
    text = models.TextField(null=True, blank=True, verbose_name="内容")
    # ...
    # 多对多
    metas = models.ManyToManyField(MetasModel, through='RelationshipsModel',
                                   through_fields=['content', 'meta'],
                                   related_name="meta_list")

分类模型类: metas/models.py

class MetasModel(models.Model):
    mid = models.AutoField(primary_key=True, unique=True, verbose_name="分类id")
    slug = models.SlugField(allow_unicode=True, verbose_name="缩写")
    name = models.CharField(max_length=10, verbose_name="分类名称")
    order = models.IntegerField(default=0, verbose_name="排序")
    count = models.IntegerField(default=0, verbose_name="文章数量")
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name="上级分类")

单独拎出来多对多字段,解释一下:

metas = models.ManyToManyField(MetasModel, through='RelationshipsModel',
                               through_fields=['content', 'meta'],
                               related_name="meta_list")

MetasModel :关系模型类

through='RelationshipsModel':多对多关系字段,默认可以帮我们自动建议一个关系表,但是如果你需要自己指定一个表,那么就可以通过这种方法,贴下RelationshipsModel的代码

关系类

class RelationshipsModel(models.Model):
    content = models.ForeignKey(ContentsModel, on_delete=models.CASCADE)
    meta = models.ForeignKey(MetasModel, on_delete=models.CASCADE)

很简单,就是两个外键。既然可以自定义关系表,所以你还可以在这个基础上新增其他字段。

详细见文档: https://docs.djangoproject.com/en/3.0/topics/db/models/#intermediary-manytomany

关系表自定义多个字段,所以在关系表中哪两个字段是需要的外键,Django区分不了

所以,这里就需要手动指定字段

through_fields=['content', 'meta'] 注意了!!! 这里填写是有讲究的,这两个字段的顺序不能错,否则会报类似下方的错误

contents.ContentsModel.metas: (fields.E339) 'RelationshipsModel.content' is not a foreign key to 'MetasModel'.

    HINT: Did you mean one of the following foreign keys to 'MetasModel': meta?

contents.ContentsModel.metas: (fields.E339) 'RelationshipsModel.meta' is not a foreign key to 'ContentsModel'.

    HINT: Did you mean one of the following foreign keys to 'ContentsModel': content?

前面我们填写了MetasModel,那么through_fields的第一个参数,应该是content,分类与文章形成对应的关系(我是这么记忆的,交叉填写),到底为什么,没找到原因。

简单点就是,如果出现上面的错误,就把这两个字段交换下位置

模型关系写好之后,就来写序列化类

serializer

class ContentSerializer(serializers.ModelSerializer):
    """内容"""

    metas = serializers.PrimaryKeyRelatedField(many=True, label="分类", queryset=MetasModel.objects.all())

    class Meta:
        model = ContentsModel
        fields = '__all__'

如果没有定义 metas ,POST请求新建文章就是普通的单表操作

queryset=MetasModel.objects.all() 列出所有的分类,POST提交的metas不在此查询集中,就会报错

{
    "metas": [
        "无效主键 “1111” - 对象不存在。"
    ]
}

DRF在接口调试界面,也帮我们生成了选择框,可以选择一个或多个

其他问题

设置allow_null=False, allow_empty=False, 使其默认不能为空(必填)

metas = serializers.PrimaryKeyRelatedField(many=True, allow_null=False, allow_empty=False, label="分类", queryset=MetasModel.objects.all())

删除分类:

对分类或文章进行删除操作,Django会自动将对应关系从关系表中删除

例如,文章A 属于分类1和分类2

现在将分类1和分类2删除,那么关系表中,分类1分类2与文章A对应的关系被删除了

但是,文章A依然存在(因为文章与分类之间不存在直接联系)

添加新评论