Почему фильтры влияют на результат StringAgg?
Я использую StringAgg
и order
следующим образом:
# Get order column & annotate with list of credits
if request.POST.get('order[0][name]'):
order = request.POST['order[0][name]']
if order == 'credits_primary':
releases = releases.annotate(credits_primary=StringAgg(
'credits__entity__name',
delimiter=', ',
filter=Q(credits__type='primary'),
ordering='credits__id'
))
elif order == 'credits_secondary':
releases = releases.annotate(credits_secondary=StringAgg(
'credits__entity__name',
delimiter=', ',
filter=Q(credits__type='secondary'),
ordering='credits__id'
))
else:
order = 'title'
# Order releases
if request.POST.get('order[0][dir]') == 'desc':
releases = releases.order_by(F(order).desc(nulls_last=True), 'title')
else:
releases = releases.order_by(F(order).asc(nulls_last=True), 'title')
for release in releases:
try: print(release.credits_primary)
except: pass
try: print(release.credits_secondary)
except: pass
Это само по себе работает именно так, как ожидалось: упорядочивание соответствует моим ожиданиям, и print
возвращает ожидаемые значения.
перед этим filter
, он начинает вести себя странно. А именно: иногда все в порядке и все работает, как ожидалось, иногда каждый credits__entity__name
повторяется случайное число раз, иногда аннотация просто возвращает None
, хотя значения есть. Я не могу понять, в чем здесь закономерность.
Ниже приведены фильтры, которые я применяю, обратите внимание, что exclude
, насколько я могу судить, не вызывает этой проблемы:
if request.POST.get('entity'):
releases = Release.objects.filter(credits__entity=request.POST['entity'])
else:
releases = Release.objects.all()
records_total = releases.count()
# Filter by type
if request.POST.get('type'):
query = Q()
for type in loads(request.POST['type']):
if type in Release.TYPES_P_TO_S:
query.add(Q(type=Release.TYPES_P_TO_S[type]), Q.OR)
releases = releases.filter(query)
# Filter by platform
if request.POST.get('platform'): releases = releases.filter(platforms__in=loads(request.POST['platform']))
# Filter by format
if request.POST.get('format'): releases = releases.filter(formats__in=loads(request.POST['format']))
# Filter by From/Until year
if request.POST.get('from'): releases = releases.filter(date__year__gte=loads(request.POST['from'])[0])
if request.POST.get('until'): releases = releases.filter(date__year__lte=loads(request.POST['until'])[0])
# Exclude parent types
if request.POST.get('exclude'):
query = Q()
for type in loads(request.POST['exclude']):
if type in Release.PARENT_TYPES_P_TO_S:
query.add(Q(parent_type=Release.PARENT_TYPES_P_TO_S[type]), Q.OR)
releases = releases.exclude(query)
# Filter by Search
if request.POST.get('search[value]'):
query = Q()
search = request.POST['search[value]']
query.add(Q(title__icontains=search), Q.OR)
query.add(Q(tags__tag__name__icontains=search), Q.OR)
query.add(Q(credits__entity__name__icontains=search), Q.OR)
releases = releases.filter(query)
# Make sure items aren't repeated
releases = releases.distinct()
Не уверен, что я понимаю ваш вариант использования, но если проблема в том, что вы получаете дубликаты в своей агрегации, возможно, вы можете попробовать использовать distinct
в своих агрегациях строк:
https://docs.djangoproject.com/fr/5.1/ref/contrib/postgres/aggregates/#stringagg
Ниже приведен применяемый мной фильтр, который влияет на агрегацию (применяется перед StringAgg):
Фильтр рассматривает (другие) отношения "один ко многим" или "многие ко многим". Это означает, что результатом будет LEFT OUTER JOIN
, который начнет действовать как множитель. То же самое работает с другими агрегатами, такими как SUM
и COUNT
.
Действительно, если вы просто агрегируете данные, запрос выглядит следующим образом:
SELECT string_agg(CASE
WHEN c.type = 'primary' THEN e.name
ELSE NULL
END
ORDER BY e.id)
FROM releases AS r
LEFT OUTER JOIN credits AS c ON c.release_id = r.id
LEFT OUTER JOIN entity AS e ON e.credit_id = e.id
и это нормально, потому что мы хотим агрегировать данные по сущностям, поэтому в каждом выпуске мы получаем все сущности.
Но теперь, если мы, например, отфильтруем по тегам, это будет выглядеть так:
SELECT string_agg(CASE
WHEN c.type = 'primary' THEN e.name
ELSE NULL
END
ORDER BY e.id)
FROM releases AS r
LEFT OUTER JOIN credits AS c ON c.release_id = r.id
LEFT OUTER JOIN entity AS e ON e.credit_id = e.id
LEFT OUTER JOIN release_tag rt1 ON rt1.release_id = r.id
LEFT OUTER JOIN tag t1 ON rt1.tag_id = t1.id
LEFT OUTER JOIN release_tag rt2 ON rt2.release_id = r.id
LEFT OUTER JOIN tag t1 ON rt2.tag_id = t2.id
WHERE t1.name = 'tag1'
OR t2.name = 'tag2'
Это означает, что имена сущностей для выпуска, в котором совпадает один из двух тегов, будут повторены один раз, но в случае совпадения обоих тегов они будут включены дважды.
Q(credits__entity__name__icontains)
делает ситуацию еще хуже: поскольку это приведет к объединению в таблице c
, и, следовательно, объекты из credits
, которые имеют два или более совпадений, будут включены так много раз, тогда как объекты выпуска, которые не соответствуют заданному кредиту, больше не будут включены.
Наименьшим общим признаком всех этих явлений, вероятно, является то, что вы должны быть очень осторожны при составлении агрегатов при создании JOIN
, особенно если несколько таблиц, в которых выполняются объединения. в этом замешаны наши родственники.
Что вы, вероятно, можете сделать, так это создать Subquery
выражение [Django-doc] на который, таким образом, не влияют "внешние" таблицы и соответствующие JOIN
.