如何根据ManyToManyField中的多个值订购模型?

时间:2016-06-07 08:39:47

标签: django postgresql django-models

基本上,我需要根据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项,因为它没有
  5. 单个查询集可以实现吗?或者我是否需要为每个组合垃圾邮件多次查询?

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

    感谢任何帮助。

4 个答案:

答案 0 :(得分:2)

这可以给你你想要的东西:

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

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

与评论相同的逻辑:

# 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添加另一个参数,以进一步完善您的需求排序。

获取匹配的Color对象的数量:

<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

答案 1 :(得分:0)

假设你有一个名为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")

答案 2 :(得分:0)

也许你可以总结它遇到的任何ID。它不会为更大的ID集合工作,但如果它只是这些(我怀疑)......

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

答案 3 :(得分:0)

对于您要订购的每个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])