Django-filter: μ—­λ°©ν–₯ μ™Έλž˜ ν‚€λ₯Ό 필터링할 수 μ—†μŒ

에 λ§Œλ“  2020λ…„ 06μ›” 22일  Β·  10μ½”λ©˜νŠΈ  Β·  좜처: carltongibson/django-filter

μ†Œκ°œ

μ—¬λ³΄μ„Έμš”! 이 λ„μ„œκ΄€μ— 정말 κ°μ‚¬ν•©λ‹ˆλ‹€! ν˜„μž¬ API둜 ν…ŒμŠ€νŠΈν•  수 μžˆμ–΄μ„œ 정말 κΈ°λ»€μŠ΅λ‹ˆλ‹€!

ν˜„μž¬ λͺ©ν‘œ

School 와 SchoolCourse 의 두 가지 λͺ¨λΈμ΄ μžˆμŠ΅λ‹ˆλ‹€. SchoolCourse μ—λŠ” School 에 λŒ€ν•œ λ‹€λŒ€μΌ 관계가 λ°˜λŒ€μž…λ‹ˆλ‹€.

SchoolCourse 의 name λ₯Ό 기반으둜 School 개체λ₯Ό 필터링할 수 있기λ₯Ό μ›ν•©λ‹ˆλ‹€.

λ˜ν•œ ν™•μž₯ λͺ©ν‘œλ‘œ κ²°κ΅­ SchoolLesson λ‘œλ„ 필터링할 수 있기λ₯Ό λ°”λžλ‹ˆλ‹€.

데이터 λͺ¨λΈ

학ꡐ λͺ¨λΈ

class SchoolModel(Model):
    name = CharField(max_length=24, unique=True)

    REQUIRED_FIELDS = ["name"]

    class Meta:
        ordering = ("id",)

    def get_courses(self):
        school_courses = SchoolCourseModel.objects.filter(school=self)
        return school_courses

학ꡐ μ½”μŠ€

class SchoolCourseModel(Model):
    description = CharField(default="", max_length=200)
    name = CharField(max_length=50)
    school = ForeignKey(SchoolModel,
                        related_name="school_course",
                        on_delete=CASCADE)

    REQUIRED_FIELDS = ["school", "name"]

    class Meta:
        ordering = ("id",)
        unique_together = ("school", "name",)

    def get_lessons(self):
        school_lessons = SchoolLessonModel.objects.filter(course=self)
        return school_lessons

μ–΄λŠ 것 - 디버그 λ³΄κΈ°μ—μ„œ μ΄λŸ¬ν•œ λͺ¨λΈλ‘œ ν”Œλ ˆμ΄ν•˜λ©΄ λ‹€μŒμ„ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

all_school_models[50].school_course
(Pdb++) <django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager object at 0x10a8177d0>
all_school_models[50].school_course.all()
(Pdb++) <QuerySet [<SchoolCourseModel: SchoolCourseModel object (151)>, <SchoolCourseModel: SchoolCourseModel object (152)>, <SchoolCourseModel: SchoolCourseModel object (153)>]>
all_school_models[50].school_course.all()[0]
(Pdb++) <SchoolCourseModel: SchoolCourseModel object (151)>
all_school_models[50].school_course.all()[0].name
(Pdb++) 'Nihongo Course One'

ν•„ν„° ꡬ성

λ‚΄κ°€ λ§Œλ“  ν•„ν„°λŠ” μƒλ‹Ήνžˆ 직관적인 것 κ°™μŠ΅λ‹ˆλ‹€.

class SchoolFilter(FilterSet):
    school_name = CharFilter(field_name="name",
                             lookup_expr="icontains")
    # ---
    course_name = CharFilter(field_name="school_course__name",
                             lookup_expr="icontains")
    course_description = CharFilter(field_name="school_course__description",
                                    lookup_expr="icontains")

    class Meta:
        model = SchoolModel
        fields = [
            "school_name",
            # ---
            "course_name",
            "course_description"
        ]

문제

κ·ΈλŸ¬λ‚˜ 이것은 μž‘λ™ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‚΄κ°€ 이 μš”μ²­μ„ ν•˜λ©΄:
{{API_URL}}/api/v1/schools/?course_name=Nihongo

