Django 过滤精确的 m2m 对象

Django filter exact m2m objects

假设我有一个团队模型,teammember 个。

所以

class Team(models.Model):
    team_member = models.ManyToManyField('Employee')

class Employee(models.Model):
    ....

假设我有一个员工 ID 列表,例如 team_members = [1001, 1003, 1004],我想找到 Team,它恰好由这三个成员组成。

我不想要拥有 [1001, 1003, 1004, 1005] 的团队或拥有 [1001, 1003] 的团队。

只有团队 [1001, 1003, 1004].

这就是我现在正在做的事情:

teams = Team.objects.all()
for t in teams:
    if set([x.id for x in t.team_member.all()]) == set(team_members):
        team = t
if not team:
    team = Team.objects.create()
    team.team_member = team_members

不过好像有点笨手笨脚的。有没有更简洁、嵌套循环更少的方法?

要让 Team 恰好包含这三名成员,您可以使用:

Team.objects.get(team_member__pk=team_members)  # This code was untested

您也可以尝试使用 Employee 个对象的列表:

# team_members = Employee.objects.filter(pk__in=tem_members)

team_members = [<Employee: Employee object>, <Employee: Employee object>, <Employee: Employee object>]

Team.objects.get(team_member=team_members)

也许你可以使用 annotate 计算 team_member 的计数。你能试试这个吗?

Team.objects.filter(team_member__pk__in=team_members).annotate(num_team=Count('team_member')).filter(num_team=len(team_members))

简答

不,我不知道在代码外观方面有更简单的方法。

但是, 您可以做一些事情来使您的代码更优雅,并可能更快。此外, 可以在数据库中完成工作,尽管对于大型团队来说效率很低。

下面列出的 DB 选项与您提供的 for 循环一样笨手笨脚,但根据您的数据集、DB 等可能会更有效。

更长的答案:减少的方法'ham-handed'

这里有几个地方我要清理样式。

此外,根据我使用 Django 的经验,像您构建的循环 do 在大型数据集上往往会变得相当昂贵。如果你最终将 10,000 个团队加载到内存中,让 ORM 将它们转换为 Team 对象,然后迭代它们,你可能会看到一些显着的减速。

速度和优雅的两件事:

  1. Team.values_list('team_members') 用于您的 in-python 过滤器循环,它会跳过 Django 将所有 SQL 数据组织到 Model 对象中的步骤。我发现这可以节省大量实例化对象的时间(有时大约一个数量级)。
  2. 整理您的 set() 来电。目前,您在每次迭代中都将 team_members 重新转换为 set(),此外,您正在将 t.team_member 隐式转换为 TeamMember 对象(因为它们是从数据库中获取的),然后进入 idlist,然后进入 set。对于第一项,只需在前面制作一个 team_members_set = set(team_members) 并重复使用它。对于第二项,您可以执行 set(t.team_member.values_list('id', flat=True)) ,这将跳过实例化 TeamMembers 的最重的 ORM 步骤(根据数据集和 Django 的,这可能与示例中的 O(n^2) 一样糟糕缓存)。
  3. 使用 Team.objects.all().iterator() 不将 Team 全部加载到内存中。如果您 运行 遇到内存问题,这将有所帮助。

但是对于任何性能优化,当然要用真实的或真实的数据测试你的性能,以确保你不会让事情变得更糟!

更长的答案:数据库选项

在尝试了所有 Q() 操作方式和此处答案中列出的其他方法后,我发现 .

基本上你需要重复filter()s,每个team_member一个。最重要的是,您使用 Count 过滤器来确保您最终不会选择具有所需成员超集的 Team

desired_members = [1001, 1003, 1004]
initial_queryset = Team.objects.annotate(cnt=models.Count('team_members')).filter(cnt=len(desired_members))
matching_teams = reduce( # Can of course use a for loop if you prefer that to reduce()
    lambda queryset, member: queryset.filter(team_members=member),
    desired_members,
    initial_queryset
)

请注意,生成的查询可能对大型团队有性能问题,因为它会为您的每个 desired_members 执行一个 JOIN。最好避免这种情况,但我不知道在不更改数据结构的情况下在数据库中执行所有操作的另一种方法。我很想学习更好的方法,如果你最终做了一些性能测试,我很想知道你学到了什么!