【问题标题】:Django - Add object in front of existing QuerySetDjango - 在现有 QuerySet 前面添加对象
【发布时间】:2021-01-18 07:46:45
【问题描述】:

我正在构建一个library Django 应用程序,其中我有一个BooksView 来显示某个Author 编写的所有Books。在某些情况下,我希望在查询集中的所有其他书籍之前显示一个 Book。这应该在图书的 id 出现在 URL 中时发生,否则,所有图书都应按时间顺序列出。

books/urls.py

urlpatterns = [

    # Books displayed in chronological order
    path("<slug:author>/books", views.BooksView.as_view()),

    # Books displayed in chronological order
    # but first book is the one with id `book`
    path("<slug:author>/books/<int:book>", views.BooksView.as_view()),
]

books/views.py

class BooksView(APIView):

    def get(self, request, author, book=None):

        author = get_object_or_404(Author, slug=author)
        books = author.author_books
        books = books.order_by("-pub_date")

        if book:
            book = get_object_or_404(Book, id=book)
            # TODO: add `book` in front of books QuerySet (and remove duplicated if any)

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

我怎样才能完成我的TODO?如何将对象推送到现有查询集的前面?

【问题讨论】:

  • 您能检查一下在数据库中创建的模式吗?它在您的列上有唯一索引吗?类似于 table(lower(col_name));

标签: python django django-models django-views django-queryset


【解决方案1】:

您可以将按位| 运算符用作

from django.http.response import Http404


class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        book_qs = author.author_books
        if book:
            single_book_qs = Book.objects.filter(id=book)
            if not single_book_qs.exists():
                raise Http404
            book_qs = single_book_qs | book_qs

        serializer = BooksSerializer(book_qs, many=True)
        return Response(serializer.data)

请注意,此解决方案的一个警告是,如果您使用 order_by(...) 方法,位置将根据 ORDER By 表达式进行更改。

更新 1

由于您使用的是order_by(...) 表达式,因此您必须这样做,

class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        book_qs = author.author_books.order_by("-pub_date")
        serialized_single_book = []
        if book:
            single_book = get_object_or_404(Book, id=book)
            book_qs.exclude(id=book) # to remove dups
            serialized_single_book = [BooksSerializer(single_book).data]

        serializer = BooksSerializer(book_qs, many=True)
        serialized_book_qs = serializer.data
        return Response([*serialized_single_book, *serialized_book_qs])

【讨论】:

  • 对此我深表歉意,但我在问题的代码中省略了一个非常重要的细节。我添加了一行,按书籍的出版日期对您所说的book_qs 进行排序。使用该行,您的代码似乎不再工作了。
  • @fredperk 检查Update-1
【解决方案2】:

这并不能准确回答您的问题,但我认为它可以让您获得所需的结果:不要担心保留查询集,而是将其转换为列表。

这不是您想要为整个代码库中的每个查询集做的事情,特别是如果它是一个您以后可能想要构建并与其他过滤器、排除等组合的查询集。但是当您即将渲染一个页面(就像你在这里一样)没关系,因为无论如何你都将导致查询集被评估。你导致它发生的时间可能早了几微秒。

class BooksView(APIView):

    def get(self, request, author, book=None):

        author = get_object_or_404(Author, slug=author)
        books = list(author.author_books)

        if book:
            book = Book.objects.get(id=book)
            # TODO: add `book` in front of books QuerySet (and remove duplicated if any)
            books = [x for x in books if not x == book] # first remove if dup
            books.insert(0, book) # now insert at front

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

编辑 1

BooksSerializer(我怀疑它是 BaseSerializer 的子类)无论如何都会在您调用它时生成一个列表:

    def to_representation(self, data):
        """
        List of object instances -> List of dicts of primitive datatypes.
        """
        # Dealing with nested relationships, data can be a Manager,
        # so, first get a queryset from the Manager if needed
        iterable = data.all() if isinstance(data, models.Manager) else data

        return [
            self.child.to_representation(item) for item in iterable
        ]

https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L663

编辑 2

试试这个怎么样?通过在查询集被评估为列表之前添加exclude,您可以防止 O(n) 扫描列表以查找并删除应该位于顶部的“主”书。

