Django-filter: [OrderingFilter] ์งˆ๋ฌธ/๊ธฐ๋Šฅ ์š”์ฒญ: ์ฃผ๋ฌธ์„ ์œ„ํ•œ ์ž„์˜์˜ Django ORM ํ‘œํ˜„์‹ ์ง€์ • ํ—ˆ์šฉ

์— ๋งŒ๋“  2019๋…„ 01์›” 08์ผ  ยท  14์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: carltongibson/django-filter

์—ฌ๋ณด์„ธ์š”! ์šฐ์„ , ์šฐ๋ฆฌ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ Django REST Framework์™€ ํ•จ๊ป˜ ๋งค์šฐ ์œ ์šฉํ•œ ๊ฒƒ์œผ๋กœ ํŒ๋ช…๋œ ์ด ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•ด ๊ฐ์‚ฌ๋ฅผ ํ‘œํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ด€๋ จ ๋ชจ๋ธ์˜ ํ•„๋“œ๋ณ„๋กœ ์ง๋ ฌํ™”, ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ ๋ฌธ์ œ๋กœ ์–ด๋ ค์›€์„ ๊ฒช๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฒˆ ํ˜ธ์—์„œ๋Š” ์ฃผ๋ฌธ์—๋งŒ ์ง‘์ค‘ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ๋ชจ๋ธ๋กœ ๋‚ด ์˜๋„๋ฅผ ์„ค๋ช…ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

# models.py

from django.contrib.auth.models import User
from django.db import models


class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False)
    created = models.DateTimeField(null=False, blank=False, auto_now_add=True)
    submitted = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return f'Order #{self.id}'


class Product(models.Model):
    name = models.CharField(null=False, blank=False, max_length=256)
    price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)

    def __str__(self):
        return self.name


class OrderLine(models.Model):
    product = models.ForeignKey(Product, on_delete=models.PROTECT, null=False, blank=False, related_name='order_lines')
    quantity = models.IntegerField(null=False, blank=False)
    product_price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)
    total_price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)
    order = models.ForeignKey(Order, on_delete=models.CASCADE, null=False, blank=False, related_name='order_lines')

    def __str__(self):
        return f'{self.order}: {self.product.name} x{self.quantity}'

๊ทธ๋Ÿฐ ๋‹ค์Œ ํ•„ํ„ฐ๋ง ๋ฐ ์ˆœ์„œ๊ฐ€ ๊ตฌ์„ฑ๋˜์ง€ ์•Š์€ DRF ViewSet์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

# views.py

from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, OrderingFilter
from .models import Order

class OrderFilterSet(FilterSet):
    pass


class OrderViewSet(viewsets.ModelViewSet):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer  # the definition of OrderSerializer is irrelevant and not shown here
    filter_backends = (DjangoFilterBackend, OrderingFilter)
    filterset_class = OrderFilterSet
    ordering_fields = ()

    class Meta:
        model = Order

์ด ๋ฌธ์ œ์˜ ๋ฒ”์œ„์—์„œ ๋‚ด ๋ชฉํ‘œ๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋‹ค์Œ ํ•„๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์š”์ฒญ๋œ ์ฃผ๋ฌธ ๋ชฉ๋ก์„ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  1. ordering=[-]created - ์ฃผ๋ฌธ ์ƒ์„ฑ ๋‚ ์งœ๋ณ„ ์ฃผ๋ฌธ( [-] ์€ ์ฃผ๋ฌธ ์„ค๋ช…์— ๋Œ€ํ•œ ์„ ํƒ์  - ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.)
  2. ordering=[-]user - ์ฃผ๋ฌธํ•œ ์‚ฌ์šฉ์ž์˜ ์ „์ฒด ์ด๋ฆ„์œผ๋กœ ์ฃผ๋ฌธ
  3. ordering=[-]total_quantity - ์ฃผ๋ฌธํ•œ ์ œํ’ˆ์˜ ์ด ์ˆ˜๋Ÿ‰์œผ๋กœ ์ฃผ๋ฌธ

$ ViewSet #$์— ๊ตฌ์„ฑ๋œ ordering_fields ์˜ ๊ธฐ๋ณธ ์ ‘๊ทผ ๋ฐฉ์‹์€ ๋ชจ๋ธ ํ•„๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ์ฃผ๋ฌธ๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค(p.1). ๋‹คํ–‰ํžˆ๋„ https://django-filter.readthedocs.io/en/master/ref/filters.html#orderingfilter ์— ์„ค๋ช…๋œ ๋Œ€๋กœ filters.OrderingFilter ๋ฅผ ํ•˜์œ„ ๋ถ„๋ฅ˜ํ•˜๋Š” ๊ณ ๊ธ‰ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

# views.py

from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, OrderingFilter
from .models import Order


class OrderOrderingFilter(OrderingFilter):

    def __init__(self):
        super().__init__(fields={
            # example: model field
            # Order by order creation date
            'created': 'created',

            # example: expression on related model
            # Order by user full name
            'user': '',  # ???

            # example: aggregate expression
            'total_quantity': ''  # ???
        })


