Django queryset attach or annotate related object field

旧时模样 提交于 2020-07-04 10:51:14

问题


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?


回答1:


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



回答2:


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'))


来源:https://stackoverflow.com/questions/20139372/django-queryset-attach-or-annotate-related-object-field

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!