patternpythondjangoMinor
Django query ManyToMany for tagged items
Viewed 0 times
manytomanydjangotaggedqueryitemsfor
Problem
I've got a item inventory that each inventory item is tagged with multiple tags. What I've got is an API that takes a query parameter with multiple comma separated query items and returns a list of all items that match all of the tags.
The following code is working, however this isn't really using the ManyToMany attributes in the Django models and I'm sure there is a better way to do this.
Schema ---
Get Function --
```
def get(self, request, *args, **kwargs):
query = request.GET.get('q', '').strip()
start = int(request.GET.get('start', 0))
count = int(request.GET.get('count', 50))
query_items = [q.strip() for q in query.split(',') if q.strip()]
items = InventoryItem.objects
if query_items:
for q in query_items:
tags = InventoryTag.objects
q = q.lstrip(':')
if ':' in q:
(k, v) = q.split(':', 1)
tags = tags.filter(Q(key = k) & Q(value__startswith = v))
else:
tags = tags.filter(value__startswith = q)
tags_pks
The following code is working, however this isn't really using the ManyToMany attributes in the Django models and I'm sure there is a better way to do this.
Schema ---
class InventoryTag(models.Model):
class Meta:
unique_together = ('key', 'value')
key = models.CharField(max_length=240, db_index=True)
value = models.CharField(max_length=240, db_index=True)
class InventoryItem(models.Model):
instance_id = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
tags = models.ManyToManyField(InventoryTag, through='InventoryItemTag', through_fields=('item', 'tag'))
def as_json(self):
return serializers.serialize('json', [self])[0]
class InventoryItemTag(models.Model):
class Meta:
unique_together = ('item', 'tag')
item = models.ForeignKey(InventoryItem, db_index=True, related_name='invitem')
tag = models.ForeignKey(InventoryTag, db_index=True, related_name='invtag')
created_at = models.DateTimeField(auto_now_add=True)Get Function --
```
def get(self, request, *args, **kwargs):
query = request.GET.get('q', '').strip()
start = int(request.GET.get('start', 0))
count = int(request.GET.get('count', 50))
query_items = [q.strip() for q in query.split(',') if q.strip()]
items = InventoryItem.objects
if query_items:
for q in query_items:
tags = InventoryTag.objects
q = q.lstrip(':')
if ':' in q:
(k, v) = q.split(':', 1)
tags = tags.filter(Q(key = k) & Q(value__startswith = v))
else:
tags = tags.filter(value__startswith = q)
tags_pks
Solution
You are, more or less, manually performing in Python what can be achieved in SQL using Q objects. Basically, you can chain Q objects to create an SQL query on the
Also note that, on queryset operations that will be converted to a
Building the tags research query should already include the lookup that span relationship and can be something like:
And from there, we can retrieve objects we want in a single query:
A few things to note:
-
you can clean your query better in the list-comprehension to avoid an extra
-
you never use
InventoryItems with one WHERE clause for each part of your HTTP query.Also note that, on queryset operations that will be converted to a
WHERE clause (like get, filter...) as well as Q objects, the keywords arguments are ANDed together. So there is no need in doing tags.filter(Q(key = k) & Q(value__startswith = v)) since tags.filter(key=k, value__startswith=v) performs the same query and is more readable.Building the tags research query should already include the lookup that span relationship and can be something like:
relevant_tags = Q() # Fallback, if the query is empty will select everything
for tag_searched in query_items:
tag_searched = tag_searched.lstrip(':')
try:
tag_name, tag_value = tag_searched.split(':', 1)
except ValueError:
# tag_searched did not contain any :
query = Q(tags__value__startswith=tag_searched)
else:
query = Q(tags__key=tag_name, tags__value__startswith=tag_value)
relevant_tags = relevant_tags & queryAnd from there, we can retrieve objects we want in a single query:
items = InventoryItems.objects.filter(relevant_tags)A few things to note:
if iterablejust beforefor element in iterableis pointless as the for loop will be a no-op anyway if the iterable is empty;
- you can use EAFP rather than LBYL, especially if you expect one path to happen more frequently than the other. I made a wild guess that the user will use the
name:valuesyntax of the query more often than the simplevalueone. It is also to show an alternate syntax.
-
you can clean your query better in the list-comprehension to avoid an extra
lstrip in the computation. Or you can avoid the list-comp and perform the whole cleanup in the for loop:for tag_searched in query.split(','):
tag_searched = tag_searched.strip().lstrip(':')
if not tag_searched:
continue
...-
you never use
start or count, you should remove them.Code Snippets
relevant_tags = Q() # Fallback, if the query is empty will select everything
for tag_searched in query_items:
tag_searched = tag_searched.lstrip(':')
try:
tag_name, tag_value = tag_searched.split(':', 1)
except ValueError:
# tag_searched did not contain any :
query = Q(tags__value__startswith=tag_searched)
else:
query = Q(tags__key=tag_name, tags__value__startswith=tag_value)
relevant_tags = relevant_tags & queryitems = InventoryItems.objects.filter(relevant_tags)for tag_searched in query.split(','):
tag_searched = tag_searched.strip().lstrip(':')
if not tag_searched:
continue
...Context
StackExchange Code Review Q#149015, answer score: 2
Revisions (0)
No revisions yet.