λ‚˜λŠ” 이것을 μ‘λ‹΅μœΌλ‘œ 되돌렀 λ°›λŠ”λ‹€.

{
    "schools": []
}

질문

λ‚΄κ°€ μ—¬κΈ°μ„œ 뭘 잘λͺ»ν•˜κ³  μžˆλ‹ˆ?

λ‚΄κ°€ μž₯κ³  ν•„ν„°λ₯Ό 잘λͺ» ν•΄μ„ν•˜κ³  μžˆμŠ΅λ‹ˆκΉŒ? 데이터 λͺ¨λΈμ„ μ—°κ²°ν•˜λŠ” 방법을 잘λͺ» ν•΄μ„ν•˜κ³  μžˆμŠ΅λ‹ˆκΉŒ?

λͺ¨λ“  지침을 μ£Όμ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€!

κ°€μž₯ μœ μš©ν•œ λŒ“κΈ€

λ‚΄ 생각에 이것은 주어진 μ˜€ν”„μ…‹κ³Ό 관련이 μžˆμŠ΅λ‹ˆλ‹€. ν•„ν„°λ§λœ 쿼리 μ„ΈνŠΈμ—λŠ” κ°œμ²΄κ°€ 1κ°œλΏμ΄μ§€λ§Œ μš”μ²­μ€ νŽ˜μ΄μ§€λ„€μ΄ν„°μ—κ²Œ 처음 49개 ν•­λͺ©μ„ κ±΄λ„ˆλ›°λ„λ‘ μ§€μ‹œν•©λ‹ˆλ‹€. ν™•μΈν•˜μ…¨λ‹€λ©΄

print(filtered_school_models[49:])

<QuerySet []> 을 얻을 것이라고 κ°€μ •ν•©λ‹ˆλ‹€.

λ‹«κΈ°, 이것이 νŽ˜μ΄μ§€ 맀김 문제인 κ²ƒμ²˜λŸΌ λ³΄μž…λ‹ˆλ‹€.

λͺ¨λ“  10 λŒ“κΈ€

μ•ˆλ…•ν•˜μ„Έμš” @loganknechtμž…λ‹ˆλ‹€. μ˜¨μ „μ„± 검사와 λ§ˆμ°¬κ°€μ§€λ‘œ URL이 μ˜¬λ°”λ₯Έμ§€ 확인할 수 μžˆμŠ΅λ‹ˆκΉŒ? 예제 URLμ—μ„œ 쿼리 λ¬Έμžμ—΄μ€ $ ? / 둜 μ‹œμž‘ν•©λ‹ˆλ‹€.

μ•„ @rpkilby - μ£„μ†‘ν•©λ‹ˆλ‹€. Postman μ—μ„œ λ³΅μ‚¬ν•˜μ—¬ λΆ™μ—¬λ„£λŠ” 것은 제 μ‹€μˆ˜μ˜€μŠ΅λ‹ˆλ‹€. url은 사싀 {{API_URL}}/api/v1/schools/?offset=49&course_name=Nihongo 인데 ν—·κ°ˆλ¦¬μ§€ μ•Šκ²Œ offset λ₯Ό μ—†μ• κ³  λ°˜λŒ€λ‘œ ν•΄μ„œ μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€ πŸ˜‚

κ·€ν•˜μ˜ μ˜ˆλŠ” λ‚˜μ—κ²Œ λͺ…λ°±ν•œ 문제λ₯Ό μ œκΈ°ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ‚΄ μΆ”μΈ‘μœΌλ‘œλŠ” API 보기에 λ¬Έμ œκ°€ μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. DjangoFilterBackend 을 μ„€μ •/보기의 filter_backends μΆ”κ°€ν•˜μ…¨μŠ΅λ‹ˆκΉŒ? 그리고 filterset_class = SchoolFilter (μ΄μ „μ—λŠ” filter_class )λ₯Ό μ„€μ •ν•˜μ…¨μŠ΅λ‹ˆκΉŒ?

μ•ˆλ…•ν•˜μ„Έμš” @rpkilby

λ‚΄κ°€ κ°€μ§€κ³ μžˆλŠ”λ³΄κΈ° μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

