我有一个简单的模型设置如下,
import random
import string
from django.db import models
def random_default():
random_str = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
return {"random": random_str, "total_price": random.randint(1, 100)}
class Foo(models.Model):
cart = models.JSONField(default=random_default)
我想从所有total_price
实例中得到
Foo
的总和。在本机 Python 中,我可以执行类似下面的操作来获得总和,但我认为它不是最优的。
sum(foo.cart["total_price"] for foo in Foo.objects.all())
我用 Django 尝试了以下聚合查询,但似乎没有一个是正确/有效的。
Foo.objects.aggregate(total=models.Sum(Cast('cart__total_price', output_field=models.IntegerField())))
# Error
# django.db.utils.DataError: cannot cast jsonb object to type integer
Foo.objects.aggregate(total=models.Sum('cart__total_price', output_field=models.IntegerField()))
# Error
# django.db.utils.ProgrammingError: function sum(jsonb) does not exist
# LINE 1: SELECT SUM("core_foo"."cart") AS "total" FROM "core_foo"
^
# HINT: No function matches the given name and argument types. You might need to add explicit type casts.
获取 JSONField 的顶级 JSON 键总和的正确/最佳方法是什么?
jsonb_extract_path_text(...)
创建了一个自定义数据库函数
from django.db.models import Aggregate
class JSONBExtractPathText(Aggregate):
function = 'jsonb_extract_path_text'
template = None
def get_template(self):
# https://stackoverflow.com/a/38985104/8283848
paths = str(self.extra['path'])[1:-1]
return f"%(function)s(%(jsonb_field)s, {paths})"
def as_sql(self, *args, **kwargs):
kwargs["template"] = self.get_template()
return super().as_sql(*args, **kwargs)
并用作
from django.db.models.functions import Cast
from django.db.models import Sum, IntegerField
from .models import Foo
result = Foo.objects.aggregate(
total_price=Sum(
Cast(
JSONBExtractPathText(
path=["nested", "price"],
jsonb_field="cart"),
output_field=IntegerField(),
)
)
)
print(result)
# {'total_price': 100}
你可以使用 KeyTextTransform 和聚合函数来实现这个。
from django.db.models import Sum, IntegerField
from django.db.models.fields.json import KeyTextTransform
from django.db.models.functions import Cast
# Sum of total_price from all Foo instances
total_price_sum = Foo.objects.annotate(
total_price_int=KeyTextTransform('total_price', 'cart')
).aggregate(
total=Sum(Cast('total_price_int', output_field=IntegerField()))
)
print(total_price_sum['total'])
使用 KeyTextTransform 从 'cart' JSON 字段中提取 'total_price' 键的值。
然后,使用 Sum 函数和 Cast 函数聚合提取值的总和,将值转换为整数。