Помощь с RAW SQL запросом для сущностей amoCRM в Django проекте
Есть три базовых сущности в БД: Lead, LeadEvent и LeadStatus.
Lead - это сделка, LeadEvent - событие связанное с одной сделкой, LeadStatus - статус события.
На каждую сделку приходится множество событий - пересечений статуса.
Модели Django выглядят следующим образом:
class LeadStatus(models.Model):
status_id = models.PositiveIntegerField(verbose_name='ID статуса', unique=True)
name = models.CharField(max_length=100, verbose_name='Имя', null=True, default='Неизвестный')
class Lead(models.Model):
lead_id = models.PositiveIntegerField(verbose_name="ID сделки", null=True, unique=True)
created_date_time = models.DateTimeField(verbose_name="Время и дата создания сделки", null=True)
price = models.IntegerField(verbose_name='Бюджет сделки')
current_status = models.ForeignKey(LeadStatus, on_delete=models.SET_NULL, verbose_name='Текущий статус', null=True)
class LeadEvent(models.Model):
note_id = models.CharField(verbose_name="ID примечания", null=True, unique=True, max_length=64)
lead = models.ForeignKey(Lead, on_delete=models.CASCADE)
old_status = models.ForeignKey(LeadStatus, related_name='old_status', null=True, blank=True,
on_delete=models.CASCADE)
new_status = models.ForeignKey(LeadStatus, null=True, related_name='new_status', blank=True,
on_delete=models.CASCADE)
crossing_status_date_time = models.DateTimeField(verbose_name="Время и дата пересечения статуса", null=True)
Имеются 2 основных интересующих группы сделок: сделки-прибыль(is_profitt) и сделки-потери(is_loss) для которых свои статусы пересечения ( например 142 и 143 соответственно)
Задача звучит так: найти для каждой сделки (Lead) 1 событие пересечения ( LeadEvent ), которое удовлетворяет условию:
1) Пересечение должно быть строго по определенным статусам (псевдо-код запроса: old_status__id__not_in=[statuses_list] and new_status__id__in=[statuses_list] )
2) Пересечение если имеется, должно быть самым первым по дате пересечения (crossing_status_date_time) для данной сделки ( Lead )
И еще одна дополнительная подгруппа: сделки которые сразу в статусе (is_in_status):
3) У такой сделки (Lead) у самого первого по дате пересечения статуса (LeadEvent) старый статус является подходящим (old_status__id__in=[statuses_list])
Эти группы должны формироваться одним (под) запросом. На языке ORM Django эта конструкция выглядит следующим образом:
class LeadSubqueryExpression:
"""
Класс для формирования подзапросов к лидам
Для выборки пересечений по статусам
"""
def __init__(self, filter_query: Q):
self.filter_query = filter_query
def get_expression(self):
""" Получение выражения запроса на основе фильтра """
# на одну аннотацию Django - один обьект
# в данном случае типа json
json_notes = JsonBuildObject(
Value('created_date_time'), F('crossing_status_date_time'),
Value('status_name'), Concat(F('old_status__name'),
Value(' -> '),
F('new_status__name'),
output_field=CharField()),
Value('status_color'), Concat(F('old_status__color'),
Value(' -> '),
F('new_status__color'),
output_field=CharField())
)
subquery = (
LeadsNote
.objects
.select_related('old_status', 'new_status', 'company')
.filter(self.filter_query, lead=OuterRef('pk'))
.order_by('lead', 'crossing_status_date_time')
.annotate(status_notes=json_notes)
.distinct('lead')
.values('status_notes')
)
return subquery
class LeadReportQuerySet(models.QuerySet):
""" Последовательность данных по лидам """
def annotate_for_report(
self,
loss_filter: Q,
profit_filter: Q,
in_status_filter: Q
):
""" Аннотация последовательности """
# включаем в аннотацию подзапросы по пересечениям статусов
# для дохода, потери и сделок сразу в статусе
# с целью последующей фильтрации
profit_subquery = LeadSubqueryExpression(filter_query=profit_filter).get_expression()
loss_subquery = LeadSubqueryExpression(filter_query=loss_filter).get_expression()
in_status_subquery = LeadSubqueryExpression(filter_query=in_status_filter).get_expression()
first_date_subquery = (
LeadsNote
.objects
.filter(lead=OuterRef('pk'))
.order_by('crossing_status_date_time')
.values('crossing_status_date_time')[:1]
)
return (self
.annotate(
is_in_status=Exists(in_status_subquery),
is_loss=Exists(loss_subquery),
is_profit=Exists(profit_subquery))
.annotate(
status_notes=Case(
When(is_profit=True, then=profit_subquery),
When(is_loss=True, then=loss_subquery),
When(Q(is_profit=False) & Q(is_loss=False) & Q(is_in_status=True), then=in_status_subquery),
output_field=JSONField()
))
.annotate(created_date_time=Cast(KeyTextTransform('created_date_time', 'status_notes'),
output_field=DateTimeField()),
status_name=Cast(KeyTextTransform('status_name', 'status_notes'),
output_field=CharField()),
status_color=Cast(KeyTextTransform('status_color', 'status_notes'),
output_field=CharField()),
)
.annotate(
is_profit=Case(
When(Q(is_profit=False) & Q(is_loss=False) & Q(is_in_status=True), then=Value(True)),
default=F('is_profit'), output_field=BooleanField()
)))
Запрос отрабатывает, но делает это медленно. Нужна помощь в переводе запроса на чистый SQL.