Python Django:自然排序查询集

Python Django:自然排序查询集,python,django,natural-sort,Python,Django,Natural Sort,我正在寻找一种自然排序Django的查询集的方法。我找到了一个,但它没有集中在QuerySets上。相反,他们直接用Python来完成 这就是我的问题。假设我有这个模型: class Item(models.Model): signature = models.CharField('Signatur', max_length=50) 在Django管理界面中,我想使用一个过滤器,对它们进行字母数字排序。目前,它们按以下方式排序: 我所期望的是[“BA 1”、“BA 2”、…]的列表。

我正在寻找一种自然排序Django的查询集的方法。我找到了一个,但它没有集中在QuerySets上。相反,他们直接用Python来完成

这就是我的问题。假设我有这个模型:

class Item(models.Model):
    signature = models.CharField('Signatur', max_length=50)
在Django管理界面中,我想使用一个过滤器,对它们进行字母数字排序。目前,它们按以下方式排序:

我所期望的是
[“BA 1”、“BA 2”、…]
的列表。我在中找到了
admin.SimpleListFilter
,这听起来非常合适。但是我在
queryset()
函数中得到的是一个queryset,它不能以自然的方式排序,因为它不包含元素,只包含对数据库的查询

QuerySet上的
order\u by
方法给出了与图像中相同的顺序。有没有办法操纵QuerySet以使其自然排序

到目前为止,我的代码是:

class AlphanumericSignatureFilter(admin.SimpleListFilter):
    title = 'Signature (alphanumeric)'
    parameter_name = 'signature_alphanumeric'

    def lookups(self, request, model_admin):
        return (
            ('signature', 'Signature (alphanumeric)'),
        )

    def queryset(self, request, queryset: QuerySet):
        return queryset.order_by('signature')
如何转换查询集以获得所需的输出?还是有不同的方法?Django管理界面非常强大,这就是为什么我希望尽可能长时间地使用它。但这一功能确实缺失了

我目前正在使用Django 1.11


任何帮助、评论或提示都将不胜感激。感谢您的帮助。

如何获得命名BA 1、BA 1000…等,最简单的解决方案是将数据存储为BA 0001、BA 0002,然后使用order by,这样就可以了。
否则,您必须使用python应用映射器来转换列表并使用python逻辑对其重新排序。

我认为这是一个简单的解决方案,但显然不是。这是一个好问题,真是太好了。这是我建议的方法:

  • /DB级别,并确定自己处理它的最佳方式。您是否需要自定义类型,是否可以使用简单的正则表达式等
  • 根据上述情况,在一个系统中为Postgres实施该解决方案。您可能需要创建一个可以通过自定义SQL迁移完成的类型。或者您可能需要在数据库级别创建一个函数
  • 利用新的postgres工件。这部分肯定会很复杂。您可能需要使用或访问函数或类型

这应该是可能的,但肯定会涉及一些DB更改和不典型的django用法。

我假设您的签名字段遵循以下模式:
AAA 123
字母后跟空格,后跟数字(int)


实际上,这不是Django的bug,这是数据库内部工作的方式,例如MySql默认情况下没有自然排序(我在谷歌上搜索得不多,所以可能我错了)。但是我们可以为这个案例使用一些变通方法

我把所有的例子和截图放在

但是基本上对于给定的
models.py
文件

from django.db import models


class Item(models.Model):
    signature = models.CharField('Signatur', max_length=50)

    def __str__(self):
        return self.signature
例如,我使用了
admin.py
来实现正确的过滤器

from django.contrib.admin import ModelAdmin, register, SimpleListFilter
from django.db.models.functions import Length, StrIndex, Substr, NullIf, Coalesce
from django.db.models import Value as V

from .models import Item


class AlphanumericSignatureFilter(SimpleListFilter):
    title = 'Signature (alphanumeric)'
    parameter_name = 'signature_alphanumeric'

    def lookups(self, request, model_admin):
        return (
            ('signature', 'Signature (alphanumeric)'),
        )

    def queryset(self, request, queryset):
        if self.value() == 'signature':
            return queryset.order_by(
                Coalesce(Substr('signature', V(0), NullIf(StrIndex('signature', V(' ')), V(0))), 'signature'),
                Length('signature'),
                'signature'
            )


@register(Item)
class Item(ModelAdmin):
    list_filter = [AlphanumericSignatureFilter]
