Django queryset attach or annotate related object field
You can use annotate
and F
>>> from django.db.models import F
>>> users = User.objects.all().annotate(member_from__status=F('member_from__status'))
>>> users[0].member_from__status
'accepted'
Tested with Django versions 1.11 and 2.2
In answer to comments
Here I guess we are rather interested by member_to__status
than member_from__status
(we want the user being requested to accept the friendship, not the other way around)
Here's the content of my membership
table:
status | from| to
---------+------+-----
accepted | 1| 2
accepted | 2| 3
refused | 3| 1
Since "it's not possible to have a symmetrical, recursive ManyToManyField
" (e.g. user 1 accepted user 2 so user 1 is now in user 2's friend list but user 2 is also in user 1's friends), this is how I would retrieve all actual friends:
>>> for user in User.objects.all():
... friends = user.friends.filter(member_to__status='accepted')
... print(str(user.id) + ': ' + ','.join([str(friend.id) for friend in friends]))
1: 2
2: 3
3:
If you want to have <User 1>
in <User 2>
's friends, I'd suggest to add another Membership
when the friendship from 1 to 2 has been accepted.
Here we assume that a Membership
can only be accepted once, so it could be a good idea to add the unique-together option. Nonetheless, if we have several Membership
with same from_user
, this is what happens:
>>> # <Insert several Membership from one to another User>
>>> Membership.objects.filter(from_user__id=1, to_user__id=2).count()
3
>>> users = User.objects.annotate(member_from__id=F('member_from__id'))
>>> print('User Membership')
... for user in users:
... print("%4d %10d" % (user.id, user.member_from__id))
User Membership
1 1
2 2
1 3 # User 1 again
1 4 # User 1 again
3 None
Currently i found a solution only using raw query.
Simplified query for user.friends.all()
is:
SELECT "users_user"."id", "users_user"."name", FROM "users_user" INNER JOIN "users_membership" ON ("users_user"."id" = "users_membership"."to_user_id") WHERE "users_membership"."from_user_id" = 10;
As we can see, users_membership
table is already joined. So, i copy this query and just add a "users_membership"."status"
field.
Then, i create a methon in my model friends_with_status
and insert new SQL query into raw
queryset method. My User models:
class User(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField('self', through='Membership',
blank=True, null=True, symmetrical=False)
def friends_with_status(self):
return User.objects.raw('SELECT "users_membership"."status", "users_user"."id", "users_user"."name", FROM "users_user" INNER JOIN "users_membership" ON ("users_user"."id" = "users_membership"."to_user_id") WHERE "users_membership"."from_user_id" = %s;', [self.pk])
Now, i use this:
>>> user = User.objects.get(name="John")
>>> friends = user.friends_with_status()
>>> friends[0].status
'accepted'
>>> friends[1].status
'thinking'
P.S.
Of course, this includes all disadvantages of raw query: it is not possible to apply any further queryset methods on it, i.e. this will not work:
>>> friends = user.friends_with_status().filter()
>>> friends = user.friends_with_status().exclude()
and so on. Also, if i modify model fields, i have to modify the raw query also.
But at least, such approach gives me what i need in one query.
I think, it will be useful to write some annotation method, like Count
or Avg
, that will allow to attach fields from joined table.
Something like this:
>>> from todo_my_annotations import JoinedField
>>> user = User.objects.get(name="John")
>>> friends = user.friends.annotate(status=JoinedField('member_from__status'))