Django 1.11, Python 2.7
С учетом следующих моделей:
class Person(models.Model):
objects = PersonManager()
...
class Job(models.Model):
person = models.ForeignKey(
to=Person,
related_name='jobs'
)
workplace = models.ForeignKey(
to=Workplace,
related_name='workers'
)
position = models.CharField(db_index=True, max_length=255, blank=False, null=False, choices=(
(POSITION_1, POSITION_1),
(POSITION_2, POSITION_2),
(POSITION_3, POSITION_3),
))
Человек может иметь несколько рабочих мест на одной и той же должности и на разных должностях.
person1: [workplace1, POSITION_1], [workplace1, POSITION_2],
[workplace2, POSITION_1]
person2: [workplace1, POSITION_2],
[workplace2, POSITION_2]
person3: [workplace3, POSITION_3]
Я хочу написать один метод в PersonManager
, который будет извлекать всех людей с несколькими работами на определенной заданной должности (и только на этой должности); или если задано несколько позиций, лица, работающие на всех этих позициях.
Person.objects.get_multiple_jobs(jobs=[])
вернутся person1, person2
Person.objects.get_multiple_jobs(jobs=[POSITION_2])
вернутся * ТОЛЬКО 1018 * (как он единственный, у которого только POSITION_2
несколько заданий). Person.objects.get_multiple_jobs(jobs=[POSITION_1, POSITION_2])
вернет person1
Person.objects.get_multiple_jobs(jobs=[POSITION_3])
не вернет ничего
Редактировать 1 : Чтобы уточнить, я хочу, чтобы лица с несколькими заданиями имели рабочие места в ВСЕХ перечисленных должностях и ТОЛЬКО в них.
Использование Person.objects.annotate(position_count=Count('jobs')).filter(position_count__gt=1, jobs__position__in=[...])
не будет работать, так как в третьем случае я также получу person2
.
Цепочка filter
/ exclude
, как Person.objects.filter(jobs__position=POSITION_1).exclude(jobs__position__in=[POSITION_1,POSITION_3]
, будет работать, но не поддерживается, - что если в будущем больше постов будет добавлено? Решать, какие задания динамически исключать, обременительно. Это также приводит к тому, что фильтры очень жестко кодируются, когда я хотел инкапсулировать логи c с помощью одного метода PersonManager
.
Я думал об использовании Q
в фильтрации или подпрограмме. запрос, но я не могу прийти с рабочим запросом, который достаточно модульный, чтобы работать во всех случаях и также включать поддержку.
Редактировать 2:
Решение, согласно Душану Магару ответ, после настройки на django 1.11:
Сначала я попытался изменить синтаксис и заменить внутреннюю filter
Count
простой Case When
:
annotations['cnt_{}'.format(pos)] = Count(
Case(
When(
jobs__position=pos,
then=1
),
default=0,
output_field=IntegerField()
)
)
Но это не сработало.
Получился следующий запрос:
SELECT "person"."id",
"person"."name",
...
COUNT(CASE WHEN "job"."position" = \'Driver\' TEHN 1 ELSE 0 END) AS "cnt_Driver"
FROM "person" LEFT OUTER JOIN **long and irrelevant**
После игры с самим SQL я нашел подзапрос, необходимый для его выполнения. работать, поскольку COUNT
не будет делать:
(SELECT COUNT(*) from "job" WHERE "job"."position" = 'Driver' and "job"."person_id" = "person"."id" ) as "cnt_Driver"
Чтобы выполнить этот подзапрос с помощью django:
sub = Job.objects.filter(
person=OuterRef('pk'),
position=pos
).values('person').annotate(c=Count('*')).values_list('c')
Важно .values('person')
- django добавляет свое собственное предложение GROUP BY
, которое включает в себя все значения схемы (и, следовательно, только один результат на человека, так как все Jobs
являются ), и на этом шаге GROUP BY
будет состоять только из "person"."id"
.