- Published on
Django 매월 특정 날짜만(마지막 날짜) 가져오기
- Authors
- Name
- hongreat
- ✉️hongreat95@gmail.com
매월(month) 마지막 날짜의 데이터를 연도(year)만 입력했을 때 필터링하여 가져오는 방법입니다.
특정 페이지에서 연도별 차트를 가져올 때, 매월 마지막 날짜만 가져와야 합니다.
예를들어서 6월 30일에 API 조회를 한다면 1월 31일/2월 29일(혹은 28일)/3월31일/…/6월 30일 의 데이터만 가져오는 것 입니다. (일별 데이터는 매일 백그라운드(celery&aws event bridge&cron)로 생성하고 있기 때문에 마지막 일 여부에 대해서는 보장이 되어있습니다.)
떠오른 방법은 2가지이고 2번째 방법에 대해서 다뤄보겠습니다.
- 특정 일을 따로 저장하거나 세팅해두고 (ex:
[1월 31일,…]
)datefield
기준__in
을 사용해서 가져오는 방법도 있습니다. - 월로 그룹화하고
max()
매소드로, 매월 가장 마지막 날짜를 가져오는 방법 입니다.
코드
import django_filters
from django.db.models import Max
from django.utils.timezone import make_aware
from datetime import datetime, timedelta
from django.db.models.functions import ExtractMonth
from .models import InvestmentResult
class InvestmentResultFilter(django_filters.FilterSet):
# 이번 시간에 사용할 필터링
year = django_filters.NumberFilter(method='filter_by_year')
# month 는 무시
month = django_filters.NumberFilter(field_name="invested_at", lookup_expr="month")
class Meta:
model = InvestmentResult
fields = ['year', 'month']
def filter_by_year(self, queryset, name, value):
# 연도별로 필터링
start_date = make_aware(datetime(value, 1, 1))
end_date = make_aware(datetime(value + 1, 1, 1)) - timedelta(seconds=1)
queryset = queryset.filter(invested_at__range=(start_date, end_date))
# 각 월의 마지막 날짜 가져오기
last_days = (
queryset
.annotate(month=ExtractMonth('invested_at'))
.values('month')
.annotate(last_day=Max('invested_at'))
)
# 마지막 날짜의 데이터만 필터링
last_day_dates = [entry['last_day'] for entry in last_days]
return queryset.filter(invested_at__in=last_day_dates)
Filters
django_filters
사용하여 필터셋 클래스를 정의합니다.year
필터는 커스텀 메서드를 사용하고,month
필터는 기본 필터링 기능을 사용합니다.
ORM
1. 기간 세팅
start_date = make_aware(datetime(value, 1, 1))
end_date = make_aware(datetime(value + 1, 1, 1)) - timedelta(seconds=1)
queryset = queryset.filter(invested_at__range=(start_date, end_date))
start_date
는 주어진 연도의 1월 1일을 나타냅니다.end_date
는 주어진 연도의 마지막 순간(12월 31일 23:59:59)을 나타냅니다.make_aware
는 날짜를 timezone-aware datetime 객체로 변환합니다.
2. 각 월의 마지막 날짜 가져오기
last_days = (
queryset
.annotate(month=ExtractMonth('invested_at'))
.values('month')
.annotate(last_day=Max('invested_at'))
)
annotate(month=ExtractMonth('invested_at'))
는invested_at
필드에서 월을 추출하여month
라는 가상의 필드를 만듭니다.month
로 Group_by(화)합니다.annotate(last_day=Max('invested_at'))
는 각 월별로invested_at
필드의 최대값을last_day
로 계산합니다.
3. 마지막 날짜의 데이터 필터링
last_day_dates = [entry['last_day'] for entry in last_days]
return queryset.filter(invested_at__in=last_day_dates)
last_days
쿼리셋에서 각 월의 마지막 날짜를 리스트로 추출합니다.- 원래의
queryset
에서invested_at
이last_day_dates
에 포함된 데이터만 필터링하여 반환합니다.
Extract 에 대해서
extra
메서드
last_days = (
queryset
.extra(select={'month': "EXTRACT(month FROM invested_at)"})
.values('month')
.annotate(last_day=Max('invested_at'))
)
extra(select={'month': "EXTRACT(month FROM invested_at)"})
는 SQL의EXTRACT
함수를 사용하여invested_at
필드에서 월을 추출합니다.- 위에서 작성한 것과 동일하게 동작합니다.
lookup
문서를 보면 코드의 스타일을 보고 프레임워크가 지향 하는 코드스타일을 파악할 수 있는데 Django 는 lookup 방식으로 많은(?) 편리함과 장점을 줍니다.
저는 실제 코드에서 look_up 방식 invested_at__month
으로 사용했습니다.
last_days = (
queryset.values("invested_at__month")
.annotate(last_day=Max("invested_at"))
)
Django ORM에서 ExtractMonth
나 invested_at__month
를 사용할 때, 내부적으로 이 EXTRACT 함수를 사용하여 SQL 쿼리를 생성합니다.
(데이터베이스 시스템에 따라 EXTRACT 함수의 사용법이나 지원되는 필드가 약간씩 다를 수 있습니다. PostgreSQL, MySQL, Oracle 에서는 사용가능 합니다.)
환경
이로써 연도별 필터링과 각 월의 마지막 날짜 데이터를 필터링하는 방법에 대해 전체적으로 정리해보았습니다. 직접 SQL을 작성하는 경우 SQL 인젝션 공격에 대비한 보안을 염두해야 하겠습니다.
- postgresql
- Django
- DRF
문서
extra
- Django
extra
메서드 공식 문서:extra
메서드를 사용하는 방법과 주의사항에 대해 설명합니다.
ExtractMonth
- Django 날짜 함수 (
ExtractMonth
):ExtractMonth
와 같은 함수들을 사용하는 방법에 대해 설명합니다.