Как реализовать `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, хотя.