【问题标题】:How do I order a model, based on multiple values from a ManyToManyField?如何根据 ManyToManyField 中的多个值订购模型?
【发布时间】:2016-06-07 08:39:47
【问题描述】:

基本上,我需要根据 ManyToManyField 中的多个值进行排序。

所以我想要实现的是将具有最受质疑值的对象放在顶部,然后继续使用具有较少质疑值的对象。继续处理没有任何这些值的对象。

模型:

    class Color(models.Model):
        name = models.CharField(max_length=200)

    class Item(models.Model):
        surface_color = models.ManyToManyField(Color)

基于上述模型创建的实例:

  • 第 1 项)surface_color=[1, 2, 3]
  • 第 2 项)surface_color=[1, 2]
  • 第 3 项)surface_color=[2]
  • 第 4 项)surface_color=[1, 3]

现在我需要订购多种颜色:

假查询:Item.objects.all().order_by(surface_color_id=[1, 3])

查询应该有以下结果:

  1. 第 4 项,因为它同时具有 1 和 3
  2. 第 1 项,因为它同时具有 1 和 3
  3. 第 2 项,因为它有 1 个
  4. 第 3 项,因为它没有

这可以通过单个查询集实现吗?还是我需要为每个组合发送多个查询?

我在互联网上发现的唯一内容是关于订购多个字段,这是关于

感谢任何帮助。

