【问题标题】:Complex query using Django QuerySets使用 Django QuerySets 的复杂查询
【发布时间】:2019-09-02 17:20:35
【问题描述】:

我正在处理一个个人项目,我正在尝试编写一个复杂的查询:

  1. 获取属于某个用户的每一台设备

  2. 获取属于每个用户设备的每个传感器

  3. 获取每个用户设备传感器的最后记录值和时间戳。

我正在使用 Sqlite,并且我设法将查询编写为普通 SQL,但是,对于我的一生,我无法想出在 django 中执行此操作的方法。我查看了其他问题,尝试浏览文档,但无济于事。

我的模型:

class User(AbstractBaseUser):
    email = models.EmailField()

class Device(models.Model):
    user = models.ForeignKey(User)
    name = models.CharField()

class Unit(models.Model):
    name = models.CharField()

class SensorType(models.Model):
    name = models.CharField()
    unit = models.ForeignKey(Unit)

class Sensor(models.Model):
    gpio_port = models.IntegerField()
    device = models.ForeignKey(Device)
    sensor_type = models.ForeignKey(SensorType)

class SensorData(models.Model):
    sensor = models.ForeignKey(Sensor)
    value = models.FloatField()
    timestamp = models.DateTimeField()

这里是 SQL 查询:

SELECT acc.email, 
           dev.name as device_name, 
           stype.name as sensor_type,
           sen.gpio_port as sensor_port,
           sdata.value as sensor_latest_value, 
           unit.name as sensor_units, 
           sdata.latest as value_received_on
FROM devices_device as dev
INNER JOIN accounts_user  as acc on dev.user_id = acc.id
INNER JOIN devices_sensor  as sen on sen.device_id = dev.id
INNER JOIN devices_sensortype as stype on stype.id = sen.sensor_type_id
INNER JOIN devices_unit as unit on unit.id = stype.unit_id
LEFT JOIN (
            SELECT MAX(sd.timestamp) latest, sd.value, sensor_id
            FROM devices_sensordata as sd
            INNER JOIN devices_sensor as s ON s.id = sd.sensor_id
        GROUP BY sd.sensor_id) as sdata on sdata.sensor_id= sen.id
WHERE acc.id = 1
ORDER BY dev.id

我一直在使用 django shell 以找到一种使用 QuerySet API 实现此查询的方法,但我无法弄清楚...

我设法得到的最接近的是:

>>> sub = SensorData.objects.values('sensor_id', 'value').filter(sensor_id=OuterRef('pk')).order_by('-timestamp')[:1]
>>> Sensor.objects.annotate(data_id=Subquery(sub.values('sensor_id'))).filter(id=F('data_id')).values(...)

但是它有两个问题:

  1. 不包括在 SensorsData 中尚无任何值的传感器
  2. 如果我将 SensorData.values 字段包含到 .values() 中,我将开始获取之前记录的传感器值

如果有人能告诉我怎么做,或者至少告诉我我做错了什么,我将非常感激!

谢谢!

附:请原谅我的语法和拼写错误,我在半夜写这个,我很累。

编辑: 根据答案,我应该澄清: 我只想要每个传感器的最新传感器值。例如我在 sensordata 中有:

id | sensor_id | value | timestamp|
1  |  1             |  2       |  <today>   |
2  |  1             |  5       | <yesterday>|
3  |  2             |  3       | <yesterday>|

每个 sensor_id 只应返回最新的:

id |   sensor_id    |   value  |  timestamp |
1  |  1             |  2       |  <today>   |
3  |  2             |  3       | <yesterday>|

或者,如果传感器在此表中还没有任何数据,我希望查询返回一条记录,其中值和时间戳为“null”(基本上是我的 SQL 查询中的左连接)。

EDIT2:

根据@ivissani 的回答,我设法制作了这个:

>>> latest_sensor_data = Sensor.objects.annotate(is_latest=~Exists(SensorData.objects.filter(sensor=OuterRef('id'),timestamp__gt=OuterRef('sensordata__timestamp')))).filter(is_latest=True)
>>> user_devices = latest_sensor_data.filter(device__user=1)
>>> for x in user_devices.values_list('device__name','sensor_type__name', 'gpio_port','sensordata__value', 'sensor_type__unit__name', 'sensordata__timestamp').order_by('device__name'):
...     print(x)

这似乎可以完成这项工作。

这是它产生的 SQL:

    SELECT
  "devices_device"."name",
  "devices_sensortype"."name",
  "devices_sensor"."gpio_port",
  "devices_sensordata"."value",
  "devices_unit"."name",
  "devices_sensordata"."timestamp"
FROM
  "devices_sensor"
  LEFT OUTER JOIN "devices_sensordata" ON (
    "devices_sensor"."id" = "devices_sensordata"."sensor_id"
  )
  INNER JOIN "devices_device" ON (
    "devices_sensor"."device_id" = "devices_device"."id"
  )
  INNER JOIN "devices_sensortype" ON (
    "devices_sensor"."sensor_type_id" = "devices_sensortype"."id"
  )
  INNER JOIN "devices_unit" ON (
    "devices_sensortype"."unit_id" = "devices_unit"."id"
  )
WHERE
  (
    NOT EXISTS(
      SELECT
        U0."id",
        U0."sensor_id",
        U0."value",
        U0."timestamp"
      FROM
        "devices_sensordata" U0
      WHERE
        (
          U0."sensor_id" = ("devices_sensor"."id")
          AND U0."timestamp" > ("devices_sensordata"."timestamp")
        )
    ) = True
    AND "devices_device"."user_id" = 1
  )
