根据另一个相关模型过滤相关字段';Django的s M2M关系

根据另一个相关模型过滤相关字段';Django的s M2M关系,django,django-models,django-queryset,Django,Django Models,Django Queryset,所以我有一个预订系统。代理商(提交预订的人员和组织)只允许在我们分配给他们的类别中进行预订。许多代理可以分配到相同的类别。这是一个简单的多对多。以下是这些模型的外观: class Category(models.Model): pass class Agent(models.Model): categories = models.ManyToManyField('Category') class Booking(models.Model): agent = model

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

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.categories的预订吗? 我们刚刚注意到,由于一个愚蠢的管理员错误,一些代理被允许提交任何类别的预订。它给我们留下了成千上万的错误地点的预订

我可以解决此问题,但我只能通过嵌套查找使其工作:

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之间传输。这也不是一次性的。我想定期审核新预订,以确保它们位于正确的位置。似乎不可能出现另一个管理问题,因此在检查代理数据库后,我想查询不属于其代理类别的预订。

同样,嵌套查询将不起作用,但随着我们的数据集增长到数百万(甚至更多),我希望能更有效地完成这项工作

我觉得通过
F()
查找应该可以做到这一点,比如:

from django.db.models import F
bad = Booking.objects.exclude(category__in=F('agent__categories'))
但这不起作用:
TypeError:“Col”对象不可编辑

我还尝试了
.exclude(category=F('agent\uu categories'))
,虽然它的语法比较好,但它并不排除“正确”的预订

在M2M上执行这种
F()
查询的秘密公式是什么


帮助我在设置(和一些数据)后准确地确定我的目标。请使用它们来编写查询。目前唯一的答案也是我在“真实”数据上看到的

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
在炮弹中,开火:

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

那是虫子吗?有没有合适的方法来实现这一点?

我有可能是错的,但我认为反过来应该可以做到:

bad=Booking.objects.exclude(agent\u 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'))

当然,指定中间模型有其自身的含义,但我相信您可以处理它们

这可能会加快速度

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

这可能不是您想要的,但您可以使用原始查询。我不知道是否可以完全在ORM中完成,但这在您的github repo中起作用:

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
。但无论哪种方式,您都可以重复此过程,将自定义逻辑插入其中。

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

# 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]
):

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

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

通常在处理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())
以下是我在服务器上运行测试时的一些统计信息: 10000名特工 500类 2479839代理到类别分配 500万次预订

2509161次不良预订。总持续时间149秒

解决方案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查询会有点重,因为您的数据库很大,但它只会命中数据库一次,而不是您第一次尝试的循环命中预订*代理\类别次数

此外,如果您正在存储数据集,并且在错误的预订开始时具有近似值,则可以通过使用日期筛选来减少数据集

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

解决方案2:

使用预回迁,这将大大减少您的时间,因为数据库命中率非常低

请在此处阅读:


如果是
bad=Booking.objects.exclude(agent\u category\u set\u contains=F('category'))
@jcplower Nope,这将是一个错误,因为使用此查询您将比较不同的表
agent\u category\u set
AgentCategory
,而
category
category
模型。此外,它将为您提供:
TypeError:Related Field get invalid lookup:contains
,用于此查询。
Booking.objects.exclude(id__in=Booking.objects.filter(category=F('agent__categories')).values('id'))
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():