【问题标题】:Django Rest Framework - Filter nested one to manyDjango Rest Framework - 过滤嵌套一对多
【发布时间】:2015-09-21 18:02:52
【问题描述】:

我想要达到的目标:

我想要一份包含各自任务的用户列表,并根据任务开始日期进行过滤。

# Pseudo json
User 1
  - mission 1
  - mission 2
User 2
  - mission 1
  - mission 2
  - mission 3

我的数据结构:

模型

class Mission(models.Model):
  start = models.DateTimeField()
  user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="missions")

序列化器

# Mission
class MissionSerializer(serializers.ModelSerializer):
  class Meta:
    model  = Mission
    fields = (
      'start',
      'end',
    )

# User
class UserSerializer(serializers.ModelSerializer):
  missions = MissionSerializer(many=True)
  class Meta:
    model  = MyUser
    fields = (
      'username',
      'missions',
    )

视图集

# Filter
class UserFilter(django_filters.FilterSet):
  class Meta:
    model  = MyUser
    fields = {
      'missions__start': ['gte','lt']
    }

# Viewset
class UserViewset(viewsets.ModelViewSet):
  filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
  filter_class     = UserFilter
  serializer_class = UserSerializer

  @list_route(methods=['get'])
  def listCalendar(self, request):
    prefetched_missions = Prefetch('missions', queryset=Mission.objects.all())
    objects_list = MyUser.objects.prefetch_related( prefetched_missions )
    objects_list = self.filter_queryset(objects_list)
    serializer   = UserSerializer(objects_list, many=True)
    return Response(serializer.data)

我的问题:

调用此 URL 时:

/api/users/listCalendar/?start__gte=2015-06-29&start__lt=2015-08-10

过滤器被忽略,我想不出办法让它工作。 我有直觉认为问题出在 ViewSet 中的 Mission.objects.all() 可能应该类似于:Mission.objects.filter(*But what here?*)

任何帮助将不胜感激!


编辑 1:

有一些进展!但仍然无法正常工作...正如您建议的 Mark Galloway 我尝试调用以下网址:

/api/users/listCalendar/?missions__start__gte=2015-06-29&missions__start__lt=2015-08-10

但这是被执行的查询:

SELECT "app_myuser"."id", "app_myuser"."username"
FROM "app_myuser"
INNER JOIN "app_mission" ON ( "app_myuser"."id" = "app_mission"."user_id" )
INNER JOIN "app_mission" T4 ON ( "app_myuser"."id" = T4."user_id" )
WHERE ("app_mission"."start" >= '2015-07-06T00:00:00+00:00'::timestamptz
AND T4."start" < '2015-07-12T00:00:00+00:00'::timestamptz)
ORDER BY "app_myuser"."username" ASC;

如您所见,有 2 个 INNER JOIN 而不是 1 个。由于某些原因,它需要 2 个过滤字段,就好像它们在单独的表中一样。结果是我的结果重复了。

【问题讨论】:

  • 这个怎么样? api/users/listCalendar/?missions__start__gte=2015-06-29&missions__start__lt=2015-08-10

标签: python django django-rest-framework django-filter


【解决方案1】:

这里有三样东西

首先,您的filter_backends list 中缺少the DjangoFilterBackend。这就是告诉 Django REST 框架查看 filter_class 并将相关过滤应用于请求的原因,如果没有它,您的 filter_class 将被忽略(如您所见)。

class UserViewset(viewsets.ModelViewSet):
    filter_backends = (filters.OrderingFilter, filters.DjangoFilterBackend, )
    filter_class = UserFilter
    serializer_class = UserSerializer

其次,您希望能够使用startend 查询参数,但告诉django-filter 查看Meta.fields 中的missions__start 字段。您可以通过使用别名手动定义 FilterSet 上的字段来解决此问题

class UserFilter(django_filters.FilterSet):
    start_gte = django_filter.DateTimeFilter(name='missions__start', lookup_type='gte', distinct=True)
    start_lte = django_filter.DateTimeFilter(name='missions__start', lookup_type='lte', distinct=True)

    end_gte = django_filter.DateTimeFilter(name='missions__end', lookup_type='gte', distinct=True)
    end_lte = django_filter.DateTimeFilter(missions__name='end', lookup_type='lte', distinct=True)

    class Meta:
        model  = MyUser
        fields = ('start_gte', 'start_lte', 'end_gte', 'end_lte', )

或者仅通过引用查询参数将获得完整值(missions__start_gte 而不是 start_gte)。

第三,由于INNER JOIN 查询跨多个表的工作方式,在执行影响单个用户下的多个任务的过滤器时,您将收到重复值。您可以通过在过滤器中使用the distinct argument(如上所示)或在filter_queryset 中的过滤器调用末尾添加.distinct() 来解决此问题。

