DRF Pattern: Many-to-many Relationship with Order

 20th August 2020 at 2:19pm

多对多关系下,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 的数据是这样的:

idroleauthor_idbook_id_order
2illustrator292711
3author107732610

同时我给 BookAuthorRelation 中的 book 字段加上了 related_nameordered_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"
        }
    ]
}