Django ORM如何获取按字段分组的原始值

Posted

技术标签:

【中文标题】Django ORM如何获取按字段分组的原始值【英文标题】:Django ORM how to get raw values grouped by a field 【发布时间】:2020-10-27 03:58:15 【问题描述】:

我有一个这样的模型:

class CPUReading(models.Model):
    host = models.CharField(max_length=256)
    reading = models.IntegerField()
    created = models.DateTimeField(auto_now_add=True)

我正在尝试获得如下所示的结果:


    "host 1": [
        
            "created": DateTimeField(...),
            "value": 20
        ,
        
            "created": DateTimeField(...),
            "value": 40
        ,
        ... 
    ],
    "host 2": [
        
            "created": DateTimeField(...),
            "value": 19
        ,
        
            "created": DateTimeField(...),
            "value": 10
        ,
        ... 
    ]

我需要按主机分组并按created 排序。

我尝试了很多东西,包括使用values()annotate() 来创建GROUP BY 语句,但我认为我必须遗漏一些东西,因为为了使用GROUP BY,我似乎需要使用一些我不想做的聚合函数。我需要reading 字段的实际值,该字段按主机字段分组并按创建的字段排序。

这或多或少是任何图表库需要数据的方式。

我知道我可以使用 python 代码或原始 sql 查询来实现它,但我更喜欢使用 django ORM,除非它明确禁止这种查询。

【问题讨论】:

在搞砸了一点之后,我认为 ORM 中没有任何内容,如果您希望在数据库中完成此操作,则需要使用原始查询。我认为如果你的数据库被规范化,你可能会更轻松,即你有一个Host 模型。 我已经基于此添加了一个答案,以防它对您有用,但如果它不起作用,它可能对其他人有用。 使用itertools.groupby and operator.itemgetter 您能展示一下您认为会创建此数据结构的 SQL 查询吗? GROUP BY 只会为每个结果行返回一行读数... 【参考方案1】:

据我所知,ORM 中没有任何东西可以让这变得简单。如果您想在没有原始查询的情况下在 ORM 中执行此操作,并且如果您愿意并且能够更改数据结构,则可以主要在 ORM 中解决此问题,并将 Python 代码保持在最低限度:

class Host(models.Model):
    pass

class CPUReading(models.Model):
    host = models.ForeignKey(Host, related_name="readings", on_delete=models.CASCADE)
    reading = models.IntegerField()
    created = models.DateTimeField(auto_now_add=True)

有了这个,你可以使用两个查询代码相当干净:

from collections import defaultdict

results = defaultdict(list)
hosts = Host.objects.prefetch_related("readings")
for host in hosts:
    for reading in host.readings.all():
        results[host.id].append(
            "created": reading.created, "value": reading.reading
        )

或者你可以用一个查询和一个循环更有效地做到这一点:

from collections import defaultdict

results = defaultdict(list)
readings = CPUReading.objects.select_related("host")
for reading in readings:
    results[reading.host.id].append(
        "created": reading.created, "value": reading.reading
    )

【讨论】:

这可能是我能得到的最好答案。我真的对 django orm 的这一方面感到沮丧。除非有人在第二天左右使用数据库给我答案,否则我会给你赏金。【参考方案2】:

假设您使用的是 PostgreSQL,您可以结合使用 array_aggjson_object 来实现您所追求的目标。

from django.contrib.postgres.aggregation import ArrayAgg
from django.contrib.postgres.fields import ArrayField, JSONField
from django.db.models import CharField
from django.db.models.expressions import Func, Value

class JSONObject(Func):
    function = 'json_object'
    output_field = JSONField()

    def __init__(self, **fields):
        fields, expressions = zip(*fields.items())
        super().__init__(
            Value(fields, output_field=ArrayField(CharField())),
            Func(*expressions, template='array[%(expressions)s]'),
        )

readings = dict(CPUReading.objects.values_list(
    'host',
    ArrayAgg(
        JSONObject(
            created_at='created_at',
            value='value',
        ),
        ordering='created_at',
    ),      
))

【讨论】:

【参考方案3】:

如果您想与 Django ORM 保持密切联系,您只需要记住它不会返回查询集而是字典,并且会动态评估,因此不要在声明性范围内使用它。不过接口和QuerySet.values()类似,还有一个额外的要求,就是需要先排序。

class PlotQuerySet(models.QuerySet):
    def grouped_values(self, key_field, *fields, **expressions):
        if key_field not in fields:
            fields += (key_field,)
        values = self.values(*fields, **expressions)
        data = 
        for key, gen in itertools.groupby(values, lambda x: x.pop(key_field)):
            data[key] = list(gen)

        return data


PlotManager = models.Manager.from_queryset(PlotQuerySet, class_name='PlotManager')

class CpuReading(models.Model):
    host = models.CharField(max_length=255)
    reading = models.IntegerField()
    created_at = models.DateTimeField(auto_now_add=True)
    objects = PlotManager()

例子:

CpuReading.objects.order_by(
    'host', 'created_at'
).grouped_values(
    'host', 'created_at', 'reading'
)                                                                                                  
Out[10]: 
'a': ['created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 215005, tzinfo=<UTC>),
   'reading': 0,
  'created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 223080, tzinfo=<UTC>),
   'reading': 1,
  'created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 230218, tzinfo=<UTC>),
   'reading': 2,
  ...],
 'b': ['created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 241476, tzinfo=<UTC>),
   'reading': 0,
  'created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 242015, tzinfo=<UTC>),
   'reading': 1,
  'created_at': datetime.datetime(2020, 7, 13, 16, 45, 23, 242537, tzinfo=<UTC>),
   'reading': 2,
   ...]

【讨论】:

以上是关于Django ORM如何获取按字段分组的原始值的主要内容,如果未能解决你的问题,请参考以下文章

在 Django ORM 中按查询分组

Django ORM命令获取没有空字段的不同值

如何使用 Django ORM 按周分组

相当于按年分组的Django ORM查询集?

django之路由分组,路由分发,FBV,CBV,ORM框架

如何按非字段值过滤 Django 查询集