ORDER BY
  "devices_device"."name" ASC

【问题讨论】:

  • 这篇文章也有帮助 stackoverflow.com/questions/48128714/…
  • 你想对数据做什么?您需要将它们放在平面表中(想想 Excel)还是作为嵌套对象(在树状结构中)?
  • 两者都应该没问题。我将在视图中使用它们并将它们显示给用户。哪个更健壮。

标签: python django sqlite django-queryset


【解决方案1】:

实际上,您的查询相当简单,唯一复杂的部分是确定每个Sensor 中哪个SensorData 是最新的。我会通过以下方式使用annotationsExists subquery

latest_data = SensorData.objects.annotate(
    is_latest=~Exists(
        SensorData.objects.filter(sensor=OuterRef('sensor'),
                                  timestamp__gt=OuterRef('timestamp'))
    )
).filter(is_latest=True)

那么只需按以下方式按用户过滤此查询集:

certain_user_latest_data = latest_data.filter(sensor__device__user=certain_user)

现在,即使传感器没有任何数据,您也想检索传感器,因此仅检索 SensorData 实例并且必须通过字段访问 SensorDevice 是不够的。不幸的是,Django 不允许通过其 ORM 进行显式连接。因此,我提出以下建议(让我说,从性能的角度来看,这远非理想)。

这个想法是用最新的SensorData(值和时间戳)的具体值来注释Sensors查询集,如果有的话,以下列方式存在:

latest_data = SensorData.objects.annotate(
    is_latest=~Exists(
        SensorData.objects.filter(sensor=OuterRef('sensor'),
                                  timestamp__gt=OuterRef('timestamp'))
    )
).filter(is_latest=True, sensor=OuterRef('pk'))

sensors_with_value = Sensor.objects.annotate(
    latest_value=Subquery(latest_data.values('value')),
    latest_value_timestamp=Subquery(latest_data.values('timestamp'))
)  # This will generate two subqueries...

certain_user_sensors = sensors_with_value.filter(device__user=certain_user).select_related('device__user')

如果某个Sensor 没有SensorData 的任何实例,则注释字段latest_valuelatest_value_timestamp 将简单地设置为None

【讨论】:

  • 好的,所以我错过了你想要获取传感器,即使它们还没有数据,让我详细说明一下
  • 你先生,是摇滚明星!谢谢!您的代码有效,除了没有数据的传感器,但我想我设法修改了您的答案来解决这个问题。请查看我的问题的编辑。
  • 哦,我刚刚看到你发布了一个阐述。我想我设法找到了另一种方法,我在我的问题上将其写为 edit2 。我不使用子查询,你怎么看?
  • 不太确定您的第二次编辑是否符合您的要求...因为您没有参考实际最新的SensorData 实例。你改变你的模型了吗?因为我看到您使用了 Sensor 模型中的 sensordata 扩展,但我看不到它指的是什么,因为关系 (ForeignKey) 朝着相反的方向发展......
  • 更正,我很确定您的第二次编辑没有按照您的意愿进行...该查询将返回同一传感器的多条记录,每个 SensorData 实例都有一条记录传感器。不仅是最新的。
【解决方案2】:

对于这种查询,我强烈建议使用 Q 对象,这里是文档https://docs.djangoproject.com/en/2.2/topics/db/queries/#complex-lookups-with-q-objects

【讨论】:

    【解决方案3】:

    使用 django 执行原始查询非常好,尤其是在它们非常复杂的情况下。

    如果要将结果映射到模型,请使用: https://docs.djangoproject.com/en/2.2/topics/db/sql/#performing-raw-queries

    否则,请参阅:https://docs.djangoproject.com/en/2.2/topics/db/sql/#executing-custom-sql-directly

    请注意,在这两种情况下,django 都不会对查询进行检查。 这意味着查询的安全性是您的全部责任,请清理参数。

    【讨论】:

    • 这就是我现在实现的方式,但出于您自己指定的原因,我想避免使用原始查询。
    • 如果 acc.id 是唯一的参数,只需使用 int() 将其转换为整数,这将防止任何 SQL 注入
    【解决方案4】:

    像这样?:

    1 个用户的多个设备

    device_ids = Device.objects.filter(user=user).values_list("id", flat=True)
    SensorData.objects.filter(sensor__device__id__in=device_ids
                              ).values("sensor__device__name", "sensor__sensor_type__name", 
                                       "value","timestamp").order_by("-timestamp")
    

    1 个设备,1 个用户

    SensorData.objects.filter(sensor__device__user=user
                              ).values("sensor__device__name", "sensor__sensor_type__name", 
                                       "value", "timestamp").order_by("-timestamp")
    

    该查询集将:

    1.获取属于某个用户的每一台设备

    2.获取属于每个用户设备的每个传感器(但它返回每个传感器的sensor_type,因为那里没有名称字段所以我返回sensor_type_name)

    3.获取每个用户设备传感器的所有记录(按最新时间戳排序)值和时间戳。

    更新

    试试这个:

    list_data=[]
    for _id in device_ids:
        sensor_data=SensorData.objects.filter(sensor__device__user__id=_id)
        if sensor_data.exists():
            data=sensor_data.values("sensor__id", "value", "timestamp", "sensor__device__user__id").latest("timestamp")
            list_data.append(data)
    

    【讨论】:

    • 这很接近,但是,我只需要每个传感器的最后一个值。请查看我对我的问题的编辑。
    猜你喜欢
    • 2011-06-08
    • 2012-10-25
    • 2021-08-28
    • 2013-06-09
    • 2011-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多