class OrderFilterSet(FilterSet):
    ordering = OrderOrderingFilter()


class OrderViewSet(viewsets.ModelViewSet):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer  # the definition of OrderSerializer is irrelevant and not shown here
    filter_backends = (DjangoFilterBackend,)
    filterset_class = OrderFilterSet

    class Meta:
        model = Order

๊ทธ๋Ÿฌ๋‚˜ ์ด ๊ณ ๊ธ‰ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ django-filter์—๋Š” ํ•„๋“œ ์ด๋ฆ„์ด ํ•„์š”ํ•œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์ œ๋ฅผ ์šฐํšŒํ•˜๋Š” ์œ ์ผํ•œ ๋ฐฉ๋ฒ•์€ ํ•„ํ„ฐ๋ง๋œ QuerySet์„ annotate 'ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

# ... the rest is omitted to brevity ...
class OrderOrderingFilter(OrderingFilter):

    def __init__(self):
        super().__init__(fields={
            # ... the rest is omitted to brevity ...
            'user_order': 'user',
        })

    def filter(self, qs, value):
        if value:
            qs = self._annotate(qs)
        return super().filter(qs, value)

    def _annotate(self, qs, value):
        if 'user' in value or '-user' in value:
            qs = qs.annotate(user_order=functions.Concat(
                models.F('user__first_name'),
                models.Value(' '),
                models.F('user__last_name'),
            ))
        return qs

์–ด๋–ค ์‚ฌ๋žŒ๋“ค์€ https://docs.djangoproject.com/en/2.1 ์— ์„ค๋ช…๋œ ๋Œ€๋กœ .annotate ์™€ .filter ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ํ˜ผํ•ฉํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์—ฌ๊ธฐ์„œ .annotate ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋”์ฐํ•œ ์ƒ๊ฐ์ด๋ผ๊ณ  ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. /topics/db/aggregation/#order -of-annotate-and-filter-clauses. ๊ทธ๋Ÿฌ๋‚˜ .annotate ํ˜ธ์ถœ์—์„œ ์ง‘๊ณ„๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ํ•œ ๊ดœ์ฐฎ์œผ๋ฏ€๋กœ ์ง€๊ธˆ๊นŒ์ง€๋Š” ๊ดœ์ฐฎ์Šต๋‹ˆ๋‹ค. (_๋‚˜๋Š” ์‹ค์ œ๋กœ ๋‚˜์ค‘์— ์ง‘๊ณ„๋กœ ๋Œ์•„๊ฐˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค_)

์ตœ์‹  ์ฝ”๋“œ ์ƒ˜ํ”Œ์˜ ๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ๋Š” ์ฝ”๋“œ๊ฐ€ ๋ฐ˜๋ณต์ ์ด๋ฉฐ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ƒ์†๋  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ณธ ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚ซ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ง€๊ธˆ์€ django-filter์—์„œ ์ œ๊ณตํ•˜๋Š” ์™„์ „ํ•œ ๋Œ€์•ˆ์ธ OrderingFilter ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ์†Œ์Šค ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. django-filter์—์„œ ๋งŽ์€ ๊ฒƒ์„ ์ฐจ์šฉํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import typing

from django.db import models
from django.db.models import expressions
from django.forms.utils import pretty_name
from django.utils.translation import ugettext_lazy as _

from django_filters import filters
from django_filters.constants import EMPTY_VALUES

__all__ = ('OrderingFilter',)


class OrderingFilter(filters.BaseCSVFilter, filters.ChoiceFilter):
    """
    An alternative to django_filters.filter.OrderingFilter that allows to specify any Django ORM "expression" for ordering.

    Usage examples:

      class MyOrderingFilter(ddf.OrderingFilter):
        def __init__(self):
          super().__init__(fields={

            # a model field
            'id': 'id'

            # an expression
            'published_by':
              functions.Concat(
                expressions.F(f'published_user__first_name'),
                expressions.Value(' '),
                expressions.F(f'published_user__last_name')
              ),

            # a complete field descriptor with custom field label
            'reported_by': {
              'label': 'Reporter',
              'desc_label': 'Reporter (descending)',  # optional, would be derived from 'label' anyway
              'expr': functions.Concat(
                expressions.F(f'reported_user__first_name'),
                expressions.Value(' '),
                expressions.F(f'reported_user__last_name')
              ),
            }
          })

    For more information about expressions, please see the official Django documentation at
    https://docs.djangoproject.com/en/2.1/ref/models/expressions/
    """

    _fields: typing.Mapping[str, 'FieldDescriptor']

    def __init__(self, fields: typing.Mapping[str, typing.Any]):
        self._fields = normalize_fields(fields)
        super().__init__(choices=build_choices(self._fields))

    # <strong i="15">@override</strong>
    def filter(self, qs: models.QuerySet, value: typing.Union[typing.List[str], None]):
        return qs if value in EMPTY_VALUES else qs.order_by(*(expr for expr in map(self.get_ordering_expr, value)))

    def get_ordering_expr(self, param) -> expressions.Expression:
        descending = param.startswith('-')
        param = param[1:] if descending else param
        field_descriptor = self._fields.get(param)
        return None if field_descriptor is None else \
            field_descriptor.expr if not descending else field_descriptor.expr.desc()