带有示例的屏幕截图

一些参考资料:

PS:看起来Django 1.9中添加了db function
Length(column\u name)
,因此您应该能够使用它,但通常任何Django版本都支持自定义db ORM函数调用,您可以调用字段的
Length()
函数


使用Python库的额外示例
natsort
它可以工作,但需要在正确排序之前加载所有可能的签名,因为它使用python端而不是DB端对行列表进行排序

它起作用了。但是如果桌子太大,速度可能会很慢

在我看来,它应该只用于小于50000行的db表(例如,取决于您的db服务器性能等等)

还有一个案例的截图示例

如果您不介意以特定数据库为目标,可以使用RawSQL()插入SQL表达式来解析“签名”字段,然后用结果注释记录集;例如(PostgreSQL):

(如果需要支持不同的数据库格式,还可以检测活动引擎并相应地提供合适的表达式)

RawSQL()的好处在于,在何时何地应用特定于数据库的功能时,您可以非常明确地表示

正如@schillingt所指出的,Func()也可能是一个选项。 另一方面,我会避免使用extra(),因为它可能会被弃用(请参阅:)

证明(针对PostgreSQL):

结果:

test_item_sorting (backend.tests.test_item.ModelsItemCase) ... [<Item: BA 1>,
 <Item: BA 10>,
 <Item: BA 100>,
 <Item: BA 2>,
 <Item: BA 1002>,
 <Item: BA 1000>,
 <Item: BA 1001>]

[<Item: BA 1>,
 <Item: BA 2>,
 <Item: BA 10>,
 <Item: BA 100>,
 <Item: BA 1000>,
 <Item: BA 1001>,
 <Item: BA 1002>]
ok

----------------------------------------------------------------------
Ran 1 test in 0.177s
test\u item\u排序(backend.tests.test\u item.ModelsItemCase)。。。[,
,
,
,
,
,
]
[,
,
,
,
,
,
]
好啊
----------------------------------------------------------------------
在0.177秒内运行了1次测试

一种简单的方法是添加另一个仅用于排序的字段:

class Item(models.Model):
    signature = models.CharField('Signatur', max_length=50)
    sort_string = models.CharField(max_length=60, blank=True, editable=False)

    class Meta:
        ordering = ['sort_string']

    def save(self, *args, **kwargs):
        parts = self.signature.split()
        parts[2] = "{:06d}".format(int(parts[2]))
        self.sort_string = "".join(parts)
        super().save(*args, **kwargs)
根据数据更新和读取的频率,这可能非常有效<代码>排序\u字符串在项目更新时计算一次,但在需要时作为简单字段可用。调整
sort\u string
的计算方式非常简单,可以满足您的确切要求

在管理员中添加重新保存操作也可能很有用(尤其是在开发过程中):

def re_save(modeladmin, request, queryset):
    for item in queryset:
        item.save()
re_save.short_description = "Re-save"

class ItemAdmin(admin.ModelAdmin):
    actions = [re_save, ]
    ....

因此很容易触发重新计算。

进一步阐述我之前的建议和@Alexandr Shurigin给出的有趣解决方案,我现在建议另一种选择

此新解决方案将“签名”分为两个字段:

  • 代码:可变长度的数字字符串
  • weigth:一个数值,可能前导0被忽略
鉴于:

    [
        'X 1',
        'XY 1',
        'XYZ 1',
        'BA 1',
        'BA 10',
        'BA 100',
        'BA 2',
        'BA 1002',
        'BA 1000',
        'BA 1001',
        'BA 003',
    ]
预期结果是:

    [
        'BA 1',
        'BA 2',
        'BA 003',
        'BA 10',
        'BA 100',
        'BA 1000',
        'BA 1001',
        'BA 1002',
        'X 1',
        'XY 1',
        'XYZ 1',
    ]
由于django.db.models.functions模块,所有计算都以通用方式委托给数据库

    queryset = (
        Item.objects.annotate(
            split_index=StrIndex('signature', Value(' ')),
        ).annotate(
            left=Substr('signature', Value(1), 'split_index', output_field=CharField()),
            right=Substr('signature', F('split_index'), output_field=CharField()),
        ).annotate(
            code=Trim('left'),
            weight=Cast('right', output_field=IntegerField())
        ).order_by('code', 'weight')
    )
