ИЛИ определение фильтров при использовании отношений в фильтре django - PullRequest
0 голосов
/ 24 апреля 2018

У меня есть три модели с простым соотношением, как показано ниже:

models.py

class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=20)

class PersonSession(models.Model):
    start_time = models.DateTimeField(auto_now_add=True)
    end_time = models.DateTimeField(null=True,
                                    blank=True)
    person = models.ForeignKey(Person, related_name='sessions')

class Billing(models.Model):
    DEBT = 'DE'
    BALANCED = 'BA'
    CREDIT = 'CR'

    session = models.OneToOneField(PersonSession,
                                   blank=False,
                                   null=False,
                                   related_name='billing')
    STATUS = ((BALANCED, 'Balanced'),
              (DEBT, 'Debt'),
              (CREDIT, 'Credit'))

    status = models.CharField(max_length=2,
                              choices=STATUS,
                              blank=False,
                              default=BALANCED
                              )

views.py

class PersonFilter(django_filters.FilterSet):
    start_time = django_filters.DateFromToRangeFilter(name='sessions__start_time',
                                 distinct=True)
    billing_status = django_filters.ChoiceFilter(name='sessions__billing__status',
                        choices=Billing.STATUS,
                        distinct=True)

    class Meta:
        model = Person
        fields = ('first_name', 'last_name')

class PersonList(generics.ListCreateAPIView):
    queryset = Person.objects.all()
    serializer_class = PersonSerializer
    filter_backends = (django_filters.rest_framework.DjangoFilterBackend)
    filter_class = PersonFilter

Я хочу получать счета от конечной точки, которые имеют статус DE в биллинге и имеют период времени:

api/persons?start_time_0=2018-03-20&start_time_1=2018-03-23&billing_status=DE

Но результат не тот, который я искал, это возвращает всех людей, у которых есть сеанс в тот период и есть биллинг со статусом DE, независимо от того, идет ли этот биллинг за период или нет.

Другими словами, кажется, используется операция or между двумя полями фильтра, я думаю этот пост связан с этой проблемой, но в настоящее время я не смог найти способ получить желаемый результат. Я использую djang 1.10.3.

Редактировать

Я пытаюсь написать пример , чтобы показать, что мне нужно и что я получаю из фильтра django. Если я получу людей, использующих приведенный ниже запрос в примере, я получу только двух человек:

select * 
from 
test_filter_person join test_filter_personsession on test_filter_person.id=test_filter_personsession.person_id join test_filter_billing on test_filter_personsession.id=test_filter_billing.session_id 
where
start_time > '2000-02-01' and start_time < '2000-03-01' and status='DE';

Что дает мне только 1 и 2 человека. Но если я получу что-то похожее от URL, я получу всех людей, аналогичный URL (по крайней мере, тот, который я ожидал, будет таким же), как показано ниже:

http://address/persons?start_time_0=2000-02-01&start_time_1=2000-03-01&billing_status=DE

Edit2

Это данные, к которым относятся мои запросы в этом примере, и, используя их, вы можете увидеть, что должно возвращаться в запросах, о которых я упоминал выше:

 id | first_name | last_name | id |        start_time         |         end_time          | person_id | id | status | session_id 
----+------------+-----------+----+---------------------------+---------------------------+-----------+----+--------+------------
  0 | person     | 0         |  0 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         0 |  0 | DE     |          0
  0 | person     | 0         |  1 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         0 |  1 | BA     |          1
  0 | person     | 0         |  2 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         0 |  2 | DE     |          2
  1 | person     | 1         |  3 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         1 |  3 | BA     |          3
  1 | person     | 1         |  4 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         1 |  4 | DE     |          4
  1 | person     | 1         |  5 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         1 |  5 | DE     |          5
  2 | person     | 2         |  6 | 2000-01-01 16:32:00+03:30 | 2000-01-01 17:32:00+03:30 |         2 |  6 | DE     |          6
  2 | person     | 2         |  7 | 2000-02-01 16:32:00+03:30 | 2000-02-01 17:32:00+03:30 |         2 |  7 | DE     |          7
  2 | person     | 2         |  8 | 2000-03-01 16:32:00+03:30 | 2000-03-01 17:32:00+03:30 |         2 |  8 | BA     |          8

Edit3

Я пытаюсь использовать prefetch_related для объединения таблиц и получения результатов, как я ожидал, потому что я думал, что дополнительное объединение вызывает эту проблему, но это не сработало, и я все еще получаю тот же результат, и это не имело никаких эффектов.

Edit4

Эта проблема имеет ту же проблему.

1 Ответ

0 голосов
/ 11 мая 2018

У меня пока нет решения; но я думал, что краткое изложение этой проблемы заставит больше и лучше думать, чем мои на работе!

Из того, что я понимаю; Ваша основная проблема является результатом двух предварительных условий:

  1. Тот факт, что у вас есть два дискретных фильтра, определенных в связанной модели; в результате чего фильтр spanning-multi-valueed-Relations
  2. Способ FilterSet реализует фильтрацию

Давайте посмотрим на них более подробно:

фильтр связывающих многозначных отношений

Это отличный ресурс, чтобы лучше понять условие проблемы # 1: https://docs.djangoproject.com/en/2.0/topics/db/queries/#spanning-multi-valued-relationships

По существу, фильтр start_time добавляет .filter(sessions__start_time=value) к вашему Queryset, а фильтр billing_status добавляет .filter(sessions_billing_status=value) к фильтру. Это приводит к описанной выше проблеме «spanning-multi-Valueed-Relations», что означает, что между этими фильтрами будет OR вместо AND, как вам требуется.

Это заставило меня задуматься, почему мы не видим ту же проблему в фильтре start_time; но хитрость здесь в том, что она определяется как DateFromToRangeFilter; он внутренне использует один запрос фильтра с конструкцией __range=. Если бы вместо этого было sessions__start_time__gt= и sessions__start_time__lt=, у нас была бы та же проблема.

Способ FilterSet реализующий фильтрацию

Разговор дешев; покажи мне код

@property
def qs(self):
    if not hasattr(self, '_qs'):
        if not self.is_bound:
            self._qs = self.queryset.all()
            return self._qs

        if not self.form.is_valid():
            if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
                raise forms.ValidationError(self.form.errors)
            elif self.strict == STRICTNESS.RETURN_NO_RESULTS:
                self._qs = self.queryset.none()
                return self._qs
            # else STRICTNESS.IGNORE...  ignoring

        # start with all the results and filter from there
        qs = self.queryset.all()
        for name, filter_ in six.iteritems(self.filters):
            value = self.form.cleaned_data.get(name)

            if value is not None:  # valid & clean data
                qs = filter_.filter(qs, value)

        self._qs = qs

    return self._qs

Как видите, свойство qs разрешается путем итерации по списку Filter объектов, последовательного прохождения начальных qs через каждого из них и возврата результата. Смотри qs = filter_.filter(qs, value)

Каждый объект Filter здесь определяет определенную операцию def filter, которая в основном принимает Queryset, а затем добавляет к нему последовательный .filter.

Вот пример из BaseFilter класса

   def filter(self, qs, value):
        if isinstance(value, Lookup):
            lookup = six.text_type(value.lookup_type)
            value = value.value
        else:
            lookup = self.lookup_expr
        if value in EMPTY_VALUES:
            return qs
        if self.distinct:
            qs = qs.distinct()
        qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
        return qs

Важная строка кода: qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})

Таким образом, два предварительных условия создают идеальный шторм для этой проблемы.

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