如何为来自相关模型的聚合数据实现自定义 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 自定义损失?
如何为来自 SpringBoot 后端代码的嵌套 JSON 响应在 angular8(TypeScript) 或更高版本中定义模型类