Фильтруйте набор запросов в зависимости от состояния на заданную дату

При следующей модели (с использованием 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'))
Вернуться на верх