如何为来自相关模型的聚合数据实现自定义 django 过滤器

Posted

技术标签:

【中文标题】如何为来自相关模型的聚合数据实现自定义 django 过滤器【英文标题】:How to implement custom django filter for aggregated data from related model 【发布时间】:2021-12-20 08:22:05 【问题描述】:

我已经使用 Django Rest Framework 构建了一个简单的 API

两种模型:Person 和 Hike(人为 FK)

我已经安装了这些 PIP 包:

package version
Django 3.2.7
django-filter 21.1
django-mathfilters 1.0.0
djangorestframework 3.12.4
djangorestframework-api-key 2.1.0

models.py

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

class Hike(models.Model):
    hiker               = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='hikes')
    hike_date           = models.DateField(max_length=100, blank=False)
    distance_mi         = models.FloatField(blank=False)

views.py

...
class PersonViewSet(viewsets.ModelViewSet):
    queryset = Person.objects.all() 
    serializer_class = PersonSerializer

serializers.py

class PersonSerializer(serializers.ModelSerializer):
    hikes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

    all_hikes = Hike.objects.all()
    def total_mi(self, obj):
        result = self.all_hikes.filter(hiker__id=obj.id).aggregate(Sum('distance_mi'))
        return round(result['distance_mi__sum'], 2)

...
total_miles = serializers.SerializerMethodField('total_mi')

...
class Meta:
    model = Person
    fields = ('id','first_name','last_name','hikes','total_miles')

filters.py

class HikerFilter(django_filters.FilterSet):
    hiker = django_filters.ModelChoiceFilter(field_name="hiker",
                                             queryset=Person.objects.all())
    class Meta:
        model = Hike
        fields = 
            'hiker': ['exact'],
            'hike_date': ['gte', 'lte', 'exact', 'gt', 'lt'],
            'distance_mi': ['gte', 'lte', 'exact', 'gt', 'lt'],
        

样本数据:远足

id hike_date distance_mi
2 2020-11-02 4.5
3 2021-03-16 3.3
5 2021-08-11 5.3
7 2021-10-29 4.3

Person 视图包括通过 Serializer (total_mi) 添加的“total_miles”统计数据。

个人端点 http://localhost:8000/persons/2/

    
        "id": 2,
        "first_name": "Miles",
        "last_name": "Marmot",
        "hikes": [
            2,
            3,
            5,
            7
        ],
        "total_miles": 17.4,
    ,

目前,“total_miles”适用于所有年份。

我的问题:如何通过传递 URL 参数按特定年份在“个人”视图中过滤“total_miles”(浮动)和“hikes”(列表)?

例如http://localhost:8000/persons/2/?year=2020 > "total_miles": 4.5,

例如http://localhost:8000/persons/2/?year=2021 > "total_miles": 12.9,

-- 我能够在Serializer.py 中按年限制“total_miles” all_hikes = Hike.objects.filter(hike_date__year='2020') 但年份是硬编码的,仅用于测试。

我可以将参数/var 传递给 Serializer 函数吗?

或者这可以通过自定义过滤器来实现吗?

可选/奖励:

Person 视图中的“hikes”(id 列表)也可以按 Year 过滤吗?

例如http://localhost:8000/persons/2/?year=2020 > "total_miles": 4.5, "hikes": [2]

例如http://localhost:8000/persons/2/?year=2021 > "total_miles": 12.9, "hikes": [3, 5, 7]

提前致谢!最好的~

【问题讨论】:

我只是想记下这个问题的辅助。我可以通过 URL 参数获取一个人一年(或任何日期范围)的所有远足: localhost:8000/hikes/?hiker=2&hike_date__gte=2021-01-01&hike_date__lt=2022-01-01 按人返回所有远足(id =2) 表示“2021” 【参考方案1】:

请求已经传递给序列化程序的__init__ 并存储在context 属性中

所以在序列化器中你可以这样做:

year = self.context["request"].GET.get("year") 
if year:
    all_hikes = Hike.objects.filter(hike_date__year=year)

c.f.

https://www.django-rest-framework.org/api-guide/serializers/#including-extra-context https://www.django-rest-framework.org/api-guide/generic-views/

【讨论】:

