Reputation: 1539
In my database I have user objects with two many to many fields (messages and following) on them that both contain a many to many field related to another object Topic.
class User():
messages = ManyToManyField('Message', related_name='users', blank=True, null=True)
following = ForeignKey('Following', related_name='users', blank=True, null=True)
class Message():
date = DateField(blank=True, null=True)
content = TextField(blank=True, null=True)
topics = ManyToManyField('Topic', related_name='messages', blank=True, null=True)
class Following():
name = CharField(max_length=255, blank=True, null=True)
description = CharField(max_length=255, blank=True, null=True)
topics = ManyToManyField('Topic', related_name='following', blank=True, null=True)
class Topic():
name = CharField(max_length=255, blank=True, null=True)
source = CharField(max_length=255, blank=True, null=True)
I want to filter for all "users" who have "messages" attached to them that do not contain all of the topics attached to the "following" objects on the user.
Right now I am using a loop to accomplish this:
users = set()
for user in User.objects.filter(messages__isnull=False, following__isnull=False).iterator():
if not set(user.following.values_list('topics', flat=True))
).issubset(set(user.messages.values_list('topics', flat=True)):
users.add(user.pk)
Is there a way to accomplish the same thing with a single query?
---- EDIT ----
What I have is this:
User.objects.filter(following__isnull=False
).annotate(following_count=Count('following__topics', distinct=True)
).filter(following__topics__exact=F('message__topics')
).annotate(missing_topics=ExpressionWrapper(
F('following_count') - Count('message__topics', distinct=True),
IntegerField())
).filter(missing_topics__gt=0)
If there is a better way to do this or there are reasons why I should most definitely not do it this way, what are they?
---- EDIT ----
This question helped me to understand and use Håken Lid's answer
This is my new model and my new query:
class User():
messages = ManyToManyField('Message', related_name='users', blank=True, null=True)
following = ManyToManyField('Topic', through='Following', related_name='users', blank=True, null=True)
class Message():
date = DateField(blank=True, null=True)
content = TextField(blank=True, null=True)
topics = ManyToManyField('Topic', related_name='messages', blank=True, null=True)
class Following():
name = CharField(max_length=255, blank=True, null=True)
description = CharField(max_length=255, blank=True, null=True)
user = ForeignKey('User', related_name='following', blank=True, null=True)
topic = ForeignKey('Topic', related_name='following', blank=True, null=True)
class Topic():
name = CharField(max_length=255, blank=True, null=True)
source = CharField(max_length=255, blank=True, null=True)
User.objects.filter(~Q(messages__topics__in=F('following'))
).values('id').annotate(missing_topics=Count('following__topics', distinct=True))
Upvotes: 0
Views: 844
Reputation: 23064
This should be possible using a subquery.
First, make sure Following.topics
uses a different related name than Messages.topics
.
class Following(models.Model):
topics = ManyToManyField('Topic', related_name='following')
Then it should be possible to create a subquery. Something like this:
from django.db.models import OuterRef, Subquery
user_following_topic = Topic.objects.filter(following__users=OuterRef('pk'))
User.objects.exclude(messages__topics__in=Subquery(user_following_topics.values('pk')))
This might not work and give you your expected output exactly as written, but I think the principle should work for your case as well.
On the other hand, I don't really understand your database structure. It seems you use m2m relations where foreign keys could be more appropriate and simpler. The more complicated your relations are, the harder it is to create this kind of advanced query. And queries with lots of database joins can be very slow, since they might have to process huge amounts of data, compared to simple queries.
For example, instead of using m2m realitions, Following
would make more sense to me like so:
class Following():
topic = ForeignKey('Topic', on_delete=models.CASCADE)
user = ForeignKey('User', on_delete=models.CASCADE)
client = models.CharField(max_length=255, blank=True, null=True)
duration = fields.DateRangeField(blank=False, null=False)
So basically a "through" model, as explained in the django docs on model relationships where there's a similar example.
Upvotes: 1