多对多关系下,API 输出的 ManyToManyFields
字段的值需要是排好序的,这应该怎么做?
具体场景是:一个图书系统中,一本图书可能有多个作者,但是作者间是有先后顺序的,比如 Harry Potter 中 J.K. Rowling 应该排在前面,而封面插画作者 Mary GrandPré 应该排在后面。
如何在 Django Models 中实现 author 带顺序
models.py
做了简化后是这样:
class Book(models.Model):
title = models.CharField(max_length=200)
authors = models.ManyToManyField('BookAuthor', through='BookAuthorRelation')
class BookAuthor(models.Model):
name = models.CharField(max_length=120)
class BookAuthorRelation(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='ordered_authors')
author = models.ForeignKey(BookAuthor, on_delete=models.CASCADE, related_name='ordered_books')
role = models.CharField(max_length=50)
class Meta:
order_with_respect_to = 'book'
上面用了一个关联表 BookAuthorRelation
来存储一个作者在某本书中的角色,以及它在这本书的作者列表中排第几位。排序的字段是通过 Meta
中的 order_with_respect_to
加上的,这是 Django 提供的一个 特性,会自动给数据库表加上一个 _order
的字段。比如这个例子中,DB 的数据是这样的:
id | role | author_id | book_id | _order |
---|---|---|---|---|
2 | illustrator | 2927 | 1 | 1 |
3 | author | 1077326 | 1 | 0 |
同时我给 BookAuthorRelation
中的 book
字段加上了 related_name
为 ordered_authors
,这会有这样的效果:
>>> from main.models import Book
>>> b = Book.objects.get(pk=1)
b
<Book: Harry Potter>
# 这个顺序是错的。直接访问 `authors` 字段时,Django 并不会帮你按 `_order` 排序,而是隐式地按 DB 中的 `id` 排了序
>>> list(b.authors.all())
[<BookAuthor: Mary GrandPré>, <BookAuthor: J.K. Rowling>]
# 这个顺序是对的,会启用 Django 的 `order_with_respect_to` 功能;同时会有 role 信息
list(b.ordered_authors.all())
[<BookAuthorRelation: Harry Potter => J.K. Rowling>, <BookAuthorRelation: => Mary GrandPré>]
如何实现 Book 的 API 输出时带的 authors 信息是正确排序的?
我们希望 Book 的 API 接口中,也把相应的作者信息,作为 嵌套的数据 带下去。一般的做法是:
class BookAuthorSerializer(serializers.ModelSerializer):
class Meta:
model = BookAuthor
fields = ['id', 'name']
class BookSerializer(serializers.ModelSerializer):
authors = BookAuthorSerializer(many=True, read_only=True)
class Meta:
model = Book
fields = ['id', 'title', 'authors']
这样输出的数据中,authors
的顺序是错的,Mary GrandPré 会排在 J.K. Rowling 前面:
{
"id": 1,
"title": "Harry Potter",
"authors": [
{
"id": 2927,
"name": "Mary GrandPré"
},
{
"id": 1077326,
"name": "J.K. Rowling"
}
]
}
并且所需要的 role
信息也没带上。
我们可以通过 ordered_authors
提供的正确的顺序及 role 信息。这是一个利用了 SerializerMethodField 来提供自定义格式的字段值的能力:
class BookSerializer(serializers.ModelSerializer):
authors = fields.SerializerMethodField(read_only=True)
def get_authors(self, obj: Book):
"""Authors should be in ordered"""
return [
{'id': o.author_id, 'name': o.author.name, 'role': o.role}
for o in obj.ordered_authors.all()
]
class Meta:
model = Book
fields = ['id', 'title', 'authors']
此时输出的数据是正确顺序并带有 role 信息的:
{
"id": 1,
"title": "Harry Potter",
"authors": [
{
"id": 1077326,
"name": "J.K. Rowling",
"role": "author"
},
{
"id": 2927,
"name": "Mary GrandPré",
"role": "illustrator"
}
]
}