Как реализовать `left outer join` с дополнительным условием соответствия, с помощью `annotate()` или как-то еще?

Для сценария транзакции (сущности) с настраиваемыми атрибутами в формате EAV мы реализуем шаблон проектирования, который собирает данные EAV с сущностями путем серии left outer join действий в SQL запросе, вкратце это выглядит следующим образом:

  • Сначала мы извлекли метаданные, например, postalcode соответствует attribute_id из 22, phone соответствует 23 и т.д.
  • Затем мы хотим построить QuerySet, добавив вызовы методов annotate(), следующие за метаданными. И мы открыты для других методов, кроме annotate().
  • Подобно приведенному ниже SQL-запросу, поведение системы заключается в повторении left outer join на той же eav_value таблице, однако, помимо внешнего ключа, условие сопоставления также требует определенного attribute_id. Таким образом, каждое соединение собирает один атрибут.

Наш вопрос:

Мы попытались собрать первый атрибут, добавив annotate() с filter к существующему QuerySet, как:

transaction.annotate(
  postalcode=F('eav_values__value_text'), 
  filter=Q(eav_values__attribute_id__exact=22)
)

Тест получил ошибку, говорящую AttributeError: 'WhereNode' object has no attribute 'select_format'. Мы считаем, что причина в части filter, потому что если мы удалим аргумент, ошибка исчезнет.

Итак, нам интересно, как исправить проблему и заставить прототип работать. И мы также не против использовать что-то еще, кроме annotate() в рамках Django framework, а не сырой запрос.

Мы новички в этой области Django ORM, поэтому будем признательны за любые подсказки и предложения.

Технические детали:

1. SQL запрос для сборки данных EAV с сущностями:

select t.`id`, t.`create_ts`
  , `eav_postalcode`.`value_text` as `postalcode`
  , `eav_phone`.`value_text` as `phone`
  -- , ... to assemble more attributes
from
    (
      select * from `ebackendapp_transaction` where (`product_id` = __PRODUCT_ID__)
      order by `id` desc
      limit 2 offset 27000
    )  as t
    left outer join `eav_value` as `eav_postalcode` 
        on t.`id` = `eav_postalcode`.`entity_id` and `eav_postalcode`.`attribute_id` = 22
    left outer join `eav_value` as `eav_phone` 
        on t.`id` = `eav_phone`.`entity_id` and `eav_phone`.`attribute_id` = 23
    -- ... to assemble more attributes
;

2. Этапы тестирования:

transaction = Transaction.active_objects.filter(product_id=__PRODUCT_ID__).order_by('-id').all()[27000:27002].values('id', 'create_ts')
print(transaction)
# OK

transaction_eav = transaction.annotate(postalcode=F('eav_values__value_text'), filter=Q(eav_values__attribute_id__exact=22))
# transaction_eav is OK, however:
print(transaction_eav)
# got an error saying "AttributeError: 'WhereNode' object has no attribute 'select_format'"

3. Определения модели:

class Transaction(models. Model):
    transaction_id = models.CharField(max_length=100)
    product = models.ForeignKey(Product)
    ...

# From the open source Django EAV library
# imported as `eav_models`
# 
class Value(models. Model):
    '''
    Putting the **V** in *EAV*.
    ...
    '''
    ...
    entity_id = models.IntegerField()
    entity = GenericForeignKey(ct_field='entity_ct',
                                       fk_field='entity_id')

    value_text = models.TextField(blank=True, null=True)
    value_float = models.FloatField(blank=True, null=True)
    value_int = models.IntegerField(blank=True, null=True)
    value_date = models.DateTimeField(blank=True, null=True)
    value_basicdate = models.DateField(blank=True, null=True)
    value_bool = models.NullBooleanField(blank=True, null=True)
    ...
    attribute = models.ForeignKey(Attribute, db_index=True,
                                  verbose_name=_(u"attribute"),
                                  on_delete=models.DO_NOTHING)
    ...
    

Предварительно мы сделали рабочий прототип с помощью FilteredRelation(), подробнее см. в этом посте.

Ссылка на документ нацелена на Django v4.1, а мы в настоящее время на v3.1, хотя.

Вернуться на верх