Django - получить объекты, имеющие связанные объекты только с определенным полем - PullRequest
2 голосов
/ 10 апреля 2020

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".

1 Ответ

1 голос
/ 10 апреля 2020

Вот как я смог заставить его работать по вашим правилам. Обратите внимание, что это модель class method на Person, а не метод менеджера.

class Person(models.Model):
   ...

    @classmethod
    def get_multiple_jobs(cls, positions=None):
        if positions is None:
            positions = []

        annotations = {}
        filters = []
        if positions:
            operator_ = "gt" if len(positions) == 1 else "gte"
            for position in positions:
                annotations[f"cnt_{position}"] = Count(
                    "jobs__position", filter=Q(jobs__position=position)
                )
                filters.append(Q(**{f"cnt_{position}__{operator_}": 1}))
            annotations["cnt_positions"] = Count("jobs__position", distinct=True)
            filters.append(Q(cnt_positions=len(positions)))
        else:
            annotations["cnt_positions"] = Count("jobs__position")
            filters.append(Q(cnt_positions__gt=1))

        return Person.objects.annotate(**annotations).filter(*filters).distinct()

Вы можете назвать его как Person.get_multiple_jobs().

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...