annotateで作った値を使ってdjango-filterでフィルタリングする方法
programmingはじめに
Djangoとdjango-filterをよく使っているのですが、 annotate
関数などで既存のフィールドから計算して作成した値を使ってdjango-filterでフィルタリングする、ということを実現するのに少し手間取ったため、その方法を備忘録として残しておきます。
やりたいこと
例えば業務の案件を管理するようなアプリで、ある案件 (Work
) に対して複数の作業記録 (WorkReport
) を紐付けて、作業記録にはその日付が保持できるようになっているとします。
やりたかったのは、案件に対して複数存在する作業記録のうち最新のものの日付を利用して案件を絞り込むということです。
例えば案件Aに対して2021年8月10日と2021年8月13日に作業記録を追加した場合、その最新の作業記録の日付は2021年8月13日になりますが、2021年8月13日で絞り込んだ際に案件Aが出てきてほしいということです。
以下にモデル定義を示します。
from django.db import models
class Work(models.Model):
# some fields
pass
class WorkReport(models.Model):
work = models.ForeignKey(
Work,
on_delete=models.CASCADE,
)
date = models.DateField()
実現方法
方針
以下の方針を考えました。
annotate
関数を用いて、案件の queryset の各アイテムに対して最新の作業記録の日付を計算しその値を追加する- django-filterのFilterの
method
をオーバーライドする (参考)
手順
1. カスタム queryset を定義
まず、先程のモデル定義に以下のようなカスタム queryset を追加します。 (参考)
from django.db import models
# 追加
class WorkQueryset(models.QuerySet):
def with_latest_work_report_date(self):
return self.annotate(
latest_work_report_date=models.Max('workreport__date')
)
class Work(models.Model):
# 追加
objects = WorkQueryset.as_manager()
このようにすると、例えば Work.objects.with_latest_work_report_date()
や {queryset}.with_latest_work_report_date()
などで、最新の作業記録の日付 latest_work_report_date
が annotate された queryset が得られます。
2. FilterSet を定義
次に、 django-filter の FilterSet を定義します。
class WorkFilter(FilterSet):
latest_work_report_date = filters.DateFromToRangeFilter(
label='最新作業記録日 (範囲指定)',
method='filter_latest_work_report_date',
)
def filter_latest_work_report_date(self, queryset, name, value):
queryset = queryset.with_latest_work_report_date() # ここが大事です
if value.start:
queryset = queryset.filter(**{f'{name}__gte': value.start})
if value.stop:
queryset = queryset.filter(**{f'{name}__lte': value.stop})
return queryset
class Meta:
model = Work
# fields = (...)
DateFromToRangeFilter
の method
引数には、 queryset を返すメソッドを作成して指定します。
こうすることで自由に絞り込みのロジックを入れることができます。
ここでややこしいのが今回 DateFromToRangeFilter
という日付のレンジのフィルターを使っているため、 filter_latest_work_report_date
メソッドの name
にはシンプルに latest_work_report_date
が入るのですが、 value
には slice
オブジェクトが入ってきます。
slice
オブジェクトはスライスを表していて、例えば以下のように配列のスライスに使えるものです。
l = list(range(10))
slice_instance = slice(1, 3)
print(l[slice_instance])
# --> [1, 2]
latest_work_report_date
の value
に入ってくる slice
オブジェクトには、以下のように start
と stop
に datetime.datetime
オブジェクト、 step
には None
が入っています。
slice(
datetime.datetime(2021, 8, 10, 0, 0, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>),
datetime.datetime(2021, 8, 10, 23, 59, 59, 999999, tzinfo=<DstTzInfo 'Asia/Tokyo' JST+9:00:00 STD>),
None
)
この start
と stop
を使って日付の範囲で queryset を絞り込んで返すことで、範囲指定の絞り込みが実現できます。
改善したい点
filter_latest_work_report_date
のように日付の範囲での絞り込みのロジックをできれば書かずに実現したかったのですが、今の所はまだこのやり方でしかうまく動かせていません。
このままだと他にも似たようなことをするときに同じ絞り込みロジックを書かないといけません。
queryset.with_latest_work_report_date()
で annotate したものを DateFromToRangeFilter
に既存の絞り込みロジックに渡すことができれば改善できそうです。
おわりに
もし改善点が見つかればまた更新したいと思います。