泽恩小站-教程区 Help

文章标签

一篇文章通常还有标签功能,作为分类的补充。

模型和视图

老规矩,首先把标签的 model 建立好:

# article/models.py ... class Tag(models.Model): """文章标签""" text = models.CharField(max_length=30) class Meta: ordering = ['-id'] def __str__(self): return self.text ... class Article(models.Model): ... # 标签 tags = models.ManyToManyField( Tag, blank=True, related_name='articles' )

一篇文章可以有多个标签,一个标签可以对应多个文章,因此是多对多关系。

接着把视图集也写好:

# article/views.py ... from article.models import Tag from article.serializers import TagSerializer class TagViewSet(viewsets.ModelViewSet): queryset = Tag.objects.all() serializer_class = TagSerializer permission_classes = [IsAdminUserOrReadOnly]

还是那三板斧,没有新内容。

最后的外围工作,就是注册路由:

# drf_vue_blog/urls.py ... router.register(r'tag', views.TagViewSet) ...

序列化器

接下来就是最重要的 TagSerializer

# article/serializers.py ... from article.models import Tag # 新增的序列化器 class TagSerializer(serializers.HyperlinkedModelSerializer): """标签序列化器""" class Meta: model = Tag fields = '__all__' # 修改已有的文章序列化器 class ArticleSerializer(serializers.HyperlinkedModelSerializer): ... # tag 字段 tags = serializers.SlugRelatedField( queryset=Tag.objects.all(), many=True, required=False, slug_field='text' ) ...

通过前面章节已经知道,默认的嵌套序列化器只显示外链的 id,需要改得更友好一些。但似乎又没必要改为超链接或者字段嵌套,因为标签就 text 字段有用。因此就用 SlugRelatedField 直接显示其 text 字段的内容就足够了。

让我们给已有的文章新增一个叫 java 的标签试试:

PS C:\...> http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/26/ tags:='[\"java\"]' ... { "tags": [ "Object with text=java does not exist." ] }

修改失败了,原因是 java 标签不存在。多对多关系,DRF 默认你必须先得有这个外键对象,才能指定其关系。虽然也合情合理,但我们更希望在创建、更新文章时,程序会自动检查数据库里是否存在当前标签。如果存在则指向它,如果不存在则创建一个并指向它。

要实现这个效果,你可能想到覆写 .validate_{field_name}() 或者 .validate() 还或者 .create()/.update() 方法。但是很遗憾,它们都是不行的。

原因是 DRF 执行默认的字段有效性检查比上述的方法都早,程序还执行不到上述的方法,框架就已经抛出错误了。

正确的解法是覆写 to_internal_value() 方法:

# article/serializers.py ... class ArticleSerializer(serializers.HyperlinkedModelSerializer): ... # 覆写方法,如果输入的标签不存在则创建它 def to_internal_value(self, data): tags_data = data.get('tags') if isinstance(tags_data, list): for text in tags_data: if not Tag.objects.filter(text=text).exists(): Tag.objects.create(text=text) return super().to_internal_value(data)

to_internal_value() 方法原本作用是将请求中的原始 Json 数据转化为 Python 表示形式(期间还会对字段有效性做初步检查)。它的执行时间比默认验证器的字段检查更早,因此有机会在此方法中将需要的数据创建好,然后等待检查的降临。 isinstance() 确定标签数据是列表,才会循环并创建新数据。

再重新请求试试:

PS C:\...> http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/26/ tags:='[\"java\", \"python\"]' ... { "tags": [ "python", "java" ], ... }

这次成功了。可以看到同时赋值多个标签也是可以的,置空也是可以的(给个空列表)。

除此之外,因为标签仅有 text 字段是有用的,两个 id 不同但是 text 相同的标签没有任何意义。更重要的是, SlugRelatedField 是不允许有重复的 slug_field 。因此还需要覆写 TagSerializercreate()/update() 方法:

# article/serializers.py ... class TagSerializer(serializers.HyperlinkedModelSerializer): """标签序列化器""" def check_tag_obj_exists(self, validated_data): text = validated_data.get('text') if Tag.objects.filter(text=text).exists(): raise serializers.ValidationError('Tag with text {} exists.'.format(text)) def create(self, validated_data): self.check_tag_obj_exists(validated_data) return super().create(validated_data) def update(self, instance, validated_data): self.check_tag_obj_exists(validated_data) return super().update(instance, validated_data) ...

这样就防止了重复 text 的标签对象出现。

这两个序列化器的完整形态是下面这样子的:

# article/serializers.py class TagSerializer(serializers.HyperlinkedModelSerializer): """标签序列化器""" def check_tag_obj_exists(self, validated_data): text = validated_data.get('text') if Tag.objects.filter(text=text).exists(): raise serializers.ValidationError('Tag with text {} exists.'.format(text)) def create(self, validated_data): self.check_tag_obj_exists(validated_data) return super().create(validated_data) def update(self, instance, validated_data): self.check_tag_obj_exists(validated_data) return super().update(instance, validated_data) class Meta: model = Tag fields = '__all__' class ArticleSerializer(serializers.HyperlinkedModelSerializer): """博文序列化器""" author = UserDescSerializer(read_only=True) # category 的嵌套序列化字段 category = CategorySerializer(read_only=True) # category 的 id 字段,用于创建/更新 category 外键 category_id = serializers.IntegerField(write_only=True, allow_null=True, required=False) # tag 字段 tags = serializers.SlugRelatedField( queryset=Tag.objects.all(), many=True, required=False, slug_field='text' ) # 覆写方法,如果输入的标签不存在则创建它 def to_internal_value(self, data): tags_data = data.get('tags') if isinstance(tags_data, list): for text in tags_data: if not Tag.objects.filter(text=text).exists(): Tag.objects.create(text=text) return super().to_internal_value(data) # category_id 字段的验证器 def validate_category_id(self, value): # 数据存在且传入值不等于None if not Category.objects.filter(id=value).exists() and value != None: raise serializers.ValidationError("Category with id {} not exists.".format(value)) return value class Meta: model = Article fields = '__all__'

标签的增删改查,就请读者自行测试吧。

Last modified: 06 January 2025