class SchoolViewSet(ViewSet):
    http_method_names = ["get", "post"]

    def list(self, request):
        all_school_models = SchoolModel.objects.all()

        filtered_school_models = SchoolFilter(request.GET, queryset=all_school_models)
        # import pdb
        # pdb.set_trace()

        paginator = HeaderLinkPagination()
        current_page_results = paginator.paginate_queryset(filtered_school_models.qs,
                                                           request)

        all_schools_serializer = SchoolSerializer(current_page_results,
                                                  many=True)
        response_data = {
            "schools": all_schools_serializer.data
        }

        response_to_return = paginator.get_paginated_response(response_data)
        return response_to_return

pdb.set_trace() λŠ” λ‚΄κ°€ μœ„μ˜ 심문을 κ²Œμ‹œν•œ κ³³μž…λ‹ˆλ‹€.

DjangoFilterBackend λŠ” μ „μ—­ ꡬ성이라고 κ°€μ •ν–ˆκΈ° λ•Œλ¬Έμ— μ„€μ •ν•˜μ§€ μ•Šμ•˜κ³  단일 λμ μ—μ„œλ§Œ ν…ŒμŠ€νŠΈν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ 같은 이유둜 ν΄λž˜μŠ€μ— λŒ€ν•΄μ„œλ„ 이것을 μ„€μ •ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

SchoolModel 이름에 이 ν•„ν„°λ₯Ό μ‚¬μš©ν•  수 μžˆμ§€λ§Œ μ—­λ°©ν–₯ μ™Έλž˜ ν‚€ school_course λ₯Ό ν•„ν„°λ§ν•˜λ©΄ μž‘λ™ν•˜μ§€ μ•ŠλŠ”λ‹€λŠ” 것을 μ•„λŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€.

Gotcha - λ·°μ—μ„œ 직접 ν•„ν„° μ„ΈνŠΈλ₯Ό μƒμ„±ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이 경우 ν•„ν„° λ°±μ—”λ“œ/filterset 클래슀λ₯Ό μ„€μ •ν•  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€.

데이터가 μ˜¬λ°”λ₯Έμ§€, 였λ₯˜κ°€ μžˆλŠ”μ§€ λ˜λŠ” SQL 쿼리가 μ˜¬λ°”λ₯΄κ²Œ κ΅¬μ„±λ˜μ—ˆλŠ”μ§€ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. λ…Έλ ₯ν•˜λ‹€:

filtered_school_models = SchoolFilter(request.GET, queryset=all_school_models)
print(filtered_school_models.data)
print(filtered_school_models.errors)
print(filtered_school_models.qs)
print(str(filtered_school_models.qs.query))

@rpkilby μ—¬κΈ° λ‚΄κ°€ λ³΄λŠ” 것이 μžˆμŠ΅λ‹ˆλ‹€

<QueryDict: {'offset': ['49'], 'course_name': ['Nihongo']}>

<QuerySet [<SchoolModel: SchoolModel object (51)>]>
SELECT "piano_gym_api_schoolmodel"."id", "piano_gym_api_schoolmodel"."name", "piano_gym_api_schoolmodel"."school_board_id" FROM "piano_gym_api_schoolmodel" INNER JOIN "piano_gym_api_schoolcoursemodel" ON ("piano_gym_api_schoolmodel"."id" = "piano_gym_api_schoolcoursemodel"."school_id") WHERE UPPER("piano_gym_api_schoolcoursemodel"."name"::text) LIKE UPPER(%Nihongo%) ORDER BY "piano_gym_api_schoolmodel"."id" ASC

@rpkilby 이것은 λ‹€μŒκ³Ό 같은 λ‚΄ νŽ˜μ΄μ§€ 넀이터 κ΅¬ν˜„μ— λ¬Έμ œκ°€ μžˆλŠ” 것 κ°™μŠ΅λ‹ˆλ‹€.

