在Django中针对另一个相关模型的M2M关系过滤相关字段

时间:2015-09-25 13:42:02

标签: django django-models django-queryset

所以我有一个预订系统。代理商(提交预订的人员和组织)仅允许在我们分配的类别中进行预订。许多代理可以分配到相同的类别。这是一个简单的多对多。这里是模型的概念:

class Category(models.Model):
    pass

class Agent(models.Model):
    categories = models.ManyToManyField('Category')

class Booking(models.Model):
    agent = models.ForeignKey('Agent')
    category = models.ForeignKey('Category')

因此,当预订进入时,我们会根据代理商可用的类别动态分配类别。代理人通常没有指定。

我可以在Booking.agent.categories中选择Booking.category不在的预订吗?

我们刚刚注意到 - 由于一个愚蠢的管理员错误的优雅 - 一些代理人被允许提交任何类别的预订。它让我们在错误的地方有成千上万的预订。

我可以解决这个问题,但我只能通过嵌套查找来实现它:

for agent in Agent.objects.all():
    for booking in Booking.objects.filter(agent=agent):
        if booking.category not in agent.categories.all():
            # go through the automated allocation logic again

这很有效,但速度超慢。数据库和Django之间传递了大量数据。这也不是一次性的。我想定期审核新的预订,以确保它们在正确的位置。在检查代理数据库之后,似乎不可能发生另一个管理问题,我想查询其代理商类别中没有的预订。 < / p>

同样,嵌套查询不会起作用,但随着我们的数据集增长到数百万(甚至更多),我希望更有效地做到这一点。

我觉得应该可以使用F()查找来执行此操作,如下所示:

from django.db.models import F
bad = Booking.objects.exclude(category__in=F('agent__categories'))

但这不起作用:TypeError: 'Col' object is not iterable

我也试过.exclude(category=F('agent__categories'))虽然它对语法更满意,但它并没有排除&#34;正确的&#34;担保。

在M2M上进行此类F()查询的秘诀是什么?

在我设置change the Sentry configuration file(以及一些数据)之后,确切地说明了我的确切内容。请用它们来编写查询。目前唯一的回答点击和问题我在我的&#34;真实&#34;数据也是。

git clone https://github.com/oliwarner/djangorelquerytest.git
cd djangorelquerytest
python3 -m venv venv
. ./venv/bin/activate
pip install ipython Django==1.9a1

./manage.py migrate
./manage.py shell

在shell中,点火:

from django.db.models import F
from querytest.models import Category, Agent, Booking
Booking.objects.exclude(agent__categories=F('category'))

这是一个错误吗?有没有正确的方法来实现这一目标?

6 个答案:

答案 0 :(得分:6)

我可能错了,但我认为反过来应该这样做:

bad = Booking.objects.exclude(agent__categories=F('category'))

修改

如果上面没有工作,这是另一个想法。我在设置上尝试了类似的逻辑,似乎有效。尝试为ManyToManyField添加中间模型:

class Category(models.Model):
    pass

class Agent(models.Model):
    categories = models.ManyToManyField('Category', through='AgentCategory')

class AgentCategory(models.Model):
    agent = models.ForeignKey(Agent, related_name='agent_category_set')
    category = models.ForeignKey(Category, related_name='agent_category_set')

class Booking(models.Model):
    agent = models.ForeignKey('Agent')
    category = models.ForeignKey('Category')

然后你可以进行查询:

bad = Booking.objects.exclude(agent_category_set__category=F('category'))

当然,指定一个中间模型有它自己的含义,但我相信你可以处理它们。

答案 1 :(得分:1)

通常在处理m2m关系时,我采用混合方法。我会把问题分成两部分,一个python和sql部分。我发现这会大大加快查询速度,并且不需要任何复杂的查询。

您要做的第一件事是将代理获取到类别映射,然后使用该映射来确定不在分配中的类别。

