【问题标题】:Filter related field against another related model's M2M relationship in Django根据 Django 中另一个相关模型的 M2M 关系过滤相关字段
【发布时间】:2015-12-23 08:51:16
【问题描述】:

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

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.category 不在 Booking.agent.categories 中的 Bookings 吗?

我们刚刚注意到——由于一个愚蠢的管理员错误——允许一些代理向任何类别提交预订。它让我们在错误的地方进行了数千次预订。

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

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' object is not iterable

我也尝试过.exclude(category=F('agent__categories')),虽然它对那里的语法更满意,但它并不排除“正确”的预订。

在 M2M 上执行这种F() 查询的秘诀是什么?


在我设置a Github repo with these models(和一些数据)之后,帮助确定我的确切身份。请使用它们来编写查询。当前唯一的答案是我在“真实”数据上看到的问题。

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

这是一个错误吗?有没有合适的方法来实现这一点?

【问题讨论】:

    标签: django django-models django-queryset


    【解决方案1】:

    我有可能是错的,但我认为反过来应该可以解决问题:

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

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

    【讨论】:

    • 应该是bad = Booking.objects.exclude(agent_category_set__contains=F('category'))
    • @jcfollower 不,这将是一个错误,因为使用此查询您正在比较不同的表。 agent_category_setAgentCategory,而 categoryCategory 模型。此外,它会为您提供:TypeError: Related Field got invalid lookup: contains 用于此查询。
    【解决方案2】:

    解决方案 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 查询会有点重,因为您的数据库很大,但它只会访问数据库一次,而不是您第一次尝试循环,这将命中 bookings * 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】:

      通常在处理 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 次不良预订。总时长 149 秒

      【讨论】:

        【解决方案4】:

        这可能会加快速度...

        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
        

        【讨论】:

          【解决方案5】:

          这可能不是您要查找的内容,但您可以使用原始查询。我不知道它是否可以完全在 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。但无论哪种方式,都可以迭代,以便您将自定义逻辑插入其中。

          【讨论】:

            【解决方案6】:

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

            # 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'))
            []
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2021-04-21
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多