Django 1.11自然排序查询集

时间:2018-06-04 22:01:04

标签: python django

我正在寻找一种自然地对Django的QuerySets进行排序的方法。我找到了similar question,但它没有关注QuerySet。相反,他们直接在Python中进行。

所以这是我的问题。让我们说我有这个模型:

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

在Django Admin界面中,我想使用一个过滤器,它将它们排序为字母数字。目前,他们按照这种方式排序:

typical sorting in django admin

我期待的是["BA 1", "BA 2", ...]的列表。我在the official documentation中找到admin.SimpleListFilter,这听起来很合适。但是我在queryset()函数中得到的是一个QuerySet,它不能以自然的方式排序,因为它不包含元素,只包含对数据库的查询。

QuerySet上的order_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')

如何转换QuerySet以获得所需的输出?或者有不同的方式吗? Django管理界面非常强大,这就是为什么我想尽可能使用它。但是这个功能真的不见了。

我目前正在使用Django 1.11

任何帮助,评论或提示都表示赞赏。谢谢你的帮助。

8 个答案:

答案 0 :(得分:10)

实际上这不是Django的bug,这是数据库在内部工作的方式,例如,看起来像MySql,例如默认情况下没有自然排序(我在Google上搜索的不多,所以也许我错了)。但是对于这种情况,我们可以使用一些解决方法。

我将所有带有示例和屏幕截图的内容放在https://gist.github.com/phpdude/8a45e1bd2943fa806aeffee94877680a

但基本上对于给定的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]

带有示例的屏幕截图

Raw user input data Data sorted by natural key

一些参考文献:

PS:好像在Django 1.9上添加了db函数Length(column_name),因此您应该可以使用它,但是通常任何Django版本都支持自定义db ORM函数调用,您可以调用length()功能。


使用Python库natsort

的其他示例

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

有效。但是如果表很大,可能会很慢。

  

从我的角度来看,它只能用于小于5万行的数据库表(例如,取决于您的数据库服务器性能等)。

from django.contrib.admin import ModelAdmin, register, SimpleListFilter
from django.db.models.functions import StrIndex, Concat
from django.db.models import Value as V
from natsort import natsorted

from .models import Item


class AlphanumericTruePythonSignatureFilter(SimpleListFilter):
    title = 'Signature (alphanumeric true python)'
    parameter_name = 'signature_alphanumeric_python'

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

    def queryset(self, request, queryset):
        if self.value() == 'signature':
            all_ids = list(queryset.values_list('signature', flat=True))
            # let's use "!:!" as a separator for signature values
            all_ids_sorted = "!:!" + "!:!".join(natsorted(all_ids))

            return queryset.order_by(
                StrIndex(V(all_ids_sorted), Concat(V('!:!'), 'signature')),
            )


@register(Item)
class Item(ModelAdmin):
    list_filter = [AlphanumericTruePythonSignatureFilter]

该案例的另一个屏幕截图示例 Python side sorted signatures list using natsorted

答案 1 :(得分:5)

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

queryset = (
    Item.objects.annotate(
        right_part=RawSQL("cast(split_part(signature, ' ', 2) as int)", ())
    ).order_by('right_part')
)

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

RawSQL()的优点是,您可以在何时何地应用特定于数据库的功能而变得非常明确。

如@schillingt所述,Func()也可能是一个选项。 另一方面,我会避免使用extra(),因为它可能已经过时了(请参阅:https://docs.djangoproject.com/en/2.2/ref/models/querysets/#extra)。

证明(对于PostgreSQL):

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

    def __str__(self):
        return self.signature

-----------------------------------------------------

import django
from django.db.models.expressions import RawSQL
from pprint import pprint
from backend.models import Item


class ModelsItemCase(django.test.TransactionTestCase):

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

        queryset = (
            Item.objects.annotate(
                right_part=RawSQL("cast(split_part(signature, ' ', 2) as int)", ())
            ).order_by('right_part')
        )

        pprint(list(queryset))

        self.assertEqual(queryset[0].signature, 'BA 1')
        self.assertEqual(queryset[1].signature, 'BA 2')
        self.assertEqual(queryset[2].signature, 'BA 10')
        self.assertEqual(queryset[3].signature, 'BA 100')
        self.assertEqual(queryset[4].signature, 'BA 1000')
        self.assertEqual(queryset[5].signature, 'BA 1001')
        self.assertEqual(queryset[6].signature, 'BA 1002')

结果:

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

答案 2 :(得分:3)

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

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)

这取决于数据更新与读取的频率,这可能非常有效。 sort_string会在每次更新项目时进行一次计算,但是在需要时可以作为简单字段使用。调整sort_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, ]
    ....

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

答案 3 :(得分:2)

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

Item.objects.extra(select={
    's1': 'cast(split_part(signature, \' \', 2) as int)', 
    's2': 'split_part(signature, \' \', 1)'
}).order_by('s2', 's1')

答案 4 :(得分:1)

您如何命名BA 1,BA 1000 ...等,最简单的解决方案是这样存储您的数据,BA 0001,BA 0002,然后使用order by,将起作用。 否则,您必须使用带有python的映射器,才能转换列表并使用python逻辑对其重新排序。

答案 5 :(得分:1)

我认为这将是简单的解决方案,但显然并非如此。在这个好问题上向您致以崇高的敬意。这是我建议的方法:

  • Read up on how others have solved this at the Postgres /数据库级别,并确定自行处理的最佳方法。您是否需要自定义类型,可以使用简单的正则表达式等
  • 根据上述情况,在Django migration中为Postgres实现该解决方案。您可能需要创建一个可以通过自定义SQL迁移完成的类型。或者也许您需要在数据库级别创建一个函数。
  • 使用新的postgres工件。这部分肯定会很复杂。您可能需要使用.extraFunc来访问该函数或类型。

这应该可行,但是肯定会涉及一些数据库更改和非典型的Django使用。

答案 6 :(得分:1)

进一步阐述我以前的建议和@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”计算为第一个空格或数字的位置,从而给出了真正的自然排序行为(例如,接受“ BA123” ”和“ BA 123”)

证明:

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
        )

针对sqlite3的查询结果为:

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

,对于PostgreSQL:

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

答案 7 :(得分:0)

假设签名字段的格式是固定的(用单个空格,第二部分是数字:[^ ]+ \d+), 我们可以将其分为两部分-base_name(字符串)和sig_value(整数)。

您也不需要SimpleListFilter(它有不同的用途-创建过滤器!)。您可以简单地覆盖get_queryset方法:

from django.contrib import admin
from django.db.models import F, IntegerField, TextField, Value
from django.db.models.functions import Cast, StrIndex, Substr

from .models import Item

@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        qs = super(ItemAdmin, self).get_queryset(request)
        return qs.annotate(
            # 1-indexed position of space
            space=StrIndex("name", Value(" ")),

            # part of text before the space
            base_name=Substr("name", 1, F("space") - 1, output_field=TextField()),

            # cast part of text after the space as int
            sig_value=Cast(Substr("name", F("space")), IntegerField()),
        ).order_by("base_name", "sig_value")