谢谢!这对于 Persons 和 Hikes 上的“total_miles”都很有效,即通过“year=2021”。还给了我按年份过滤远足的想法。除了total_miles,我还有total_hikes、total_elevation_gain、highest_elev。在我最初的实现中,我在这些方法之外进行了一次数据库调用all_hikes = Hike.objects.all() 并使用了`self.all_hikes。在计算聚合的方法中。为了实施您的建议/修复,我在每个方法中进行此调用。因此,每次我获取数据时,它都会进行 4 次数据库调用,而不是原来的一次。这很糟糕吗? 不要将all_hikes = Hike.objects.all() 声明为类属性,在方法内部进行。或者您可以从 Person 实例访问它:obj.hikes.filter(.... 对不起,我之前的后续问题问得很糟糕。我原来的帖子只显示了“total_miles”,但实际上我有 4 种方法/4 种计算(total_hikes、total_miles、total_elevation、highest_elevation)。以前,我对数据库进行了一次调用(作为类属性),并对方法中检索到的数据进行了计算。因为我现在已经在每个方法中移动了Hikes.objects.all(),所以我对数据库进行了 4 次调用,然后进行计算。似乎糟糕的应用程序设计来获取该数据 4xs。无论如何,我感谢您的反馈。【参考方案2】:

这是我如何实现@pleasedontbelong 的答案。

    对于 hiker > total_miles - 如果传递了 year,则仅计算该 total。如果没有,total 将计算 所有年份

serializers.py

...
    def total_mi(self, obj):
        all_hikes = Hike.objects.all()
        year = self.context["request"].GET.get("year")
        if year:
            all_hikes = all_hikes.filter(hike_date__year=year)
        result = all_hikes.filter(hiker__id=obj.id).aggregate(Sum('distance_mi'))
        return round(result['distance_mi__sum'], 2)
    对于 hiker > hikes[] - 如果通过了 year,则列表仅包含该 hike ids。如果不是,则包括所有年份远足ID

我替换了hikes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)

在我的 PersonSerializer 中使用以下 get_user_hikes 方法:

serializers.py

...
    def get_user_hikes(self, obj):
        all_hikes = Hike.objects.filter(hiker__id=obj.id)
        year = self.context["request"].GET.get("year")
        if year:
            all_hikes = all_hikes.filter(hike_date__year=year)
        result = all_hikes.values_list('pk', flat=True)
        return list(result)
...
hikes = serializers.SerializerMethodField('get_user_hikes')
    为了重用year=2020 来过滤Hikes,我在现有的HikerFilter 中添加了year

filters.py

...
year = django_filters.NumberFilter(field_name='hike_date', lookup_expr='year')

成功了!

http://localhost:8000/hikes/?hiker=2&year=2020

[
    
        "id": 2,
        "hike_date": "2020-11-02",
        "location": "Frog Lake",
        "state": "OR",
        "distance_mi": 4.5,
        "elevation_gain_ft": 1275,
        "highest_elev_ft": 6739,
        "alltrails_url": null,
        "blogger_url": null,
        "hiker": 
            "id": 2,
            "first_name": "Miles",
            "last_name": "Marmot",
            "slug": "miles-marmot",
            "join_date": "2020-10-11T11:45:02-07:00",
            "email": "miles.marmot@wta.com",
            "profile_img": "http://localhost:8000/static/images/2021/11/01/hat_miles_2020-1.jpeg",
            "hikes": [
                2
            ],
            "total_hikes": 1,
            "total_miles": 4.5,
            "total_elev_feet": 1275,
            "highest_elev_feet": 6739
        
    
]

【讨论】:

以上是关于如何为来自相关模型的聚合数据实现自定义 django 过滤器的主要内容,如果未能解决你的问题,请参考以下文章

如何为 keras 模型使用 tensorflow 自定义损失?

如何为 LSTM 实现 Keras 自定义损失函数

如何为 Flutter WebRTC 使用自定义视频源

如何为来自 SpringBoot 后端代码的嵌套 JSON 响应在 angular8(TypeScript) 或更高版本中定义模型类

如何为其模型中文本的不同代码点自定义Java 9+ JTextField的视图?

如何为spring数据编写自定义模块