Reputation: 55
class Keyword(models.Model):
keyword = models.CharField(max_length=100, unique=True)
class Item(models.Model):
keywords = models.ManyToManyField(Keyword)
How can I create the following dynamically? i.e. so that I could add 'c'
items = Item.objects.filter(keywords__keyword__exact = 'a')
.filter(keywords__keyword__exact = 'b')
I've tried building Q objects:
tags = ['a', 'b']
queries = [Q(keywords__keyword__exact=tag) for tag in tags]
query = Q()
for q in queries:
query &= q
items = Item.objects.filter(query)
But this seems to use the query like keyword = a & keyword = b
which always returns empty.
Upvotes: 2
Views: 446
Reputation: 1242
Your solution is far over-engineered for this problem.
EDIT: I did not realize that this question was looking for a logical AND between keywords rather than a logical OR. I have updated this answer with options for both.
This is useful for when you want to retrieve items with any of the keywords listed
Django provides a very good API for Queryset filtering, which gives us an easier way to create the behavior you desire. There is no need to create a confusing Q object solution for this. I'll use your last code snippet to explain a more direct solution, making use of the Queryset lookup keyword in
(here's a link to the docs).
tags = ['a', 'b']
# Removed the complex Q object definition.
items = Item.objects.filter(keywords__keyword__in=tags)
This should give you a resultant Queryset which includes Item objects that are associated with Keyword objects containing a keyword matching any value within the list tags
. For example:
tags = ['a', 'b']
keyword_one = Keyword.objects.create(keyword='a')
keyword_two = Keyword.objects.create(keyword='b')
keyword_three = Keyword.objects.create(keyword='c')
item_one = Item.objects.create()
item_one.keywords.add(keyword_one)
item_one.save()
item_two = Item.objects.create()
item_two.keywords.add(keyword_two)
item_two.save()
item_three = Item.objects.create()
item_three.keywords.add(keyword_three)
item_three.save()
# items will only contain item_one and item_two, but not item_three.
tag_items = Item.objects.filter(keywords__keyword__in=tags)
This is useful for when you want to retrieve items with all of the keywords listed
Here's an approach which will serve our purposes.
Relevant code sample:
tags = ['a', 'b']
items_with_one_of_tags = Item.objects.filter(keywords__keyword__in=tags)
items_with_all_of_tags = items_with_one_of_tags.annotate(number_of_keywords=Count('keywords')).filter(number_of_keywords=len(tags))
The way that this method works is by creating an aggregate column in the Queryset which holds the number of Keyword objects associated with each Item object that has an association with least one of the Keyword objects. This aggregate column, named number_of_keywords
is then used to filter only the Item objects which have the exact number of keywords listed.
By this method, Item objects which have some of the keywords are eliminated by the Annotation method. Note that this does not return the set of Item objects that have the listed keywords and only the listed keywords. It is merely guaranteed that the Item objects returned will have each of the keywords. There is an example in that linked discussion thread using nested queries that would enable exclusion of Item objects which also have extra keywords aside from the listed keywords.
Upvotes: 4