泽恩小站-教程区 Help

文章分类

博客文章通常需要分类,方便用户快速识别文章的类型,或者进行某种关联操作。

本章就来实现对文章的分类。

增加分类模型

首先在 article/models.py 里增加一个分类的模型,并且将其和博文成为一对多的外键:

# article/models.py ... class Category(models.Model): """文章分类""" title = models.CharField(max_length=100) created = models.DateTimeField(default=timezone.now) class Meta: ordering = ['-created'] def __str__(self): return self.title class Article(models.Model): # 分类 category = models.ForeignKey( Category, null=True, blank=True, on_delete=models.SET_NULL, related_name='articles' ) ...

字段很简单,大体上就 title 字段会用到。

别忘了数据迁移。

视图与路由

视图还是用视图集的形式:

# article/views.py ... from article.models import Category from article.serializers import CategorySerializer class CategoryViewSet(viewsets.ModelViewSet): """分类视图集""" queryset = Category.objects.all() serializer_class = CategorySerializer permission_classes = [IsAdminUserOrReadOnly]

与博文的视图集完全一样,没有新的知识。 CategorySerializer 还没写,不慌等一会来搞定它。

将路由也注册好:

# drf_vue_blog/urls.py ... from rest_framework.routers import DefaultRouter from article import views ... # 其他都不改,就增加这行 router.register(r'category', views.CategoryViewSet) urlpatterns = [ ... ]

是否感受到了自动注册的方便?

序列化器

接下来把 article/serializers.py 改成下面这样:

# article/serializers.py from rest_framework import serializers from article.models import Article from user_info.serializers import UserDescSerializer from article.models import Category class CategorySerializer(serializers.ModelSerializer): """分类的序列化器""" url = serializers.HyperlinkedIdentityField(view_name='category-detail') class Meta: model = Category fields = '__all__' read_only_fields = ['created'] 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) # category_id 字段的验证器 def validate_category_id(self, value): if not Category.objects.filter(id=value).exists() and value is not None: raise serializers.ValidationError("Category with id {} not exists.".format(value)) return value class Meta: model = Article fields = '__all__'

稍微开始有点复杂了,让我们来拆分解读一下代码。

先看 CategorySerializer

  • HyperlinkedIdentityField 前面章节有讲过,作用是将路由间的表示转换为超链接。 view_name 参数是路由名,你必须显示指定。 category-detail 是自动注册路由时, Router 默认帮你设置的详情页面的名称,类似的还有 category-list 等,更多规则参考文档

  • 创建日期不需要后期修改,所以设置为 read_only_fields

再来看 ArticleSerializer

  • 由于我们希望文章接口不仅仅只返回分类的 id 而已,所以需要显式指定 category ,将其变成一个嵌套数据,与之前的 author 类似。

  • DRF 框架原生没有实现可写的嵌套数据 (因为其操作逻辑没有统一的标准),那我想创建/更新文章和分类的外键关系怎么办?一种方法是自己去实现序列化器的 create()/update() 方法;另一种就是 DRF 框架提供的修改外键的快捷方式,即显式指定 category_id 字段,则此字段会自动链接到 category 外键,以便你更新外键关系。

  • 再看 category_id 内部。 write_only 表示此字段仅需要可写; allow_null 表示允许将其设置为空; required 表示在创建/更新时可以不设置此字段。

经过以上设置,实际上序列化器已经可以正常工作了。但有个小问题是如果用户提交了一个不存在的分类外键,后端会返回外键数据不存在的 500 错误,不太友好。解决方法就是对数据预先进行验证

验证方式又有如下几种:

  • 覆写序列化器的 .validate(...) 方法。这是个全局的验证器,其接收的唯一参数是所有字段值的字典。当你需要同时对多个字段进行验证时,这是个很好的选择。

  • 另一种就是教程用到的,即 .validate_{field_name}(...) 方法,它会只验证某个特定的字段,比如 category_id

validate_category_id 检查了两样东西:

  • 数据库中是否包含了对应 id 值的数据。

  • 传入值是否为 None。这是为了能够将已有的外键置空。

如果没通过上述检查,后端就抛出一个 400 错误(代替之前的 500 错误),并返回错误产生的提示,这就更友好一些了。

这就基本完成了对分类的开发。接下来就是实际的测试了。

测试

打开命令行,首先创建分类:

C:\...> http -a dusai:admin123456 POST http://127.0.0.1:8000/api/category/ title=Django ... { "created": "2020-08-23T08:01:20.113074Z", "id": 6, "title": "Django", "url": "http://127.0.0.1:8000/api/category/6/" }

更新已有的分类:

C:\...> http -a dusai:admin123456 PUT http://127.0.0.1:8000/api/category/6/ title=Flask ... { "created": "2020-08-23T08:01:20.113074Z", "id": 6, "title": "Flask", "url": "http://127.0.0.1:8000/api/category/6/" }

创建文章时指定分类:

C:\Users\Dusai>http -a dusai:admin123456 POST http://127.0.0.1:8000/api/article/ category_id=6 title=ILoveDRF body=WishYouToo! ... { "author": { ... }, "body": "WishYouToo!", "category": { "created": "2020-08-23T08:01:20.113074Z", "id": 6, "title": "Flask", "url": "http://127.0.0.1:8000/api/category/6/" }, "title": "ILoveDRF", ... }

把已有的分类置空:

C:\Users\Dusai>http -a dusai:admin123456 PATCH http://127.0.0.1:8000/api/article/20/ category_id:=null ... { "category": null, ... }

这里细心一点的就会发现,在更新资源时用到了 POSTPUTPATCH 三种请求方法,它们的区别是啥?

  • POST :创建新的资源。

  • PUT: 整体更新特定资源,默认情况下你需要完整给出所有必须的字段。

  • PATCH: 部分更新特定资源,仅需要给出需要更新的字段,未给出的字段默认不更改。

删除以及权限等功能就不试了,读者朋友自行尝试吧。

完善分类详情

上面写的分类接口中,我希望分类的列表页面不显示其链接的文章以保持数据清爽,但是详情页面则展示出链接的所有文章,方便接口的使用。因此就需要同一个视图集用到两个不同的序列化器了,即前面章节讲的覆写 get_serializer_class()

修改序列化器:

# article/serializers.py ... class ArticleCategoryDetailSerializer(serializers.ModelSerializer): """给分类详情的嵌套序列化器""" url = serializers.HyperlinkedIdentityField(view_name='article-detail') class Meta: model = Article fields = [ 'url', 'title', ] class CategoryDetailSerializer(serializers.ModelSerializer): """分类详情""" articles = ArticleCategoryDetailSerializer(many=True, read_only=True) class Meta: model = Category fields = [ 'id', 'title', 'created', 'articles', ]

然后修改视图:

# article.views.py ... from article.serializers import CategorySerializer, CategoryDetailSerializer class CategoryViewSet(viewsets.ModelViewSet): """分类视图集""" ... def get_serializer_class(self): if self.action == 'list': return CategorySerializer else: return CategoryDetailSerializer

除此之外没有新的魔法。

Last modified: 06 January 2025