Сложный запрос с использованием Django QuerySets - PullRequest
1 голос
/ 12 апреля 2019

Я работаю над личным проектом и пытаюсь написать сложный запрос, который:

  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, чтобы найти способ реализовать этот запрос с помощью API QuerySet, но я не могу понять это ...

Самое близкое, что мне удалось получить, это:

>>> 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 ​​(), я начинаю получать ранее записанные значения датчиков

Если кто-то может показать мне, как это сделать, или хотя бы сказать, что я делаю неправильно, я буду очень признателен!

Спасибо!

P.S. Пожалуйста, извините мои грамматические и орфографические ошибки, я пишу это среди ночи, и я устала.

EDIT: Исходя из ответов, я должен уточнить: Я хочу только последнее значение датчика для каждого датчика. Например у меня в сенсордате:

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

Ответы [ 4 ]

0 голосов
/ 12 апреля 2019

На самом деле ваш запрос довольно прост, единственная сложная часть - установить, какой SensorData является самым последним для каждого Sensor. Я бы использовал аннотации и Существующий подзапрос следующим образом

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 и доступ к Sensor и Device должен осуществляться через поля. К сожалению, Django не допускает явных объединений через ORM. Поэтому я предлагаю следующее (и позвольте мне сказать, что это далеко от идеала с точки зрения производительности).

Идея состоит в том, чтобы аннотировать набор запросов Sensor конкретными значениями последних SensorData (значение и метка времени), если таковые существуют, следующим образом:

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_value и latest_value_timestamp будут просто установлены на None.

0 голосов
/ 12 апреля 2019

Как то так?:

Несколько устройств на 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")

Этот Queryset будет:

1.Получает каждое устройство, принадлежащее определенному пользователю

2.Получает каждый датчик, принадлежащий каждому из устройств пользователя (но он возвращает датчик_тип каждого датчика, потому что там нет поля имени, поэтому я возвращаю имя_сенсора)

3.Получает все записанные (упорядоченные по последней метке времени) значения и метки времени для каждого из датчиков устройств пользователя.

UPDATE

попробуйте это:

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)
0 голосов
/ 12 апреля 2019

Совершенно нормально выполнять необработанные запросы с помощью 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 не проверяет запрос. Это означает, что безопасность запроса - это ваша полная ответственность, очистите параметры.

0 голосов
/ 12 апреля 2019

Для такого рода запросов я настоятельно рекомендую использовать объекты Q, здесь документы https://docs.djangoproject.com/en/2.2/topics/db/queries/#complex-lookups-with-q-objects

...