class HeaderLinkPagination(LimitOffsetPagination):
    default_limit = settings.DEFAULT_LIMIT
    max_limit = settings.DEFAULT_MAX_LIMIT
    min_limit = settings.DEFAULT_MIN_LIMIT
    min_offset = settings.DEFAULT_MIN_OFFSET
    max_offset = settings.DEFAULT_MAX_OFFSET

    def get_paginated_response(self, data):
        next_url = self.get_next_link()
        previous_url = self.get_previous_link()

        links = []
        header_data = (
            (previous_url, "prev"),
            (next_url, "next"),
        )
        for url, label in header_data:
            if url is not None:
                links.append("<{}>; rel=\"{}\"".format(url, label))

        headers = {"Link": ", ".join(links)} if links else {}

        return Response(data, headers=headers)

    def paginate_queryset(self, queryset, request, view=None):

        limit = request.query_params.get("limit")
        offset = request.query_params.get("offset")

        if limit is None:
            limit = settings.DEFAULT_LIMIT

        if offset is None:
            offset = settings.DEFAULT_OFFSET

        limit = int(limit)
        if limit > self.max_limit:
            error_message = ("Limit should be less than or equal to {0}"
                             ).format(self.max_limit)
            errors = {"limit": [error_message]}
            raise ValidationError(errors)
        elif limit < self.min_limit:
            error_message = ("Limit should be greater than or equal to {0}"
                             ).format(self.min_limit)
            errors = {"limit": [error_message]}
            raise ValidationError(errors)

        offset = int(offset)
        if offset > self.max_offset:
            error_message = ("Offset should be less than or equal to {0}"
                             ).format(self.max_offset)
            errors = {"offset": [error_message]}
            raise ValidationError(errors)
        elif offset < self.min_offset:
            error_message = ("Offset should be greater than or equal to {0}"
                             ).format(self.min_offset)
            errors = {"offset": [error_message]}
            raise ValidationError(errors)
        import pdb
        pdb.set_trace()

        return super(self.__class__, self).paginate_queryset(queryset, request, view)

해결책이 보이면 μ•Œλ €μ£Όμ„Έμš”. λ‚΄κ°€ 이것을 떨쳐내렀고 ν•˜λŠ” λ™μ•ˆ 계속 μ§€μΌœλ΄ μ£Όμ‹­μ‹œμ˜€.

λ‚΄ 생각에 이것은 주어진 μ˜€ν”„μ…‹κ³Ό 관련이 μžˆμŠ΅λ‹ˆλ‹€. ν•„ν„°λ§λœ 쿼리 μ„ΈνŠΈμ—λŠ” κ°œμ²΄κ°€ 1κ°œλΏμ΄μ§€λ§Œ μš”μ²­μ€ νŽ˜μ΄μ§€λ„€μ΄ν„°μ—κ²Œ 처음 49개 ν•­λͺ©μ„ κ±΄λ„ˆλ›°λ„λ‘ μ§€μ‹œν•©λ‹ˆλ‹€. ν™•μΈν•˜μ…¨λ‹€λ©΄

print(filtered_school_models[49:])

<QuerySet []> 을 얻을 것이라고 κ°€μ •ν•©λ‹ˆλ‹€.

λ‹«κΈ°, 이것이 νŽ˜μ΄μ§€ 맀김 문제인 κ²ƒμ²˜λŸΌ λ³΄μž…λ‹ˆλ‹€.

μ•ˆλ…•ν•˜μ„Έμš” @rpkilby

이 이야기λ₯Ό ν•΄μ£Όμ…”μ„œ 정말 κ°μ‚¬ν•˜λ‹€λŠ” 말씀을 λ“œλ¦¬κ³  μ‹ΆμŠ΅λ‹ˆλ‹€. django-filter λ¬Έμ œλ„ μ•„λ‹ˆμ—ˆμŠ΅λ‹ˆλ‹€. μ €λŠ” κ·Έλƒ₯ λ”©κ΅¬μŠ€μ˜€μŠ΅λ‹ˆλ‹€. 당신은 λ‚˜λ₯Ό 고무 더킹 슈퍼 슈퍼 κ°μ‚¬ν•©λ‹ˆλ‹€.

당신은 ν›Œλ₯­ν•©λ‹ˆλ‹€. λ‹€μ‹œ ν•œ 번 이 λ†€λΌμš΄ λΌμ΄λΈŒλŸ¬λ¦¬μ— κ°μ‚¬λ“œλ¦½λ‹ˆλ‹€!

κ±±μ • 마. 도와 쀄 μˆ˜μžˆμ–΄μ„œ 기뻐!

이 νŽ˜μ΄μ§€κ°€ 도움이 λ˜μ—ˆλ‚˜μš”?
0 / 5 - 0 λ“±κΈ‰