def normalize_fields(fields: typing.Mapping[str, typing.Any]) -> typing.Mapping[str, 'FieldDescriptor']:
    return dict((
        param_name,
        FieldDescriptor(param_name, {'expr': normalize_expr(field)} if isinstance(field, (str, expressions.Expression)) else field)
    ) for param_name, field in fields.items())


def normalize_expr(expr: typing.Union[str, expressions.Expression]):
    return models.F(expr) if isinstance(expr, str) else expr


descending_fmt = _('%s (descending)')


class FieldDescriptor:
    expr: models.Expression

    def __init__(self, param_name: str, data: typing.Mapping[str, typing.Any]):
        self.expr = normalize_expr(data['expr'])
        self.label = data.get('label', _(pretty_name(param_name)))
        self.desc_label = data.get('desc_label', descending_fmt.format(self.label))


def build_choices(fields: typing.Mapping[str, 'FieldDescriptor']):
    choices = []
    for param_name, field_descriptor in fields.items():
        choices.append((param_name, field_descriptor.label))
        choices.append((f'-{param_name}', field_descriptor.desc_label))
    return choices

์ด ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด OrdersOrderingFilter ๊ฐ€ ๋ฉ‹์ง€๊ณ  ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค.

# ... the rest is omitted to brevity ...
class OrderOrderingFilter(ddf.OrderingFilter):

    def __init__(self):
        super().__init__(fields={
            # ... the rest is omitted to brevity ...
            'user': functions.Concat(
                models.F('user__first_name'),
                models.Value(' '),
                models.F('user__last_name'),
            ),
        })

์ด๊ฒƒ์€ ๋‚ด๊ฐ€ ์—ฌ๊ธฐ์— ์˜จ ์งˆ๋ฌธ์„ ์ œ๊ธฐํ•ฉ๋‹ˆ๋‹ค.

_ django_filters.rest_framework.OrderingFilter ์—์„œ Django ORM ํ‘œํ˜„์‹์œผ๋กœ ์ˆœ์„œ๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์— ๋Œ€ํ•ด ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์‹ญ๋‹ˆ๊นŒ?_

๊ทธ๋Ÿฌ๋‚˜ ๋Œ€๋‹ตํ•˜๊ธฐ ์ „์— ์šฐ๋ฆฌ๋Š” 1, 2ํŽ˜์ด์ง€๋งŒ ํ’€์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์ƒ๊ธฐ์‹œ์ผœ ๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. p.3์„ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ์ง‘๊ณ„ ํ‘œํ˜„์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

aggregates.Sum(models.F('order_lines__quantity'))

.annotate ์—์„œ๋Š” ์ด ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. QuerySet ํ•„ํ„ฐ๋ง๊ณผ ํ˜ผํ•ฉํ•œ ๊ฒฐ๊ณผ๋ฅผ ์˜ˆ์ธกํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. Query.order(...) ํ˜ธ์ถœ์—์„œ๋„ ์ด ํ‘œํ˜„์‹์„ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

django.core.exceptions.FieldError: Using an aggregate in order_by() without also including it in annotate() is not allowed: Sum(F(order_lines__quantity))

๋”ฐ๋ผ์„œ p.3์€ ๋„๋‹ฌํ•  ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ViewSet๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์—ฌ ๋ชจ๋“  ๊ฒƒ์„ ๊ด€ํ†ตํ•˜๋Š” ์†”๋ฃจ์…˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Django-filters๋ณด๋‹ค Django REST Framework์™€ ๋” ๊ด€๋ จ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ง‘๊ณ„ ํ‘œํ˜„์‹ ์ง€์› ์—†์ด django_filters.rest_framework.OrderingFilter ์—์„œ ํ‘œํ˜„์‹ ์ง€์›์ด ์‹ค์ œ๋กœ ํ•„์š”ํ•ฉ๋‹ˆ๊นŒ? ์ด๊ฒƒ์€ ๋งŽ์€ ์ด์ ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  django-filter ์‚ฌ์šฉ์ž๋ฅผ ํ˜ผ๋ž€์Šค๋Ÿฝ๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ท€ํ•˜์˜ ์˜๊ฒฌ์„ ๊ธฐ๋‹ค๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์†Œํ™”ํ•  ์ •๋ณด๊ฐ€ ๋งŽ๋‹ค๋Š” ๊ฒƒ์„ ์••๋‹ˆ๋‹ค. ๋‚ด ํ…Œ์ŠคํŠธ ํ”„๋กœ์ ํŠธ๊ฐ€ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. https://github.com/earshinov/django_sample/tree/master/django_sample/ordering_by_expression

ImprovemenFeature

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

ach - ์ด์— ๋Œ€ํ•œ ์‘๋‹ต์„ ์‹œ์ž‘ํ–ˆ์ง€๋งŒ ๋‚ด ์ปดํ“จํ„ฐ๊ฐ€ kaput ์ƒํƒœ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์— ์ œ์•ˆ๋œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํ•ฉ๋ฆฌ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. model_field - parameter_name ์Œ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ชจ๋ธ์—์„œ ๋…ธ์ถœ๋œ params/forms/etc๋ฅผ ํŒŒ์ƒํ•˜๋ฏ€๋กœ ๋‹น์‹œ์—๋Š” ์˜๋ฏธ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ ์ด๊ฒƒ์ด ํ•„์š”ํ•œ ์ด์œ ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋งคํ•‘์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด ์˜๋ฏธ๊ฐ€ ์žˆ๊ณ  ๋” ๋ณต์žกํ•œ ํ‘œํ˜„์‹์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์‚ฌ์šฉ ์ค‘๋‹จ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ์–ด๋ ค์šธ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์œผ๋ฉฐ ๊ธฐ๊บผ์ด ๋„์™€๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ fields ๋ฐ field_labels ๋Š” params ๋ฐ param_labels ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. ์ด์ „ ๋ฒ„์ „๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ์ด์ „ ์ธ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์‚ฌ์šฉ ์ค‘๋‹จ ์•Œ๋ฆผ์„ ํ‘œ์‹œํ•˜๋ฉด์„œ ํ•˜๋‚˜์—์„œ ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ์ถฉ๋ถ„ํžˆ ์‰ฝ์Šต๋‹ˆ๋‹ค.

ํ•œ ๊ฐ€์ง€ ๊ณ ๋ คํ•ด์•ผ ํ•  ์‚ฌํ•ญ์€ ๋‚ด๋ฆผ์ฐจ์ˆœ ๋Œ€์†Œ๋ฌธ์ž( param vs -param )์— ๋Œ€ํ•œ ๋ณต์žกํ•œ ํ‘œํ˜„์‹์˜ ์ž๋™ ๋ณ€ํ™˜์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์˜ค๋ฆ„์ฐจ์ˆœ์ด .asc(nulls_last=True) ์ธ ๊ฒฝ์šฐ, ์ด๊ฒƒ์˜ ์—ญํ•จ์ˆ˜๋Š” .desc(nulls_first=True) ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๊นŒ, ์•„๋‹ˆ๋ฉด ์ •๋ ฌ ๋ฐฉํ–ฅ์— ๊ด€๊ณ„์—†์ด null์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ๋‚จ์•„ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

๋ชจ๋“  14 ๋Œ“๊ธ€

์•ˆ๋…•ํ•˜์„ธ์š” @earshinov์ž…๋‹ˆ๋‹ค. ์‹ ๊ณ ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๋งค์šฐ ํฅ๋ฏธ๋กœ์šด. ๋‚ด๊ฐ€ ๊ทธ๊ฒƒ์— ๋Œ€ํ•ด ์ƒ๊ฐํ•˜์ž. ๊ทธ๋•Œ๊นŒ์ง€ ์‘๋‹ตํ•˜์ง€ ์•Š์œผ๋ฉด 3์›”์— ์ €์—๊ฒŒ Ping์„ ๋ณด๋‚ด์ฃผ์‹ญ์‹œ์˜ค. ๐Ÿ™‚

๋˜ ๋‹ค๋ฅธ ์•„์ด๋””์–ด: ํ•œ ๋‹จ๊ณ„ ๋” ๋‚˜์•„๊ฐ€ ํ•„ํ„ฐ๊ฐ€ ํ•˜๋‚˜๊ฐ€ ์•„๋‹Œ ์—ฌ๋Ÿฌ ํ‘œํ˜„์‹ ๋˜๋Š” ํ•„๋“œ ์ด๋ฆ„์„ ์ง€์ •ํ•˜๋„๋ก ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ „(์˜ต์…˜ 1):

class OrderOrderingFilter(ddf.OrderingFilter):

    def __init__(self):
        super().__init__(fields={
            'user': functions.Concat(
                models.F('user__first_name'),
                models.Value(' '),
                models.F('user__last_name'),
            ),
        })

์ดํ›„(์˜ต์…˜ 2):

class OrderOrderingFilter(ddf.OrderingFilter):

    def __init__(self):
        super().__init__(fields={
            'user': ('user__first_name', 'user__last_name'),
        })

๋‘ ๊ฒฝ์šฐ ๋ชจ๋‘ ordering=user ๋Š” ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„์„ ๋จผ์ €, ์‚ฌ์šฉ์ž์˜ ๋‘ ๋ฒˆ์งธ ์ด๋ฆ„์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ •๋ ฌํ•˜์ง€๋งŒ ์˜ต์…˜ 2๋Š” ์“ฐ๊ธฐ๊ฐ€ ๋” ์‰ฝ๊ณ  DB ์ฟผ๋ฆฌ์™€ ๊ด€๋ จํ•˜์—ฌ ๋” ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ˆ๋…•ํ•˜์„ธ์š” @carltongibson ,