一个更紧凑但可读性更低的解决方案是:

    queryset = (
        Item.objects.annotate(
            split_index=StrIndex('signature', Value(' ')),
        ).annotate(
            code=Trim(Substr('signature', Value(1), 'split_index', output_field=CharField())),
            weight=Cast(Substr('signature', F('split_index'), output_field=CharField()), output_field=IntegerField())
        ).order_by('code', 'weight')
    )
这里我真正缺少的是一个“IndexOf”函数,用于计算“split_index”作为第一个空格或数字的位置,从而提供一种真正自然的排序行为(到
    [
        'X 1',
        'XY 1',
        'XYZ 1',
        'BA 1',
        'BA 10',
        'BA 100',
        'BA 2',
        'BA 1002',
        'BA 1000',
        'BA 1001',
        'BA 003',
    ]
    [
        'BA 1',
        'BA 2',
        'BA 003',
        'BA 10',
        'BA 100',
        'BA 1000',
        'BA 1001',
        'BA 1002',
        'X 1',
        'XY 1',
        'XYZ 1',
    ]
    queryset = (
        Item.objects.annotate(
            split_index=StrIndex('signature', Value(' ')),
        ).annotate(
            left=Substr('signature', Value(1), 'split_index', output_field=CharField()),
            right=Substr('signature', F('split_index'), output_field=CharField()),
        ).annotate(
            code=Trim('left'),
            weight=Cast('right', output_field=IntegerField())
        ).order_by('code', 'weight')
    )
    queryset = (
        Item.objects.annotate(
            split_index=StrIndex('signature', Value(' ')),
        ).annotate(
            code=Trim(Substr('signature', Value(1), 'split_index', output_field=CharField())),
            weight=Cast(Substr('signature', F('split_index'), output_field=CharField()), output_field=IntegerField())
        ).order_by('code', 'weight')
    )
import django
#from django.db.models.expressions import RawSQL
from pprint import pprint
from backend.models import Item
from django.db.models.functions import Length, StrIndex, Substr, Cast, Trim
from django.db.models import Value, F, CharField, IntegerField


class ModelsItemCase(django.test.TransactionTestCase):

    def test_item_sorting(self):

        signatures = [
            'X 1',
            'XY 1',
            'XYZ 1',
            'BA 1',
            'BA 10',
            'BA 100',
            'BA 2',
            'BA 1002',
            'BA 1000',
            'BA 1001',
            'BA 003',
        ]
        for signature in signatures:
            Item.objects.create(signature=signature)
        print(' ')
        pprint(list(Item.objects.all()))
        print('')

        expected_result = [
            'BA 1',
            'BA 2',
            'BA 003',
            'BA 10',
            'BA 100',
            'BA 1000',
            'BA 1001',
            'BA 1002',
            'X 1',
            'XY 1',
            'XYZ 1',
        ]

        queryset = (
            Item.objects.annotate(
                split_index=StrIndex('signature', Value(' ')),
            ).annotate(
                code=Trim(Substr('signature', Value(1), 'split_index', output_field=CharField())),
                weight=Cast(Substr('signature', F('split_index'), output_field=CharField()), output_field=IntegerField())
            ).order_by('code', 'weight')
        )
        pprint(list(queryset))

        print(' ')
        print(str(queryset.query))
        self.assertSequenceEqual(
            [row.signature for row in queryset],
            expected_result
        )
SELECT 
    "backend_item"."id", 
    "backend_item"."signature", 
    INSTR("backend_item"."signature",  ) AS "split_index", 
    TRIM(SUBSTR("backend_item"."signature", 1, INSTR("backend_item"."signature",  ))) AS "code", 
    CAST(SUBSTR("backend_item"."signature", INSTR("backend_item"."signature",  )) AS integer) AS "weight" 
FROM "backend_item" 
ORDER BY "code" ASC, "weight" ASC
SELECT 
    "backend_item"."id", 
    "backend_item"."signature", 
    STRPOS("backend_item"."signature",  ) AS "split_index", 
    TRIM(SUBSTRING("backend_item"."signature", 1, STRPOS("backend_item"."signature",  ))) AS "code", 
    (SUBSTRING("backend_item"."signature", STRPOS("backend_item"."signature",  )))::integer AS "weight" 
FROM "backend_item" 
ORDER BY "code" ASC, "weight" ASC