Django:自然排序查询集
Django: Natural Sort QuerySet
我正在寻找一种自然排序 Django 的查询集的方法。我找到了一个similar question,但是它没有关注QuerySets。相反,他们直接在 Python.
中进行操作
所以这是我的问题。假设我有这个模型:
class Item(models.Model):
signature = models.CharField('Signatur', max_length=50)
在 Django 管理界面中,我想使用一个过滤器,对它们进行字母数字排序。目前,它们是这样排序的:
我期望的是 ["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
感谢任何帮助、评论或提示。感谢您的帮助。
如何命名 BA 1、BA 1000 ...等,最简单的解决方案是将数据存储为 BA 0001、BA 0002,然后使用 order by,这样就可以了。
否则你必须使用 python 应用映射器来转换你的列表并使用 python 逻辑重新排序。
我认为这是一个简单的解决方案,但显然不是。感谢您提出的好问题。这是我建议的方法:
- Read up on how others have solved this at the Postgres / DB 级别并确定自己处理它的最佳方法。您需要自定义类型吗,可以使用简单的正则表达式等吗
- 根据以上内容,在 Django migration 中为 Postgres 实施该解决方案。您可能需要创建一个可以通过自定义 SQL 迁移完成的类型。或者您可能需要在数据库级别创建一个函数。
- 利用新的 postgres 神器。这部分肯定会很复杂。您可能需要使用
.extra
or a Func
来访问函数或类型。
这应该是可能的,但它肯定会涉及一些数据库更改和非典型的 django 用法。
我假设您的签名字段遵循以下模式:AAA 123
字母后跟 space 后跟数字 (int)。
Item.objects.extra(select={
's1': 'cast(split_part(signature, \' \', 2) as int)',
's2': 'split_part(signature, \' \', 1)'
}).order_by('s2', 's1')
这实际上不是 Django 的错误,那是数据库内部工作的方式,例如看起来 MySql 默认情况下没有自然排序(我没有用谷歌搜索很多,所以也许我错了) .但是我们可以使用一些解决方法来解决这个问题。
我把所有的例子和截图都放在 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]
带有示例的屏幕截图
一些参考资料:
- http://www.mysqltutorial.org/mysql-natural-sorting/
- https://docs.djangoproject.com/en/2.0/ref/contrib/admin/
PS: 好像db函数Length(column_name)
是在Django 1.9上添加的,所以你应该可以使用它,但一般任何Django版本都支持自定义db ORM函数调用,你可以调用字段的 length()
函数。
使用 Python 库的额外示例 natsort
它会工作,但需要在正确排序之前加载所有可能的签名,因为它使用 python 端而不是数据库端对行列表进行排序。
有效。但是如果 table 尺寸很大,它可能会很慢。
From my point of view it should be used only on db tables sizes less than 50 000 rows (for example, depends on your DB server performance & etc).
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]
还有一个案例的截图示例
如果你不介意针对特定的数据库,你可以使用 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
一个简单的方法是添加另一个仅用于排序的字段:
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, ]
....
所以很容易触发重新计算
进一步阐述我之前的建议和@Alexandr Shurigin 给出的有趣解决方案,我现在建议另一种选择。
这个新解决方案将 "signature" 分成两个字段:
- 代码:可变长度的字母数字字符串
- 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" 作为第一个 space 或数字的位置,从而给出真正自然排序行为(接受,例如 "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
假设签名字段的格式是固定的(单个 space,第二部分是数字:[^ ]+ \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")
我正在寻找一种自然排序 Django 的查询集的方法。我找到了一个similar question,但是它没有关注QuerySets。相反,他们直接在 Python.
中进行操作所以这是我的问题。假设我有这个模型:
class Item(models.Model):
signature = models.CharField('Signatur', max_length=50)
在 Django 管理界面中,我想使用一个过滤器,对它们进行字母数字排序。目前,它们是这样排序的:
我期望的是 ["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
感谢任何帮助、评论或提示。感谢您的帮助。
如何命名 BA 1、BA 1000 ...等,最简单的解决方案是将数据存储为 BA 0001、BA 0002,然后使用 order by,这样就可以了。 否则你必须使用 python 应用映射器来转换你的列表并使用 python 逻辑重新排序。
我认为这是一个简单的解决方案,但显然不是。感谢您提出的好问题。这是我建议的方法:
- Read up on how others have solved this at the Postgres / DB 级别并确定自己处理它的最佳方法。您需要自定义类型吗,可以使用简单的正则表达式等吗
- 根据以上内容,在 Django migration 中为 Postgres 实施该解决方案。您可能需要创建一个可以通过自定义 SQL 迁移完成的类型。或者您可能需要在数据库级别创建一个函数。
- 利用新的 postgres 神器。这部分肯定会很复杂。您可能需要使用
.extra
or aFunc
来访问函数或类型。
这应该是可能的,但它肯定会涉及一些数据库更改和非典型的 django 用法。
我假设您的签名字段遵循以下模式:AAA 123
字母后跟 space 后跟数字 (int)。
Item.objects.extra(select={
's1': 'cast(split_part(signature, \' \', 2) as int)',
's2': 'split_part(signature, \' \', 1)'
}).order_by('s2', 's1')
这实际上不是 Django 的错误,那是数据库内部工作的方式,例如看起来 MySql 默认情况下没有自然排序(我没有用谷歌搜索很多,所以也许我错了) .但是我们可以使用一些解决方法来解决这个问题。
我把所有的例子和截图都放在 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]
带有示例的屏幕截图
一些参考资料:
- http://www.mysqltutorial.org/mysql-natural-sorting/
- https://docs.djangoproject.com/en/2.0/ref/contrib/admin/
PS: 好像db函数Length(column_name)
是在Django 1.9上添加的,所以你应该可以使用它,但一般任何Django版本都支持自定义db ORM函数调用,你可以调用字段的 length()
函数。
使用 Python 库的额外示例 natsort
它会工作,但需要在正确排序之前加载所有可能的签名,因为它使用 python 端而不是数据库端对行列表进行排序。
有效。但是如果 table 尺寸很大,它可能会很慢。
From my point of view it should be used only on db tables sizes less than 50 000 rows (for example, depends on your DB server performance & etc).
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]
还有一个案例的截图示例
如果你不介意针对特定的数据库,你可以使用 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
一个简单的方法是添加另一个仅用于排序的字段:
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, ]
....
所以很容易触发重新计算
进一步阐述我之前的建议和@Alexandr Shurigin 给出的有趣解决方案,我现在建议另一种选择。
这个新解决方案将 "signature" 分成两个字段:
- 代码:可变长度的字母数字字符串
- 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" 作为第一个 space 或数字的位置,从而给出真正自然排序行为(接受,例如 "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
假设签名字段的格式是固定的(单个 space,第二部分是数字:[^ ]+ \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")