简体   繁体   English

Django Rest Framework - 过滤嵌套的一对多

[英]Django Rest Framework - Filter nested one to many

What I want to achieve: 我想要实现的目标:

I want a list of users with their respective missions, and filter on missions start date. 我想要一个具有各自任务的用户列表,并过滤任务开始日期。

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

My data structure: 我的数据结构:

Models : 型号

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

Serializers : 序列化器

# 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',
    )

Viewsets : 视图集

# 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)

My Problem: 我的问题:

When calling this URL: 调用此URL时:

/api/users/listCalendar/?start__gte=2015-06-29&start__lt=2015-08-10 / API /用户/ listCalendar /?start__gte = 2015-07-02&start__lt = 2015年8月10日

The filter is ignored and I can't figure out a way to make it work. 过滤器被忽略,我无法找到使其工作的方法。 I have the intuition that the problem is with Mission.objects.all() in the ViewSet that should probably be something like: Mission.objects.filter(*But what here?*) 我有直觉认为问题出在ViewSet中的Mission.objects.all()应该是这样的: Mission.objects.filter(*But what here?*)

Any help would be very much appreciated! 任何帮助将非常感谢!


Edit 1: 编辑1:

There is some progress! 有一些进步! But still not working... As you suggested Mark Galloway i tried calling the following url: 但仍然没有工作......正如你建议马克加洛韦我尝试调用以下网址:

/api/users/listCalendar/?missions__start__gte=2015-06-29&missions__start__lt=2015-08-10 / API /用户/ listCalendar /?missions__start__gte = 2015-07-02&missions__start__lt = 2015年8月10日

But this is the query that gets executed: 但这是执行的查询:

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;

As you can see, there are 2 INNER JOIN instead of 1. For some reasons it takes the 2 filtered fields as if they were in separate tables. 正如您所看到的,有2个INNER JOIN而不是1.由于某些原因,它将2个过滤字段视为在单独的表中。 The result is that my results are dupplicated. 结果是我的结果是重复的。

There are three things here 这里有三件事

First, you're missing the DjangoFilterBackend in your filter_backends list . 首先,你错过DjangoFilterBackendfilter_backends列表 This is what tells Django REST framework to look at the filter_class and apply the related filtering to the request, and without it your filter_class will be ignored (as you saw). 这就是告诉Django REST框架查看filter_class并将相关过滤应用于请求,如果没有它,您的filter_class将被忽略(如您所见)。

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

Second, you're expecting to be able to use the start and end query parameters but are telling django-filter to look at the missions__start field in the Meta.fields . 其次,你期待能够使用的startend的查询参数,但告诉Django的过滤器看missions__start领域Meta.fields You can fix this by manually defining the fields on the FilterSet with your alias 您可以通过使用别名手动定义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', )

Or by just referencing the query parameters will the full values ( missions__start_gte instead of start_gte ). 或只是引用查询参数将全值( missions__start_gte而不是start_gte )。

Third, because of how INNER JOIN queries work across multiple tables, you will receive duplicate values when doing a filter that affects multiple missions under a single user. 第三,由于INNER JOIN查询在多个表中的工作方式,在执行影响单个用户下的多个任务的过滤器时,您将收到重复值。 You can fix this by using the distinct argument in your filters (as shown above) or adding .distinct() to the end of your filter calls in filter_queryset . 您可以通过在过滤器中使用distinct参数 (如上所示)或在filter_queryset过滤器调用的末尾添加.distinct()来解决此filter_queryset

Given that you want to filter nested missions 鉴于您想要过滤嵌套任务

I would suggest that you do this the other way around, and then handle the rest client side. 我建议你反过来这样做,然后处理其余的客户端。 ie

First send a request for filtered missions, that reference the id of their user. 首先发送过滤任务的请求,引用其用户的ID。
Then send a request for the referenced users ie "#id__in=1,2,3" 然后发送对引用用户的请求,即“#id__in = 1,2,3”
...or if you'll only ever have a small number of users: Send a request for all the user ...或者如果您只有少量用户:向所有用户发送请求

That being said, I think you can also have your way if you want, by applying the filters to the missions as well, by extending filter_queryset 话虽如此,我认为你也可以通过扩展filter_queryset将你的过滤器应用于任务。

Here is one approach to filtering nested missions 这是一种过滤嵌套任务的方法

Note that if you don't want to filter the nested missions, you can simply delete the filter_queryset method from the class. 请注意,如果您不想过滤嵌套任务,则只需从类中删除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))

Here is another idea 这是另一个想法

This way you can use the same query parameters that you're already using. 这样您就可以使用已经使用的相同查询参数。

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))

I haven't tested any of this, so it would be cool to get some feedback. 我还没有测试过这个,所以得到一些反馈会很酷。

Your filter_class is being ignored because you are not declaring the DjangoFilterBackend inside filter_backends. 您的filter_class被忽略,因为您没有在filter_backends中声明DjangoFilterBackend。

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

Since you have an OrderingFilter but no ordering_fields, perhaps you put the wrong backend? 既然你有一个OrderingFilter但没有排序字段,也许你放错了后端?

我猜你的任务是Mission.objects.filter(id = self.request.user),你将获得当前用户的所有任务

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM