Рефакторинг нескольких условий if с цепочкой Q () в оценке строки запроса Python / Django - PullRequest
2 голосов
/ 16 июня 2020

Я работаю над клонированием системы регистрации комнат airbnb в Django. У меня есть представление на основе классов (метод HTTP get), которое фильтрует комнаты, хранящиеся в базе данных, в соответствии с различными параметрами значения ключа строки запроса и возвращает эти комнаты. Параметры фильтра, предоставляемые через строку запроса:

    location        = request.GET.get('location')
    adults          = int(request.GET.get('adults', 0))
    children        = int(request.GET.get('children', 0))
    infants         = request.GET.get('infants', 0)
    min_cost        = float(request.GET.get('min_cost', 0))
    max_cost        = float(request.GET.get('max_cost', sys.maxsize))
    property_type   = request.GET.get('property_type', None)
    place_type      = request.GET.get('place_type', None)
    check_in        = request.GET.get('checkin', None)
    check_in_date   = datetime.datetime.strptime(check_in, '%Y-%m-%d') if check_in else None
    check_out       = request.GET.get('checkout', None)
    check_out_date  = datetime.datetime.strptime(check_out, '%Y-%m-%d') if check_out else None
    min_beds        = request.GET.get('min_beds', None)
    min_bedrooms    = request.GET.get('min_bedrooms', None)
    min_baths       = request.GET.get('min_baths', None)
    amenities       = request.GET.getlist('amenities', None)
    languages       = request.GET.getlist('languages', None)

Я решил сохранить все выражения фильтра как объекты Q (), используя операцию & =. Комнаты с уже забронированными датами и выбранными хозяином недоступными датами («заблокированная дата»), которые противоречат предоставленным check_in_date и check_out_date, будут отфильтрованы. После сохранения всех выражений Q () в переменной под названием «запросы» я передал «запросы» в качестве аргумента функции Room.objects.filter (). Параметры min_beds, min_bedrooms и min_baths были оценены после начальной фильтрации, чтобы я мог выполнить функцию annotate () для отфильтрованного набора запросов.

Следующий код работает, но мне интересно, есть ли более сжатый и эффективный способ фильтрации с точки зрения db-вызовов и временной сложности. Возможно, используя prefetch_related ()? На данный момент кажется, что существует слишком много повторяющихся операторов if, но я не мог придумать лучшего способа оценки случаев None для параметров строки запроса.

queries = (
        Q(address__icontains = location) & 
        Q(max_capacity__gte = adults + children) &
        Q(price__range = (min_cost, max_cost))
        )
    if check_in_date and check_out_date:
        queries &= (
            ~Q(blockeddate__start_date__range = (check_in_date, check_out_date)) &
            ~Q(blockeddate__end_date__range = (check_in_date, check_out_date)) &
            ~Q(booking__start_date__range = (check_in_date, check_out_date)) &
            ~Q(booking__end_date__range = (check_in_date, check_out_date))
        )
    if property_type:
        queries &= Q(property_type__name = property_type)
    if place_type:
        queries &= Q(place_type__name = place_type)
    if amenities:
        q_expressions = [Q(amenities__name = amenity) for amenity in amenities]
        for expression in q_expressions:
            queries &= expression
    if languages:
        q_expressions = [Q(host__userlanguage__language__name = language) for language in languages]
        for expression in q_expressions:
            queries &= expression 

    room_qs = Room.objects.filter(queries)
    if min_beds:
        room_qs = room_qs.annotate(num_beds=Sum('bedroom__bed__quantity')).filter(num_beds__gte = min_beds)
    if min_bedrooms:
        room_qs = room_qs.annotate(num_bedrooms=Count('bedroom')).filter(num_bedrooms__gte = min_bedrooms)
    if min_baths:
        room_qs = room_qs.annotate(num_baths=Count('bath')).filter(num_baths__gte = min_baths)

Ответы [ 2 ]

2 голосов
/ 16 июня 2020

Обратите внимание на пакет django-filters: django -фильтры

Он предоставляет FilterSet классы, охватывающие все c logi фильтрации по множеству полей в декларативном путь.

1 голос
/ 16 июня 2020

Честно говоря, при условии, что индексы существуют, я не могу видеть, возможно, не добавляя таблицы кеша и тому подобное.

Когда у вас есть набор запросов, вы можете print(queryset.query) получить SQL и записать результат в EXPLAIN .... (Плагин Django Debug Toolbar также может это делать.)

Вы можете DRY немного удалить код:

from django.db.models import Q


def list_q(queries, field, values):
    for value in values:
        queries &= Q(**{field: value})
    return queries


def annotation_filter(qs, name, aggregation, op, value):
    if value:
        return qs.annotate(**{name: aggregation}).filter(
            **{f"{name}__{op}": value}
        )
    return qs


def q(...):
    queries = (
        Q(address__icontains=location)
        & Q(max_capacity__gte=adults + children)
        & Q(price__range=(min_cost, max_cost))
    )
    if check_in_date and check_out_date:
        date_range = (check_in_date, check_out_date)
        queries &= (
            ~Q(blockeddate__start_date__range=date_range)
            & ~Q(blockeddate__end_date__range=date_range)
            & ~Q(booking__start_date__range=date_range)
            & ~Q(booking__end_date__range=date_range)
        )
    if property_type:
        queries &= Q(property_type__name=property_type)
    if place_type:
        queries &= Q(place_type__name=place_type)
    queries = list_q(queries, "amenities__name", amenities)
    queries = list_q(
        queries, "host__userlanguage__language__name", languages
    )
    room_qs = Room.objects.filter(queries)
    room_qs = annotation_filter(
        room_qs, "num_beds", Sum("bedroom__bed__quantity"), "gte", min_beds,
    )
    room_qs = annotation_filter(
        room_qs, "num_bedrooms", Count("bedroom"), "gte", min_bedrooms,
    )
    room_qs = annotation_filter(
        room_qs, "num_baths", Count("bath"), "gte", min_baths
    )
    return room_qs
...