【问题讨论】:

  • 完全不清楚您想要的排序与数据的关系。你能进一步解释一下吗?
  • @DanielRoseman 对不起,这有帮助吗?
  • @DanielRoseman 我猜不是,请告诉我需要解释什么。 :(
  • 它确实有助于解释你想要什么(尽管我仍然不清楚项目 4 和 1 的排序是否有任何意义)。不幸的是,我想不出实现它的好方法。
  • 您似乎随意分配订单。 django 或数据库应该如何知道这一点?您必须想出一种机制来告诉数据库您的想法。专门用于订购的专栏怎么样?

标签: django postgresql django-models


【解决方案1】:

这应该会给你你想要的:

list(Item.objects.filter(surface_color__in=[1,3]).distinct().annotate(num_colors=Count('surface_color')).order_by('-num_colors')) + list(Item.objects.exclude(surface_color__in=[1,3]).distinct())

它需要两个查询,但您不需要为每个项目单独查询。


用 cmets 分解相同的逻辑:

# Make sure you import Count somewhere in the file first
from django.db.models import Count

# id of Color objects to match
color_ids_to_match = [1,3]

#-----------------------------------------------------------------------------------------------------------------------

# Item objects with Color objects matching `color_ids_to_match`
# - The `surface_color` field has **at least one** Color object with a matching id from `color_ids_to_match`
# - This matches Items #4, #1, #2 from your sample data
items_with_matching_color = Item.objects.filter(surface_color__in=color_ids_to_match).distinct()

# Adds a select field to the query to track the number of surface_color objects matched
# - **NOT the total** number of Color objects associated through `surface_color`
items_with_matching_color = items_with_matching_color.annotate(num_colors=Count('surface_color'))

# Order by that field in descending order
# - Note that the order is undetermined between Item objects with the same `num_colors` value
items_with_matching_color = items_with_matching_color.order_by('-num_colors')

#-----------------------------------------------------------------------------------------------------------------------

# Item objects **NOT** associated with any Color objects with a id in `color_ids_to_match`
# - This matches Item #3 from your sample data
items_without_matching_color = Item.objects.exclude(surface_color__in=color_ids_to_match).distinct()

# Optional - Sets the num_colors field to 0 for this queryset in case you need this information
from django.db.models.expressions import RawSQL
items_without_matching_color = items_without_matching_color.annotate(num_colors=RawSQL('0', ()))

#-----------------------------------------------------------------------------------------------------------------------

# Convert the two querysets to lists and concatenate them
# - This is necessary because a simple queryset union such as `items_with_matching_color | items_without_matching_color`
#   does not maintain the order between the two querysets
ordered_items = list(items_with_matching_color) + list(items_without_matching_color)

使用您的样本数据输出:

>>> ordered_items
[<Item: 1: <QuerySet [<Color: 1>, <Color: 2>, <Color: 3>]>>, <Item: 4: <QuerySet [<Color: 1>, <Color: 3>]>>, <Item: 2: <QuerySet [<Color: 1>, <Color: 2>]>>, <Item: 3: <QuerySet [<Color: 2>]>>]

请注意,此处第 1 项在第 4 项之前。您提到两者之间的顺序无关紧要,因为它们都匹配相同数量的 Color 对象。您可以向order_by 添加另一个参数,以根据您的需要进一步优化排序。

获取匹配颜色对象的数量:

<Item: 1: <QuerySet [<Color: 1>, <Color: 2>, <Color: 3>]>>
>>> ordered_items[0].num_colors
2
>>> ordered_items[1]
<Item: 4: <QuerySet [<Color: 1>, <Color: 3>]>>
>>> ordered_items[1].num_colors
2
>>> ordered_items[2]
<Item: 2: <QuerySet [<Color: 1>, <Color: 2>]>>
>>> ordered_items[2].num_colors
1
>>> ordered_items[3]
<Item: 3: <QuerySet [<Color: 2>]>>
>>> ordered_items[3].num_colors
0

【讨论】:

    【解决方案2】:

    也许您可以将它遇到的任何 ID 相加。它不适用于更大的 ID 集,但如果只有这些(我怀疑)......

    from django.db.models import Sum
    Item.objects.annotate(my_order=Sum('surface_color__id')).order_by('-my_order')
    

    【讨论】:

      【解决方案3】:

      对于您要订购的每个color_id,您需要创建一个subquery 并标记True/False 该行是否有此color_id。 假设您有一个 ItemQuerySet 类,那么您可以通过以下方式抽象您的订单逻辑:

      class ItemQuerySet(models.QuerySet):
      
          def order_by_colors(self, color_ids):
              extra_kwargs = {
                  'select': {},
                  'select_params' = [],
                  'order_by': []
              }
      
              for (index, color_id) in enumerate(color_ids):
                  order_by_key = 'color_%s' % index
                  extra_kwargs['select'][order_by_key] = """EXISTS(
                      SELECT 1 FROM myapp_item_colors
                      WHERE myapp_item_colors.color_id = %s
                          AND myapp_item_colors.item_id = myapp_item.id)"""
                  extra_kwargs['select_params'].append(color_id)
                  extra_kwargs['order_by'].append('-%s' % order_by_key)
      
              return self.extra(**extra_kwargs)
      
      #usage
      Item.objects.all().order_by_colors([1, 3])
      

      【讨论】:

        【解决方案4】:

        因此,假设您有名为 itemscolorsitem_surface_colors 的 Postgres 表,您可以使用如下查询:

        SELECT items.*
        FROM   items
        LEFT OUTER JOIN item_surface_colors isc
        ON     isc.item_id = items.id
        AND    isc.surface_color_id IN (1,3)
        GROUP BY items.id
        ORDER BY COUNT(DISTINCT isc.surface_color_id) DESC
        

        我可以告诉您如何在 ActiveRecord 中执行该查询,但对于 Django,恐怕您只能靠自己了。 :-)

        raw(...) 方法可以执行原始 SQL 查询,但请确保参数化输入以防止 SQL 注入(我在这里没有这样做)。在此处阅读有关在 Django 中执行原始 sql 查询的更多信息:https://docs.djangoproject.com/en/1.9/topics/db/sql/

        Item.objects.raw("SELECT items.* \
        FROM   items \
        LEFT OUTER JOIN item_surface_colors isc \
        ON     isc.item_id = items.id \
        AND    isc.surface_color_id IN (1,3) \
        GROUP BY items.id \
        ORDER BY COUNT(DISTINCT isc.surface_color_id) DESC")
        

        【讨论】:

          猜你喜欢
          • 2017-07-05
          • 2011-05-15
          • 2012-07-06
          • 2012-01-29
          • 1970-01-01
          • 2012-09-18
          • 2022-11-13
          • 2018-06-30
          • 2013-02-15
          相关资源
          最近更新 更多