How to combine two or more querysets in a Django view?

后端 未结 13 2351
猫巷女王i
猫巷女王i 2020-11-21 22:40

I am trying to build the search for a Django site I am building, and in that search, I am searching in 3 different models. And to get pagination on the search result list, I

相关标签:
13条回答
  • 2020-11-21 23:24

    Requirements: Django==2.0.2, django-querysetsequence==0.8

    In case you want to combine querysets and still come out with a QuerySet, you might want to check out django-queryset-sequence.

    But one note about it. It only takes two querysets as it's argument. But with python reduce you can always apply it to multiple querysets.

    from functools import reduce
    from queryset_sequence import QuerySetSequence
    
    combined_queryset = reduce(QuerySetSequence, list_of_queryset)
    

    And that's it. Below is a situation I ran into and how I employed list comprehension, reduce and django-queryset-sequence

    from functools import reduce
    from django.shortcuts import render    
    from queryset_sequence import QuerySetSequence
    
    class People(models.Model):
        user = models.OneToOneField(User, on_delete=models.CASCADE)
        mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')
    
    class Book(models.Model):
        name = models.CharField(max_length=20)
        owner = models.ForeignKey(Student, on_delete=models.CASCADE)
    
    # as a mentor, I want to see all the books owned by all my mentees in one view.
    def mentee_books(request):
        template = "my_mentee_books.html"
        mentor = People.objects.get(user=request.user)
        my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
        mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])
    
        return render(request, template, {'mentee_books' : mentee_books})
    
    0 讨论(0)
  • 2020-11-21 23:27

    Try this:

    matches = pages | articles | posts
    

    It retains all the functions of the querysets which is nice if you want to order_by or similar.

    Please note: this doesn't work on querysets from two different models.

    0 讨论(0)
  • 2020-11-21 23:31

    The big downside of your current approach is its inefficiency with large search result sets, as you have to pull down the entire result set from the database each time, even though you only intend to display one page of results.

    In order to only pull down the objects you actually need from the database, you have to use pagination on a QuerySet, not a list. If you do this, Django actually slices the QuerySet before the query is executed, so the SQL query will use OFFSET and LIMIT to only get the records you will actually display. But you can't do this unless you can cram your search into a single query somehow.

    Given that all three of your models have title and body fields, why not use model inheritance? Just have all three models inherit from a common ancestor that has title and body, and perform the search as a single query on the ancestor model.

    0 讨论(0)
  • 2020-11-21 23:32

    You can use the QuerySetChain class below. When using it with Django's paginator, it should only hit the database with COUNT(*) queries for all querysets and SELECT() queries only for those querysets whose records are displayed on the current page.

    Note that you need to specify template_name= if using a QuerySetChain with generic views, even if the chained querysets all use the same model.

    from itertools import islice, chain
    
    class QuerySetChain(object):
        """
        Chains multiple subquerysets (possibly of different models) and behaves as
        one queryset.  Supports minimal methods needed for use with
        django.core.paginator.
        """
    
        def __init__(self, *subquerysets):
            self.querysets = subquerysets
    
        def count(self):
            """
            Performs a .count() for all subquerysets and returns the number of
            records as an integer.
            """
            return sum(qs.count() for qs in self.querysets)
    
        def _clone(self):
            "Returns a clone of this queryset chain"
            return self.__class__(*self.querysets)
    
        def _all(self):
            "Iterates records in all subquerysets"
            return chain(*self.querysets)
    
        def __getitem__(self, ndx):
            """
            Retrieves an item or slice from the chained set of results from all
            subquerysets.
            """
            if type(ndx) is slice:
                return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
            else:
                return islice(self._all(), ndx, ndx+1).next()
    

    In your example, the usage would be:

    pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
                                Q(body__icontains=cleaned_search_term))
    articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
                                      Q(body__icontains=cleaned_search_term) |
                                      Q(tags__icontains=cleaned_search_term))
    posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
                                Q(body__icontains=cleaned_search_term) | 
                                Q(tags__icontains=cleaned_search_term))
    matches = QuerySetChain(pages, articles, posts)
    

    Then use matches with the paginator like you used result_list in your example.

    The itertools module was introduced in Python 2.3, so it should be available in all Python versions Django runs on.

    0 讨论(0)
  • 2020-11-21 23:33
    DATE_FIELD_MAPPING = {
        Model1: 'date',
        Model2: 'pubdate',
    }
    
    def my_key_func(obj):
        return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
    
    And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)
    

    Quoted from https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. See Alex Gaynor

    0 讨论(0)
  • 2020-11-21 23:34

    In case you want to chain a lot of querysets, try this:

    from itertools import chain
    result = list(chain(*docs))
    

    where: docs is a list of querysets

    0 讨论(0)
提交回复
热议问题