在Django中,我如何按任意非字母顺序对多选CharField进行排序?

11

假设有一个模型 Shirts,具有一个size CharField,其值被限制为少量的选项,例如 'small','medium','large','xlarge' 等。

要按尺码分组获取衬衫,您可以执行以下操作:

Shirts.objects.order_by('size')

但是Django会自然地按字母顺序排列组,即先是“large”,然后是“medium”,然后是“small”,最后是“xlarge”。我想要的是在“medium”之前有“small”,在“large”之前有“medium”等。

也就是说,我想自然地执行以下伪代码:

size_order = {'small': 1, 'medium': 2, 'large': 3, 'xlarge': 4}
Shirts.objects.order_by('size_order[size]')

如何最好地实现这个目标?

编辑:请查看我对下面答案的评论,了解各种建议方法的想法。我偶然发现了一种使用SQL ORDER BY CASE语法的自定义Manager/QuerySet方法,正在调查中。


可能是使用SQL的CASE WHEN/THEN语法django进行排序的重复问题。 - e4c5
5个回答

11

我找到了最接近我想要的东西,就是使用QuerySet.extra()方法来利用SQL的CASE WHEN/THEN语法,这是Django不能直接支持的:

CASE_SQL = '(case when size="small" then 1 when size="medium" then 2 when size="large" then 3 when size="xlarge" then 4 end)' 
Shirt.objects.extra(select={'shirt_order': CASE_SQL}, order_by=['shirt_order'])

尽管我的(人工)示例可能看起来有点过度和凌乱,但这正是我正在寻找的技巧!感谢大家提供的其他完全有效的解决方法,它们间接地激发了我去想出这种方法。

顺便说一句,很想创建一个自定义的模型Manager/QuerySet组合,通过SQL的CASE WHEN/THEN语法为这种自定义排序提供更本地的Django接口,但我会把它留作自己以后的作业任务!

注意:CASE WHEN/THEN的语法是特定于数据库的。上面的语法适用于SQLite。对于PostgreSQL,省略括号并使用转义的单引号而不是双引号。


这个简明易懂的解决方案对我在 2022 年使用 Django 3.9/PostgreSQL 是有效的。虽然将选项更改为整数字段也是一种可行的解决方案,但对于我来说会带来问题,因为我已经有基于选择名称的其他条件。将这些名称更改为整数会使代码更难以阅读和理解。 - David Rhoden

3

看起来你不想硬编码可能的选择(因为你使用了CharField),但与此同时,你也说有少量的选择。

如果你愿意硬编码选择,那么你可以改用IntegerField:

class Shirt(models.Model):
  SIZE_CHOICES = (
    (1, u'small'),
    (2, u'medium'),
    (3, u'large'),
    (4, u'x-large'),
    (5, u'xx-large'),
  )
  size = models.IntegerField(choices = SIZE_CHOICES)

如果您不想硬编码尺寸选择,则可能需要将可用尺寸移动到单独的模型中,并将其作为外键从Shirt模型引用。要使其任意可排序,您需要除主键之外的某种索引,可以对其进行排序。也许像这样:

class Size(models.Model):
  sortorder = models.IntegerField()
  name = models.CharField()
  class Meta:
    ordering = ['sortorder']

是的,我不想以这种方式进行硬编码。(顺便说一下,这件衬衫的例子只是一个人工的例子。)我真的希望有一种更简单的方法可以按字段的“映射”排序,而不必像你建议的那样使用单独的表格,但也许没有...... - Ghopper21
如果您不想硬编码可用选项,则必须将它们移动到像我上面建议的单独模型中。这是您唯一的选择。 - Daniel Eriksson
我想出了另一个选项 :-). 请看我的回答。 - Ghopper21

2

您应该使用有序的选择元组设置size字段。在您的models.py中,您需要编写如下代码:

from django.db import models

SHIRT_SIZE_CHOICES = (
    (u"0", u"small"),
    (u"1", u"medium"),
    (u"2", u"large"),
    (u"3", u"xlarge"))

class Shirt(models.Model):
    ...
    size = models.CharField(max_length=2, choices=SHIRT_SIZE_CHOICES)

然后,order_by 将按照你的意愿对它们进行排序。

1
谢谢,但出于各种原因,我希望字段值本身是“小”的等等。(请注意,这个Shirts示例只是一个人为的例子。) - Ghopper21

2

不需要编写自定义排序函数,只需将order字段附加到模型上即可。

class Shirt(models.Model):
    ...
    order = models.IntegerField(default=0)
    class Meta:
        ordering = ('order',)

Shirt.objects.filter(name='Awesome Shirt')

或者更恰当地说,创建一个名为Size的新模型。

1
谢谢,这很有道理,但我想避免创建另一个字段或表格。(“Shirts”只是一个愚蠢的例子。)请查看我的答案,了解我如何通过在查询集中注入使用SQL CASE WHEN/THEN表达式实现动态创建自定义排序字段。 - Ghopper21

1

如果您不想将字段值存储为整数,则内置的order_by()方法将无法处理您的自定义情况。一旦检索到数据,您必须创建自己的函数来对数据进行排序。

为了做到这一点,当然最好的方法是将任意值映射到相应顺序的整数中,然后按照该排列进行排序 :)


啊...你的意思是创建一个自定义模型管理器,它返回一个新的QuerySet子类,其中包含一个新的order_by_map()方法(或其他方法),以实现我想要的功能?如果是这样,那么这很有道理。对于我上面提供的愚蠢示例来说,这可能有点过头了,但如果可以实现,那就是我要找的东西。 - Ghopper21
好的,看起来我需要为 SQL ORDER BY CASE 添加一个 QuerySet 方法。不确定是否有标准的 SQL 语法,还是它是特定于数据库的……正在调查中。 - Ghopper21
1
哦,等等,我明白你的意思了,你不是在谈论自定义 Manager/QuerySet,而是在查询后对结果进行排序。没错,那肯定是个选项,但不是我要找的。出于某些原因,你的答案引发了我关于自定义 Manager/QuerySets 的想法,所以还是谢谢你! - Ghopper21
哈哈,我很高兴能够帮忙。顺便说一下,好的解决方案。 - Intenex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接