【讨论】:

  • 顶,我也忘记了 DjangoFilterBackend 并且一直想知道我做错了什么。谢谢!
【解决方案2】:

鉴于您要过滤嵌套任务

我建议你反过来做,然后处理 其余的客户端。即

首先发送过滤任务的请求,该请求引用其用户的 ID。
然后发送引用的用户请求,即“#id__in=1 ,2,3"
...或如果您只有少数用户:为所有用户发送请求

话虽如此,我认为你也可以随心所欲,通过 通过扩展 filter_queryset 将过滤器也应用于任务

这是过滤嵌套任务的一种方法

请注意,如果您不想过滤嵌套任务,您可以简单地 从类中删除 filter_queryset 方法。

class MissionFilter(django_filters.FilterSet):
    class Meta:
        model = Mission
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = MyUser
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserViewset(viewsets.ModelViewSet):
    filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
    filter_class     = UserFilter
    serializer_class = UserSerializer

    def get_queryset(self):
        # Get the original queryset:
        qs = super(UserViewset, self).get_queryset()

        # * Annotate:
        #     * start = the start date of the first mission
        #     * end = the end date of the last mission
        # * Make sure, we don't get duplicate values by adding .distinct()
        return qs.annotate(start=models.Min('missions__start'),
                           end=models.Max('missions__end')).distinct()

    def filter_queryset(self, queryset):
        # Get the original queryset:
        qs = super(UserViewset, self).filter_queryset(queryset)

        # Apply same filters to missions:
        mqs = MissionFilter(self.request.query_params,
                            queryset=Missions.objects.all()).qs
        # Notice: Since we "start", and "end" in the User queryset,
        #         we can apply the same filters to both querysets

        return qs.prefetch_related(Prefetch('missions', queryset=mqs))

这是另一个想法

这样您就可以使用您已经在使用的相同查询参数。

class MissionFilter(django_filters.FilterSet):
    class Meta:
        model = Mission
        fields = {
            'start': ['gte', 'lt'],
            'end': ['gte', 'lt'],
        }

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = MyUser
        fields = {
            'missions__start': ['gte', 'lt'],
            'missions__end': ['gte', 'lt'],
        }

class UserViewset(viewsets.ModelViewSet):
    filter_backends  = (filters.OrderingFilter, filters.DjangoFilterBackend,)
    filter_class     = UserFilter
    serializer_class = UserSerializer
    queryset         = MyUser.objects.all().distinct()

    def filter_queryset(self, queryset):
        # Get the original queryset:
        qs = super(UserViewset, self).filter_queryset(queryset)

        # Create a copy of the query_params:
        query_params = self.request.GET.copy()

        # Check if filtering of nested missions is requested:
        if query_params.pop('filter_missions', None) == None:
            return qs

        # Find and collect missions filters with 'missions__' removed:
        params = {k.split('__', 1)[1]: v
                  for k, v in query_params.items() if k.startswith('missions__')}

        # Create a Mission queryset with filters applied:
        mqs = MissionFilter(params, queryset=Missions.objects).qs.distinct()

        return qs.prefetch_related(Prefetch('missions', queryset=mqs))

我还没有测试过任何这些,所以得到一些反馈会很酷。

【讨论】:

  • 啊,等等...您只想要列出这些日期内的任务,对吧?在这种情况下,这是行不通的。
  • 我刚刚添加了一个 filter_queryset 方法,它也可以处理这种情况。
  • 在这里完全构建一个几乎取代过滤器集的自定义过滤器是大材小用
  • filter_queryset 中只添加了 12 行代码。当然,这有点像 hack,但我真的不认为我应该对这个投反对票。
  • 我重写了这个例子,并在原来的基础上增加了一个新的想法。
【解决方案3】:

您的 filter_class 将被忽略,因为您没有在 filter_backends 中声明 DjangoFilterBackend。

class UserViewset(viewsets.ModelViewSet):
  filter_backends = (filters.OrderingFilter, filters.DjangoFilterBackend)
  filter_class = UserFilter

由于您有一个 OrderingFilter 但没有 ordering_fields,也许您放错了后端?

【讨论】:

  • 嗨,你是对的,它无法工作。但是我添加了它,它仍然忽略了过滤器......
【解决方案4】:

我猜你想要的是 Mission.objects.filter(id=self.request.user),这样你就可以获得当前用户的所有任务

【讨论】:

  • 嗨,我想过滤任务开始日期。我编辑了这个问题,使它更明显。
猜你喜欢
  • 2017-10-19
  • 1970-01-01
  • 2019-07-11
  • 1970-01-01
  • 2015-02-27
  • 1970-01-01
  • 2018-07-28
  • 2017-04-09
  • 1970-01-01
相关资源
最近更新 更多