Match์—์„œ ํ•‘์„ ์š”์ฒญํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฒŒ์จ 5์›”์ž…๋‹ˆ๋‹ค :)

ํ‘œํ˜„์‹(ํ•˜๋‚˜ ๋˜๋Š” ์—ฌ๋Ÿฌ ๊ฐœ)์— ์˜ํ•œ ์ˆœ์„œ ์ง€์ •์— ๋Œ€ํ•œ ์ œ ์ œ์•ˆ์œผ๋กœ ๋Œ์•„๊ฐ€์„œ, ์ €๋Š” ์ด๊ฒƒ์ด ํผ์ฆ์˜ ๋ˆ„๋ฝ๋œ ๋ถ€๋ถ„์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋„ค ๊ฐ€์ง€ ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ž‘์—…์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
ใ…. ์ง๋ ฌํ™”
๋น„. ~ํŽ˜์ด์ง€ ๋งค๊น€~(์ด ํ† ๋ก ๊ณผ ๊ด€๋ จ์ด ์—†์Œ)
์”จ. ํ•„ํ„ฐ๋ง
๋””. ์ •๋ ฌ

DRF ๋ฐ django-filter๋ฅผ ์‚ฌ์šฉ ํ•˜๋ฉด ์ •๋ ฌ์„ ์ œ์™ธํ•˜๊ณ  ๊ด€๋ จ ๋ชจ๋ธ์„ ์ง€์›ํ•˜์—ฌ ์ด๋Ÿฌํ•œ ๋ชจ๋“  ๊ฒƒ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ใ…. ์ง๋ ฌํ™”๋ฅผ ์œ„ํ•ด ์ค‘์ฒฉ๋œ ์ง๋ ฌ ๋ณ€ํ™˜๊ธฐ ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ ์ง‘๊ณ„๋œ ํ•„๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ( count ์ƒ๊ฐ) ์ฟผ๋ฆฌ ์ง‘ํ•ฉ์„ .annotate() ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from django.db.models import aggregates, functions, F, Value

class ModelViewSet(...):

    def get_queryset(self):
        qs = Model.objects.all()

        if self.action == 'list':
            qs = qs.annotate(
                author_full_name=functions.Trim(functions.Concat(
                    F('author__first_name'),
                    Value(' '),
                    F('author__last_name'),
                )),
                submodel_count=aggregates.Count('submodel'))
            )

        return qs

์”จ. Django ํ‘œํ˜„์‹์œผ๋กœ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ์ž ์ •์˜ ํ•„ํ„ฐ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(์•„๋ž˜ ์˜ˆ ์ฐธ์กฐ). ์ผ๋ฐ˜ ์Šค์นผ๋ผ ํ•„ํ„ฐ( filters.NumberFilter )๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ง‘๊ณ„ ํ•„๋“œ( submodel_count )๋กœ ํ•„ํ„ฐ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

from django_filters import filters
from django_filters.rest_framework import FilterSet

class ModelFilterSet(FilterSet):

    author = UserFilter(field_name='author', label='author', lookup_expr='icontains')


