Django ORM 根据多对多字段获取对象

huangapple go评论59阅读模式
英文:

Django ORM get object based on many-to-many field

问题

I have model with m2m field users:

class SomeModel(models.Model):
    
    objects = SomeModelManager()

    users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)

My goal is to get instance of this model where set of instance users matches given queryset (it means that every user from queryset should be in m2m relation and no other users).

If I do

obj = SomeModel.objects.get(users=qs)

I get

ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing.

And I totally understand the reason for such error. So the next thing I did was creating a custom Queryset class for this model to override .get() behavior:

class SomeModelQueryset(QuerySet):

    def get(self, *args, **kwargs):
        qs = super()  # Prevent recursion
        if (users := kwargs.pop('users', None)) is not None:
            qs = qs.annotate(count=Count('users__id')).filter(users__in=users, count=users.count())
        return qs.get(*args, **kwargs)


class SomeModelManager(models.Manager.from_queryset(SomeModelQueryset)):
    ...

So what I try to do is to filter only objects with matching users and make sure that the number of users is the same as in the queryset.

But I don't like the current version of code. users__in adds an instance to the queryset each time it finds a match, so it results in n occurrences for each object (n - number of m2m users for a specific object). Count in .annotate() counts unique user IDs for each occurrence and then produces a single object with all counts combined. So for each object, there are n occurrences with a count of n, and the resulting object will have a count of n**2.

Is there a way to rewrite this annotate+filter to produce a count=n, not n^2?

英文:

I have model with m2m field users:

class SomeModel(models.Model):
    
    objects = SomeModelManager()

    users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)

My goal is to get instance of this model where set of instance users matches given queryset (it means that every user from queryset should be in m2m relation and no other users).

If I do

obj = SomeModel.objects.get(users=qs)

I get

ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing.

And I totaly understand the reason of such error, so the next thing I did was creating a custom Queryset class for this model to override .get() behavior:

class SomeModelQueryset(QuerySet):

    def get(self, *args, **kwargs):
        qs = super()  # Prevent recursion
        if (users := kwargs.pop('users', None)) is not None:
            qs = qs.annotate(count=Count('users__id')).filter(users__in=users, count=users.count()**2)
        return qs.get(*args, **kwargs)


class SomeModelManager(models.Manager.from_queryset(SomeModelQueryset)):
    ...

So what I try to do is to filter only objects with matching users and make sure that amount of users is the same as in queryset.

But I don't like current version of code. users__in adds instance to queryset each time it finds match, so it results in n occurrences for each object (n - number of m2m users for specific object). Count in .annotate() counts unique users ids for each occurrence and then produces single object with all counts combined. So for each object there are n occurrences with count n, and the resulting object will have count n**2.

Is there a way to rewrite this annotate+filter to produce count=n, not n^2 ?

答案1

得分: 1

你可以使用 __in 过滤器来检查,然后确定用户数量的计数:

from django.db.models import Q

my_users = User.objects.none()  # 一些用户的查询集
my_users = {user.pk for user in my_users}
obj = SomeModel.objects.alias(
    nusers=Count('users'), nmatch=Count('users', filter=Q(users__pk__in=my_users))
).filter(nusers=len(my_users), nmatch=len(my_users))

请注意,这部分代码中的注释和代码块并没有进行翻译。

英文:

You can check with an __in filter and then determine the count of the number of users:

<pre><code>from django.db.models import Q

my_users = User.objects.none() # some queryset of Users
my_users = {user.pk for user in my_users}
obj = SomeModel.objects.alias(
<b>nusers=Count('users'), nmatch=Count('users', filter=Q(users__pk__in=my_users))</b>
).filter(<b>nusers=len(my_users), nmatch=len(my_users)</b>)</code></pre>

huangapple
  • 本文由 发表于 2023年5月29日 17:59:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76356365.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定