class BooksView(APIView):

    def get(self, request, author, book=None):

        author = get_object_or_404(Author, slug=author)
        books = author.author_books

        if book:
            book = Book.objects.get(id=book)
            # TODO: add `book` in front of books QuerySet (and remove duplicated if any)
            # ensure the queryset doesn't include the "main" book
            books = books.exclude(book_id=book.id)
            # evaluate it into a list just like the BookSerializer will anyhow
            books = list(books)
            # now insert the "main" book at the front of the list 
            books.insert(0, book)

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

【讨论】:

  • 评估 QuerySet 只是为了插入不是一个好主意
  • 查询集的评估在序列化程序被调用后立即发生。我不明白为什么早一点执行该操作会导致问题。
  • 感谢您的解决方案。但是,在我看来,它非常昂贵。我希望有更接近 O(1) 算法的东西(不过我可能错了)
  • 将查询集渲染为 json 将是 O(n) 并且这也会执行 O(n) 操作,因此它将它变成 O(2*n) 对我来说似乎不是成为一件大事。我认为有一种方法可以降低成本。
  • 到目前为止,您的第二个解决方案对我来说是赢家。我正在等待是否有更好的解决方案出现,否则我会接受您的回答。
【解决方案3】:

这个应该可以,只要在QuerySet对象中使用union方法,并排除 我们试图从 books 查询集访问的书。

我对代码做了一些小改动,但您不需要做更多的事情来完成您需要的工作。

class BooksView(APIView):

    def get(self, request, author, book=None):
        author = get_object_or_404(Author, slug=author)
        books = author.author_books.all().order_by('-pub_date')

        if book:
            # I am assuming this line if for validating that the request is valid.
            # I've changed the query to also include author's slug,
            # so the request doesn't get books not related to the author.
            book_obj = get_object_or_404(Book, id=book, author__slug=author)

            # Just Query the same book, and union it with the books queryset,
            # excluding the current book in the book_obj
            books = Book.objects.filter(id=book_obj.id).union(books.exclude(id=book_obj.id))

        serializer = BooksSerializer(books, many=True)
        return Response(serializer.data)

【讨论】:

  • 这是要走的路,应该是国际海事组织接受的答案。您可以进行的一项优化是将.union(books.exclude(id=book_obj.id)) 替换为.union(books),因为SQL UNION 默认排除重复项docs.djangoproject.com/en/3.1/ref/models/querysets/#union
  • 我也试过这个,但至少在 Postgres 上联合排序是不可预测的。是的,我翻转了工会的第一个/第二个,排除,不排除,工会内部的顺序 - 全部。
【解决方案4】:

可能有点开箱即用,但是如果您想将其保留为查询集,那么这就是解决方案。请注意,这解决了X problem, not the Y problem。目标是将一本书放在列表的第一位,这可以通过插入来实现,也可以通过基于注释的重新排序来实现。

from django.db.models import Case, When, Value, IntegerField
from rest_framework import generics

from . import models, serializers


class BooksPerAuthor(generics.ListAPIView):
    serializer_class = serializers.BookWithoutAuthor

    def get_queryset(self):
        book_pk = self.kwargs.get("book", 0)
        queryset = (
            models.Book.objects.filter(author__slug=self.kwargs["author"])
            .annotate(
                promoted=Case(
                    When(pk=book_pk, then=Value(1)),
                    default=Value(0),
                    output_field=IntegerField(),
                )
            )
            .order_by("-promoted", "-pub_date")
        )
        return queryset

这不会捕获不存在的引用 - 不喜欢返回 404 以获取列表视图,并且如果推广的书不在集合中,那么不会造成真正的伤害。上面有很多例子可以用 404 做同样的事情,但需要额外的查询(少量)成本。

【讨论】:

    猜你喜欢
    • 2020-10-31
    • 2011-03-24
    • 1970-01-01
    • 1970-01-01
    • 2019-06-22
    • 2021-08-05
    • 2017-06-22
    • 2013-05-10
    • 1970-01-01
    相关资源
    最近更新 更多