Фильтруйте набор запросов в зависимости от состояния на заданную дату
При следующей модели (с использованием django-simple-history
):
class MyModel (models.Model):
status = models.IntegerField()
history = HistoricalRecords()
Я хотел бы получить все экземпляры, которые не имели определенного status
на заданную дату (т.е. все экземпляры, которые имели другой статус на дату ограничения, плюс все экземпляры, которые не существовали в это время).
Следующий запрос вернет все экземпляры, которые никогда не имели status = 4
в любой момент до даты ограничения:
MyModel.filter (~Exists (
MyModel.history.filter (
id = OuterRef ("id"),
history_date__lte = limit_date,
status = 4))
Но, к сожалению, он также удаляет экземпляры, которые имели status = 4
на какую-то прошлую дату, а затем изменились на другие status
к предельной дате, а я хочу их сохранить.
Следующее должно дать правильный результат:
MyModel.filter (~Exists (
MyModel.history.filter (
id = OuterRef ("id"),
history_date__lte = limit_date)
.order_by ("-history_date")
[:1]
.filter (status = 4)))
К сожалению, это не работает: Cannot filter a query once a slice has been taken.
Этот вопрос ссылается на эту страницу документации, которая объясняет, что фильтрация не допускается после нарезки набора запросов.
Обратите внимание, что ошибка возникает из-за assert
в Django. Если я закомментирую assert
в django/db/models/query.py:953
, то код заработает и даст ожидаемый результат. Однако закомментировать assert
в зависимости от восходящего потока - не лучшее решение в производстве.
Так есть ли чистый способ отфильтровать мой набор запросов в зависимости от некоторого прошлого состояния объекта?
Модель истории сохраняет запись только тогда, когда элемент изменяется, а не каждый день. Таким образом, мы можем получить статус на определенную дату с помощью:
from django.db.models import OuterRef, Q, Subquery
MyModel.annotate(
historic_status=Subquery(
MyModel.history.filter(id=OuterRef('id'), history_date__lte=limit_date)
.order_by('-history_date')
.values('status')[:1]
)
).filter(~Q(history_status=4) | Q(history_status=None))
Таким образом, сначала мы ищем status
исторической модели с датой раньше или равной limit_date
. Упорядочив их так, чтобы первым был самый последний history_date
, мы получим самый последний статус.
Таким образом, historic_status
установится status
на момент limit_date
, или , если запись не существует на тот момент, NULL
(None
).
Таким образом, мы можем отфильтровать MyModel
, у которых таким образом history_status
не четыре (и мы добавили проверку NULL
явно), хотя обычно должно быть достаточно следующего:
from django.db.models import OuterRef, Q, Subquery
MyModel.annotate(
historic_status=Subquery(
MyModel.history.filter(id=OuterRef('id'), history_date__lte=limit_date)
.order_by('-history_date')
.values('status')[:1]
)
).filter(~Q(history_status=4))
@willeM_ Van Onsem's answer works but it executes the subquery twice: once to create the historic_status
column, which I don't need, and once in the WHERE
clause. It pointed me in a direction that only runs the subquery once, but requires hand-writing some SQL:
MyModel.extra (where = [ limit_date.strftime (
'(SELECT U0."status" FROM "myapp_historicalmymodel" U0'
' WHERE (U0."history_date" <= "%Y-%m-%d %H:%M:%S" '
' AND U0."id" = "myapp_mymoedel"."id")'
' ORDER BY U0."history_date" DESC'
' LIMIT 1)'
'!= 4'))