def get_agent_to_cats():
    # output { agent_id1: [ cat_id1, cat_id2, ], agent_id2: [] }
    result = defaultdict(list)

    # get the relation using the "through" model, it is more efficient
    # this is the Agent.categories mapping
    for rel in Agent.categories.through.objects.all():
        result[rel.agent_id].append(rel.category_id)
    return result


def find_bad_bookings(request):
    agent_to_cats = get_agent_to_cats()

    for (agent_id, cats) in agent_to_cats.items():
        # this will get all the bookings that NOT belong to the agent's category assignments
        bad_bookings = Booking.objects.filter(agent_id=agent_id)
                                         .exclude(category_id__in=cats)

        # at this point you can do whatever you want to the list of bad bookings
        bad_bookings.update(wrong_cat=True)            

    return HttpResponse('Bad Bookings: %s' % Booking.objects.filter(wrong_cat=True).count())
  

当我在服务器上运行测试时,这是一些小的统计信息:   10,000个代理商   500类别   2,479,839代理到类别分配   5,000,000次预订

     

2,509,161 Bad Bookings。总持续时间149秒

答案 2 :(得分:1)

解决方案1:

您可以使用此查询找到好的预订

good = Booking.objects.filter(category=F('agent__categories'))

您可以检查此

的sql查询
print Booking.objects.filter(category=F('agent__categories')).query

因此,您可以从所有预订中排除好预订。 解决方案是:

Booking.objects.exclude(id__in=Booking.objects.filter(category=F('agent__categories')).values('id'))

它将创建一个MySql嵌套查询,这是针对此问题的最优化的MySql查询(据我所知)。

这个MySql查询会有点沉重,因为你的数据库很大但它只会打到数据库一次,而不是你第一次尝试循环,这将会预订* agent_categories次。

此外,如果您要存储这些数据,则可以通过使用日期过滤来减少数据集,并且在错误预订开始时您有近似值。

您可以定期使用上述命令检查预订是否不一致。 但我建议过度使用管理表格,并在预订时检查类别是否正确。 此外,您可以使用某些javascript仅添加管理表单中的类别,这些类别在当时为选定/登录代理提供。

解决方案2:

使用prefetch_related,这将大大减少您的时间,因为数据库命中次数非常少。

在此处阅读:https://docs.djangoproject.com/en/1.8/ref/models/querysets/

for agent in Agent.objects.all().prefetch_related('bookings, categories'):
    for booking in Booking.objects.filter(agent=agent):
        if booking.category not in agent.categories.all():

答案 3 :(得分:0)

这可能加快它的速度......

for agent in Agent.objects.iterator():
    agent_categories = agent.categories.all()
    for booking in agent.bookings.iterator():
        if booking.category not in agent_categories:
            # go through the automated allocation logic again

答案 4 :(得分:0)

这可能不是您正在寻找的内容,但您可以使用原始查询。我不知道它是否完全可以在ORM中完成,但这可以在你的github回购中使用:

Booking.objects.raw("SELECT id \
                     FROM querytest_booking as booking \
                     WHERE category_id NOT IN ( \
                         SELECT category_id \
                         FROM querytest_agent_categories as agent_cats \
                         WHERE agent_cats.agent_id = booking.agent_id);")

我认为除非您的应用被称为querytest,否则表格名称会有所不同。但无论哪种方式,都可以迭代,以便将自定义逻辑插入。

答案 5 :(得分:0)

你快到了。首先,让我们创建两个预订元素:

# b1 has a "correct" agent
b1 = Booking.objects.create(agent=Agent.objects.create(), category=Category.objects.create())
b1.agent.categories.add(b1.category)

# b2 has an incorrect agent
b2 = Booking.objects.create(agent=Agent.objects.create(), category=Category.objects.create())

以下是所有不正确预订的查询集(即:[b2]):

# The following requires a single query because
# the Django ORM is pretty smart
[b.id for b in Booking.objects.exclude(
    id__in=Booking.objects.filter(
        category__in=F('agent__categories')
    )
)]
[2]

请注意,根据我的经验,以下查询不会产生任何错误,但由于某些未知原因,结果也不正确:

Booking.objects.exclude(category__in=F('agent__categories'))
[]