Search code examples
djangodjango-querysetlist-comprehensiondjango-q

Using Q object in list comprehension


I have the following code:

def get_elements(self, obj):
  book_elements = Element.objects.filter(book__pk=obj.id)
  elements = Element.objects.filter( (Q(book__pk=obj.id) | Q(theme__pk=obj.theme_id)), ~Q(pk__in = [o.element_id for o in book_elements if o.element_id]))
  serializer = GetElementSerializer(elements, context=self.context, many=True)

The elements variable is a query using the Q object implementation. However, Q(book__pk=obj.id) and book_elements reference the exact same set of values. How can I reference the Q(book__pk=obj.id) inside of the list comprehension to avoid having to run 2 queries. Something like the following:

def get_elements(self, obj):
  elements = Element.objects.filter( (Q(book__pk=obj.id) | Q(theme__pk=obj.theme_id)), ~Q(pk__in = [o.element_id for o in Q(book__pk=obj.id) if o.element_id]))
  serializer = GetElementSerializer(elements, context=self.context, many=True)

My Element Model as requested:

class Element(models.Model):
  name = models.CharField(max_length=100)
  pub_date = models.DateTimeField('date published', auto_now_add=True)
  mod_date = models.DateTimeField('modified date', auto_now=True)
  book = models.ForeignKey(Book, blank=True, null=True, related_name='elements')
  book_part = models.ForeignKey(BookPart)
  theme = models.ForeignKey(Theme, blank=True, null=True, related_name='elements')
  element = models.ForeignKey('self', blank=True, null=True, related_name='parent')
  def __str__(self):
    return self.name

Any help is appreciated!


Solution

  • Here is a one query with a sub-query solution:

    elements = Element.objects.filter( 
        (Q(book__pk=obj.id) | Q(theme__pk=obj.theme_id)),
        ~Q(pk__in = Element.objects.filter(
                book__pk=obj.id,
                element__isnull=False
            ).values_list('element', flat=True)
        )
    )
    

    this will hit the database only once.

    BTW, after reading what you actually are trying to achieve:

    get me all the elements, with a book_id or theme_id, that equals the book id & theme id for this book, excluding elements that also have a FK relationship to another element in this book

    In an ORM fashion I think this can also be achieved like this:

    elements = Element.objects \
        .filter(Q(book_id=book.id)|Q(theme_id=book.theme_id)) \
        .exclude(element__in=book.elements.all()) #not tested, if throws an error transform it into: book.elements.values_list('id', flat=True)
    

    Again this will hit the database only once, because querysets are lazy and Django is smart enough to transform them into a sub-query when they take apart into another queryset.