class UserFilter(filters.Filter):
    """A django_filters filter that implements filtering by user's full name.

    def filter(self, qs: QuerySet, value: str) -> QuerySet:
        # first_name <lookup_expr> <value> OR last_name <lookup_expr> <value>
        return qs if not value else qs.filter(
            Q(**{f'{self.field_name}__first_name__{self.lookup_expr}': value}) |
            Q(**{f'{self.field_name}__last_name__{self.lookup_expr}': value})
        )

๋””. ์ •๋ ฌ์— ๋Œ€ํ•œ ํ•ด๊ฒฐ์ฑ…์€ ์—†์Šต๋‹ˆ๋‹ค :-(

๋‹ค์Œ์€ ์ง€๊ธˆ๊นŒ์ง€ ์‚ฌ์šฉํ•ด์˜จ ์‚ฌ์šฉ์ž ์ •์˜ OrderingFilter ์˜ ์™„์ „ํ•œ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

class OrderingFilter(filters.BaseCSVFilter, filters.ChoiceFilter):
    """An alternative to :class:`django_filters.filters.OrderingFilter` that allows to specify any Django ORM expression for ordering.

    Usage example:

    .. code-block:: python

      from django.db import models
      from django.db.models import aggregates, expressions, fields

      import ddl

      class OrderOrderingFilter(ddl.OrderingFilter):
        def __init__(self):
          super().__init__(fields={

            # a model field
            'created': 'created'

            # an expression
            'submitted': expressions.ExpressionWrapper(
                models.Q(submitted_date__isnull=False),
                output_field=fields.BooleanField()
            ),

            # multiple fields or expressions
            'user': ('user__first_name', 'user__last_name'),

            # a complete field descriptor with custom field label
            'products': {
              'label': 'Total number of items in the order',
              # if not specified, `desc_label` would be derived from 'label' anyway
              'desc_label': 'Total number of items in the order (descending)',
              'expr': aggregates.Sum('order_lines__quantity'),
              # it is also possible to filter by multiple fields or expressions here
              #'exprs': (...)
            },
          })

    For more information about expressions, see the official Django documentation at
    https://docs.djangoproject.com/en/dev/ref/models/expressions/
    """

    _fields: typing.Mapping[str, 'FieldDescriptor']

    def __init__(self, fields: typing.Mapping[str, typing.Any]):
        self._fields = normalize_fields(fields)
        super().__init__(choices=build_choices(self._fields))

    # <strong i="7">@override</strong>
    def filter(self, qs: models.QuerySet, value: typing.Union[typing.List[str], None]):
        return qs if value in EMPTY_VALUES else qs.order_by(*(itertools.chain(*(self.__get_ordering_exprs(param) for param in value))))

    def __get_ordering_exprs(self, param) -> typing.Union[None, typing.List[expressions.Expression]]:
        descending = param.startswith('-')
        param = param[1:] if descending else param
        field_descriptor = self._fields.get(param)
        return () if field_descriptor is None else \
            field_descriptor.exprs if not descending else \
            (expr.desc() for expr in field_descriptor.exprs)



def normalize_fields(fields: typing.Mapping[str, typing.Any]) -> typing.Mapping[str, 'FieldDescriptor']:
    return dict((
        param_name,
        FieldDescriptor(param_name, field if isinstance(field, collections.Mapping) else {'exprs': normalize_exprs(field)})
    ) for param_name, field in fields.items())

def normalize_exprs(exprs: typing.Union[
        typing.Union[str, expressions.Expression],
        typing.List[typing.Union[str, expressions.Expression]]
    ]) -> typing.List[expressions.Expression]:
    # `exprs` is either a single expression or a Sequence of expressions
    exprs = exprs if isinstance(exprs, collections.Sequence) and not isinstance(exprs, str) else (exprs,)
    return [normalize_expr(expr) for expr in exprs]

def normalize_expr(expr: typing.Union[str, expressions.Expression]) -> expressions.Expression:
    return models.F(expr) if isinstance(expr, str) else expr


descending_fmt = _('%s (descending)')


class FieldDescriptor:
    exprs: typing.List[models.Expression]

    def __init__(self, param_name: str, data: typing.Mapping[str, typing.Any]):
        exprs = data.get('exprs') or data.get('expr')
        if not exprs:
            raise ValueError("Expected 'exprs' or 'expr'")
        self.exprs = normalize_exprs(exprs)
        self.label = data.get('label', _(pretty_name(param_name)))
        self.desc_label = data.get('desc_label', descending_fmt.format(self.label))


def build_choices(fields: typing.Mapping[str, 'FieldDescriptor']):
    choices = []
    for param_name, field_descriptor in fields.items():
        choices.append((param_name, field_descriptor.label))
        choices.append((f'-{param_name}', field_descriptor.desc_label))
    return choices

_์—…๋ฐ์ดํŠธ_: normalize_fields ๋ฐ ๊ธฐํƒ€ ๋„์šฐ๋ฏธ ๋ฉ”์„œ๋“œ์˜ ๊ตฌํ˜„์ด ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ข‹์€. ๐Ÿ˜€

ํ•‘ ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

Tbh ๋‚˜๋Š” ์ด๊ฒƒ์— ๋Œ€ํ•ด ์ƒ๊ฐํ•  ์‹œ๊ฐ„์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์—์„œ ๋‹น์‹ ์˜ ๋ชจ๋“  ํ›Œ๋ฅญํ•œ ์ƒ๊ฐ์—์„œ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ์ž‘์€ ๋ณ€ํ™”์— ๋Œ€ํ•œ ์ œ์•ˆ์ด ์žˆ์Šต๋‹ˆ๊นŒ? (์•„๋งˆ๋„ PR๋กœ ์•ž์œผ๋กœ ๋‚˜์•„๊ฐ€๋Š” ๊ฒƒ์ด ๋” ์‰ฌ์šธ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.)

์˜ˆ, ๊ทธ๋Ÿฌํ•œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ด์ „ ๋ฒ„์ „๊ณผ ํ˜ธํ™˜๋˜๋„๋ก ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์ฆ‰์‹œ ๋ช…ํ™•ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ƒ๊ฐํ•ด ๋ด์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ณง PR์„ ๊ธฐ๋Œ€ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค(์ธํ„ฐ๋„ท์ด ๋ถ€์กฑํ•œ ๊ณณ์—์„œ ํœด๊ฐ€๋ฅผ ๊ฐˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค).

๊ดœ์ฐฎ์•„. ๐Ÿ™‚

์„œ๋‘๋ฅผ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์ƒ๊ฐํ•ด ๋ณด๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

@carltongibson , ์ข‹์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์•ฝ๊ฐ„ ์ƒ๊ฐํ–ˆ์ง€๋งŒ OrderingFilter ์˜ ์ด์ „ ๊ตฌํ˜„๊ณผ ์ƒˆ ๊ตฌํ˜„์„ ๊ฒฐํ•ฉํ•˜๋Š” ์ ์ ˆํ•œ ๋ฐฉ๋ฒ•์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  ๊ฒƒ์„ ํ•˜๋‚˜์˜ ํด๋ž˜์Šค์— ๋„ฃ์œผ๋ ค๋Š” ๊ฒฝ์šฐ ๊ฐ€์žฅ ํฐ ๋ฌธ์ œ๋Š” ์ด์ „ ๊ตฌํ˜„์—์„œ dict ํ•„๋“œ๊ฐ€ model_field - parameter_name ์Œ์„ ์ €์žฅํ•˜๋Š” ๋ฐ˜๋ฉด ์ƒˆ ๊ตฌํ˜„์—์„œ๋Š” ๋ฐ˜๋Œ€ parameter_name - model_field ( model_field ๋Œ€์‹ ์— ํ‘œํ˜„์‹์„ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๊ฐ’์—๋งŒ ์ €์žฅํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์‚ฌ์ „์˜ ํ‚ค์—๋Š” ์ €์žฅํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ฐ˜๋“œ์‹œ ๊ทธ๋ž˜์•ผ ํ•จ) .

๊ธฐ์ˆ ์ ์œผ๋กœ ์‚ฌ์ „์— ๋ฌธ์ž์—ด๋งŒ ํฌํ•จ๋œ ๊ฒฝ์šฐ "์ด์ „ ๋ฐฉ์‹์œผ๋กœ" ํ•ด์„ํ•˜๊ณ  ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ "์ƒˆ๋กœ์šด ๋ฐฉ์‹์œผ๋กœ" ํ•ด์„ํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ๊ทน๋ณตํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์‚ฌ์šฉ์ž๋Š” ํ‘œํ˜„์‹์„ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•ด์•ผ ํ•  ๋•Œ ์‚ฌ์ „ ํ•ญ๋ชฉ์„ "๋’ค์ง‘์–ด์•ผ" ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ˆœ์„œ ํ‘œํ˜„์ด ์ œ๊ฑฐ๋˜๋ฉด ์‚ฌ์ „ ํ•ญ๋ชฉ์„ "๋’ค์ง‘๊ธฐ"์— ์ฃผ์˜ํ•˜์‹ญ์‹œ์˜ค... ์ด๊ฒƒ์€ ๋‚˜์—๊ฒŒ ๋”์ฐํ•œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ฒ˜๋Ÿผ ๋“ค๋ฆฝ๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ์€ ๋˜ํ•œ ๊ตฌํ˜„์„ ๋ณต์žกํ•˜๊ฒŒ ๋งŒ๋“ค ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ExpressionOrderingFilter ์™€ ๊ฐ™์ด ๋‹ค๋ฅธ ์ด๋ฆ„์œผ๋กœ ์ƒˆ OrderingFilter๋ฅผ ์ด์ „ ๊ฒƒ๊ณผ ํ•จ๊ป˜ ๋„์ž…ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹ญ๋‹ˆ๊นŒ?

๋‚ด๊ฐ€ ํ•„์š”ํ–ˆ๋˜ ๋ฐ”๋กœ ๊ทธ ์ˆœ๊ฐ„์— ํ›Œ๋ฅญํ•ฉ๋‹ˆ๋‹ค! @earshinov ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!

์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”?

MyModel.objects.all().order_by(F('price').desc(nulls_last=True))

์ฃผ๋ฌธ ํ•„ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?

    o = ExpressionOrderingFilter(

        fields={
                'price': F('price').desc(nulls_last=True)
        }

    )

์ž˜ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค!
์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ์—ฌ๋Ÿฌ ํ•„ํ„ฐ(ak)๋ฅผ ๊ฒฐํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋‚ด๋Š” ๊ฒƒ๋ฟ์ž…๋‹ˆ๋‹ค. '๊ฐ€๊ฒฉ'๊ณผ '์ฃผ์‹'์„ ํ•˜๋‚˜๋กœ.

ach - ์ด์— ๋Œ€ํ•œ ์‘๋‹ต์„ ์‹œ์ž‘ํ–ˆ์ง€๋งŒ ๋‚ด ์ปดํ“จํ„ฐ๊ฐ€ kaput ์ƒํƒœ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์— ์ œ์•ˆ๋œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํ•ฉ๋ฆฌ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. model_field - parameter_name ์Œ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ๋ชจ๋ธ์—์„œ ๋…ธ์ถœ๋œ params/forms/etc๋ฅผ ํŒŒ์ƒํ•˜๋ฏ€๋กœ ๋‹น์‹œ์—๋Š” ์˜๋ฏธ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ ์ด๊ฒƒ์ด ํ•„์š”ํ•œ ์ด์œ ๋Š” ์—†์Šต๋‹ˆ๋‹ค. ๋งคํ•‘์„ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด ์˜๋ฏธ๊ฐ€ ์žˆ๊ณ  ๋” ๋ณต์žกํ•œ ํ‘œํ˜„์‹์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์‚ฌ์šฉ ์ค‘๋‹จ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์—„์ฒญ๋‚˜๊ฒŒ ์–ด๋ ค์šธ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์œผ๋ฉฐ ๊ธฐ๊บผ์ด ๋„์™€๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ fields ๋ฐ field_labels ๋Š” params ๋ฐ param_labels ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. ์ด์ „ ๋ฒ„์ „๊ณผ์˜ ํ˜ธํ™˜์„ฑ์„ ์œ ์ง€ํ•˜๊ณ  ์ด์ „ ์ธ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ ์‚ฌ์šฉ ์ค‘๋‹จ ์•Œ๋ฆผ์„ ํ‘œ์‹œํ•˜๋ฉด์„œ ํ•˜๋‚˜์—์„œ ๋‹ค๋ฅธ ๊ฒƒ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ์ถฉ๋ถ„ํžˆ ์‰ฝ์Šต๋‹ˆ๋‹ค.

ํ•œ ๊ฐ€์ง€ ๊ณ ๋ คํ•ด์•ผ ํ•  ์‚ฌํ•ญ์€ ๋‚ด๋ฆผ์ฐจ์ˆœ ๋Œ€์†Œ๋ฌธ์ž( param vs -param )์— ๋Œ€ํ•œ ๋ณต์žกํ•œ ํ‘œํ˜„์‹์˜ ์ž๋™ ๋ณ€ํ™˜์ž…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์˜ค๋ฆ„์ฐจ์ˆœ์ด .asc(nulls_last=True) ์ธ ๊ฒฝ์šฐ, ์ด๊ฒƒ์˜ ์—ญํ•จ์ˆ˜๋Š” .desc(nulls_first=True) ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๊นŒ, ์•„๋‹ˆ๋ฉด ์ •๋ ฌ ๋ฐฉํ–ฅ์— ๊ด€๊ณ„์—†์ด null์ด ๋งˆ์ง€๋ง‰์œผ๋กœ ๋‚จ์•„ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๊นŒ?

์ข‹์•„, ์Šˆํผ @rpkilby.

์šฐ๋ฆฌ๊ฐ€ ์—ฌ๊ธฐ์„œ ์•ž์œผ๋กœ ๋‚˜์•„๊ฐ€๋Š” ๊ฒƒ์„ ๋ณด๊ฒŒ ๋˜์–ด ๊ธฐ์ฉ๋‹ˆ๋‹ค. ๋‚˜์˜ ์ฃผ์š” ๊ด€์‹ฌ์‚ฌ๋Š” ์šฐ๋ฆฌ๊ฐ€ ํ•˜๋Š” ์ผ์„ ์ œ๋Œ€๋กœ ๋ฌธ์„œํ™”ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์ด๋ฏธ df ๋ฌธ์„œ๋ฅผ ์กฐ๊ธˆ _๊ฐ„๋‹จํ•˜๊ฒŒ_ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. ๐Ÿ™‚ โ€” API๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋˜์–ด ๊ธฐ์˜์ง€๋งŒ ๋ช…ํ™•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ ์ผ์€ ํ•œ ์‚ฌ๋žŒ์˜ ๋…ธ๋ ฅ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

@rpkilby , @carltongibson , ๋‚ด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐ ๋„์›€์ด ํ•„์š”ํ•˜๋ฉด ์‹œ๊ฐ„์„ ํ• ์• ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ฐฉํ–ฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์–ด๋””์„œ ์–ด๋–ป๊ฒŒ ์‹œ์ž‘ํ•ฉ๋‹ˆ๊นŒ?

์•ˆ๋…•ํ•˜์„ธ์š” @earshinov์ž…๋‹ˆ๋‹ค. ๋ฌธ์„œ ์ดˆ์•ˆ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ํ•˜๋Š” ์ด์•ผ๊ธฐ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ๊ฑฐ๊ธฐ์—์„œ ์ฝ”๋“œ๊ฐ€ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. PR, ๋น„๋ก ๋‹จ์ง€ ์ดˆ์•ˆ์ด ์šฐ๋ฆฌ์—๊ฒŒ ์ด์•ผ๊ธฐํ•  ๋ฌด์–ธ๊ฐ€๋ฅผ ์ œ๊ณตํ•˜๋”๋ผ๋„.

๋˜ํ•œ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ ์žˆ์œผ๋ฉด ํ…Œ์ŠคํŠธํ•˜์‹ญ์‹œ์˜ค. ์ตœ์ข… ๋ณ€๊ฒฝ ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜๋„๋ก ์กฐ์ •ํ•ด์•ผ ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ๋งค์šฐ ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค(์˜ˆ: .asc ๋ฅผ .desc , ์ฒ˜๋ฆฌ nulls_fist / nulls_last ์ธ์ˆ˜ ๋“ฑ).

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