Аннотирование на основе связанной области, с фильтрами
class IncomeStream(models.Model):
product = models.ForeignKey(Product, related_name="income_streams")
from_date = models.DateTimeField(blank=True, null=True)
to_date = models.DateTimeField(blank=True, null=True)
value = MoneyField(max_digits=14, decimal_places=2, default_currency='USD')
class Product(models.Model):
...
class Sale(models.Model):
product = models.ForeignKey(Product, related_name="sales")
created_at = models.DateTimeField(auto_now_add=True)
...
Предположим, что я хочу добавить значение к некоторым Sale
, используя .annotate
.
Это значение называется cpa
(cost per action): cpa - это значение IncomeStream
, чьи from_date
и to_date
включают Sale created_at
в свой диапазон.
Кроме того, from_date и to_date являются nullable, и в этом случае мы предполагаем, что они означают бесконечность.
Например:
<IncomeStream: from 2021-10-10 to NULL, value 10$, product TEST>
<IncomeStream: from NULL to 2021-10-09, value 5$, product TEST>
<Sale: product TEST, created_at 2019-01-01, [cpa should be 5$]>
<Sale: product TEST, created_at 2021-11-01, [cpa should be 10$]>
Мой вопрос: можно ли написать все эти условия, используя только Django ORM и аннотацию? Если да, то как?
Я знаю, что объекты F могут обходить отношения следующим образом:
Sale.objects.annotate(cpa=F('product__income_streams__value'))
Но тогда где именно я могу написать всю логику, чтобы определить, из какого именно income_stream
он должен выбрать value
?
Предположим, что ни один поток доходов не имеет пересекающихся дат для одного и того же продукта, поэтому вышеупомянутые спецификации никогда не приводят к конфликтам.
Что-то вроде этого должно помочь вам начать
subquery = (
IncomeStream
.objects
.values('product') # group by product primary key i.e. product_id
.filter(product=OuterRef('product'))
.filter(from_date__gte=OuterRef('created_at'))
.filter(to_date__lte=OuterRef('created_at'))
.annotate(total_value=Sum('value'))
)
Затем с помощью подзапроса
Sale
.objects
.annotate(
cpa=Subquery(
subquery
).values('total_value') # subquery should return only one row so
# so just need the total_value column
)
Не имея возможности самому поиграть с этим в оболочке, я не уверен на 100%. В любом случае, это должно быть близко.