It is needed to attach to queryset results related object field.
Models:
class User(models.Model):
name = models.CharField(max_length=50)
friends = models.ManyToManyField('self', through='Membership',
blank=True, null=True, symmetrical=False)
class Membership(models.Model):
status = models.CharField(choices=SOME_CHOICES, max_length=50)
from_user = models.ForeignKey(User, related_name="member_from")
to_user = models.ForeignKey(User, related_name="member_to")
I can do this:
>>> User.objects.all().values('name', 'member_from__status')
[{'member_from__status': u'accepted', 'name': 'Ann'}, {'member_from__status': u'thinking', 'name': 'John'}]
'member_from__status'
contains information, that i need. But together with it, i need a model instance also.
What i want is:
>>> users_with_status = User.objects.all().do_something('member_from__status')
>>> users_with_status
[<User 1>, <User 2>]
>>> users_with_status[0] # <-- this is an object, so i can access to all its methods
Every instance in queryset has a 'member_from__status' field with corresponding value:
>>> users_with_status[0].member_from__status
u'accepted'
How this could be achieved?
annotate()that has been computed over the objects that are related to the objects in the QuerySet . Each argument to annotate() is an annotation that will be added to each object in the QuerySet that is returned. The aggregation functions that are provided by Django are described in Aggregation Functions below.
A QuerySet is a collection of data from a database. A QuerySet is built up as a list of objects. QuerySets makes it easier to get the data you actually need, by allowing you to filter and order the data.
The Django ORM is a convenient way to extract data from the database, and the annotate() clause that you can use with QuerySets is also a useful way to dynamically generate additional data for each object when the data is being extracted.
In the Django framework, both annotate and aggregate are responsible for identifying a given value set summary. Among these, annotate identifies the summary from each of the items in the queryset. Whereas in the case of aggregate, the summary is calculated for the entire queryset.
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
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'
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'))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With