Это не проблема с Django ORM, это просто способ работы реляционных баз данных.Когда вы создаете простые наборы запросов, такие как
Player.objects.annotate(weapon_count=Count('unit_set__weapon_set'))
или
Player.objects.annotate(rarity_sum=Sum('unit_set__rarity'))
ORM делает именно то, что вы ожидаете, - присоединяйтесь Player
с Weapon
SELECT "sandbox_player"."id", "sandbox_player"."name", COUNT("sandbox_weapon"."id") AS "weapon_count"
FROM "sandbox_player"
LEFT OUTER JOIN "sandbox_unit"
ON ("sandbox_player"."id" = "sandbox_unit"."player_id")
LEFT OUTER JOIN "sandbox_weapon"
ON ("sandbox_unit"."id" = "sandbox_weapon"."unit_id")
GROUP BY "sandbox_player"."id", "sandbox_player"."name"
или Player
с Unit
SELECT "sandbox_player"."id", "sandbox_player"."name", SUM("sandbox_unit"."rarity") AS "rarity_sum"
FROM "sandbox_player"
LEFT OUTER JOIN "sandbox_unit" ON ("sandbox_player"."id" = "sandbox_unit"."player_id")
GROUP BY "sandbox_player"."id", "sandbox_player"."name"
и выполните агрегирование COUNT
или SUM
над ними.
Обратите внимание, что хотя первый запрос имеет два объединенияМежду тремя таблицами промежуточная таблица Unit
не находится ни в столбцах, на которые есть ссылки в SELECT
, ни в предложении GROUP BY
.Единственная роль, которую играет здесь Unit
, - присоединиться к Player
с Weapon
.
Теперь, если вы посмотрите на свой третий набор запросов, все станет сложнее.Опять же, как и в первом запросе, соединения находятся между тремя таблицами, но теперь на Unit
есть ссылка на SELECT
, поскольку существует SUM
агрегация для Unit.rarity
:
SELECT "sandbox_player"."id",
"sandbox_player"."name",
COUNT(DISTINCT "sandbox_weapon"."id") AS "weapon_count",
SUM("sandbox_unit"."rarity") AS "rarity_sum"
FROM "sandbox_player"
LEFT OUTER JOIN "sandbox_unit" ON ("sandbox_player"."id" = "sandbox_unit"."player_id")
LEFT OUTER JOIN "sandbox_weapon" ON ("sandbox_unit"."id" = "sandbox_weapon"."unit_id")
GROUP BY "sandbox_player"."id", "sandbox_player"."name"
И этопринципиальная разница между вторым и третьим запросами.Во втором запросе вы присоединяете Player
к Unit
, поэтому один Unit
будет указан один раз для каждого игрока, на которого он ссылается.
Но в третьем запросе, к которому вы присоединяетесь Player
до Unit
, а затем Unit
до Weapon
, поэтому не только один Unit
будет указан один раз для каждого игрока, на которого он ссылается, , но и для каждого оружия, которое ссылается на Unit
.
Давайте рассмотрим простой пример:
insert into sandbox_player values (1, "player_1");
insert into sandbox_unit values(1, 10, 1);
insert into sandbox_weapon values (1, 1), (2, 1);
Один игрок, один юнит и два оружия, которые ссылаются на один и тот же юнит.
Убедитесь, что проблемасуществует:
>>> from sandbox.models import Player
>>> from django.db.models import Count, Sum
>>> Player.objects.annotate(weapon_count=Count('unit_set__weapon_set')).values()
<QuerySet [{'id': 1, 'name': 'player_1', 'weapon_count': 2}]>
>>> Player.objects.annotate(rarity_sum=Sum('unit_set__rarity')).values()
<QuerySet [{'id': 1, 'name': 'player_1', 'rarity_sum': 10}]>
>>> Player.objects.annotate(
... weapon_count=Count('unit_set__weapon_set', distinct=True),
... rarity_sum=Sum('unit_set__rarity')).values()
<QuerySet [{'id': 1, 'name': 'player_1', 'weapon_count': 2, 'rarity_sum': 20}]>
Из этого примера легко увидеть, что проблема в том, что в комбинированном запросе юнит будет указан дважды, по одному разу для каждого оружия, ссылающегося на него:
sqlite> SELECT "sandbox_player"."id",
...> "sandbox_player"."name",
...> "sandbox_weapon"."id",
...> "sandbox_unit"."rarity"
...> FROM "sandbox_player"
...> LEFT OUTER JOIN "sandbox_unit" ON ("sandbox_player"."id" = "sandbox_unit"."player_id")
...> LEFT OUTER JOIN "sandbox_weapon" ON ("sandbox_unit"."id" = "sandbox_weapon"."unit_id");
id name id rarity
---------- ---------- ---------- ----------
1 player_1 1 10
1 player_1 2 10
Что делать?
Как уже упоминал @ivissani, одним из самых простых решений было бы написать подзапросы для каждого из агрегатов:
>>> from django.db.models import Count, Sum, Subquery, IntegerField
>>> weapon_count = Player.objects.annotate(weapon_count=Count('unit_set__weapon_set')).filter(pk=OuterRef('pk'))
>>> rarity_sum = Player.objects.annotate(rarity_sum=Sum('unit_set__rarity')).filter(pk=OuterRef('pk'))
>>> qs = Player.objects.annotate(
... weapon_count=Subquery(weapon_count.values('weapon_count'), output_field=IntegerField()),
... rarity_sum=Subquery(rarity_sum.values('rarity_sum'), output_field=IntegerField())
... )
>>> qs.values()
<QuerySet [{'id': 1, 'name': 'player_1', 'weapon_count': 2, 'rarity_sum': 10}]>
, который выдает следующий SQL
SELECT "sandbox_player"."id", "sandbox_player"."name",
(
SELECT COUNT(U2."id") AS "weapon_count"
FROM "sandbox_player" U0
LEFT OUTER JOIN "sandbox_unit" U1
ON (U0."id" = U1."player_id")
LEFT OUTER JOIN "sandbox_weapon" U2
ON (U1."id" = U2."unit_id")
WHERE U0."id" = ("sandbox_player"."id")
GROUP BY U0."id", U0."name"
) AS "weapon_count",
(
SELECT SUM(U1."rarity") AS "rarity_sum"
FROM "sandbox_player" U0
LEFT OUTER JOIN "sandbox_unit" U1
ON (U0."id" = U1."player_id")
WHERE U0."id" = ("sandbox_player"."id")
GROUP BY U0."id", U0."name") AS "rarity_sum"
FROM "sandbox_player"