Django-filter: [OrderingFilter] Frage / Funktionsanfrage: Erlauben Sie die Angabe eines beliebigen Django-ORM-Ausdrucks für die Bestellung

Erstellt am 8. Jan. 2019  ·  14Kommentare  ·  Quelle: carltongibson/django-filter

Hallo! Zunächst einmal möchte ich Ihnen für dieses Projekt danken, das sich in Kombination mit dem Django REST Framework in unserer Anwendung als äußerst nützlich erwiesen hat.

Derzeit kämpfe ich mit einem Problem beim Serialisieren, Filtern und Ordnen nach Feldern verwandter Modelle. In dieser Ausgabe möchte ich mich nur auf das Bestellen konzentrieren.

Lassen Sie mich meine Absichten mit dem folgenden Modell veranschaulichen:

# 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}'

Beginnen wir dann mit einem DRF-ViewSet ohne konfigurierte Filterung und Reihenfolge:

# 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

Mein Ziel im Rahmen dieser Ausgabe ist es, dem Kunden zu ermöglichen, die angeforderte Liste von Bestellungen nach diesen Feldern zu ordnen:

  1. ordering=[-]created - Bestellung nach Erstellungsdatum der Bestellung ( [-] zeigt eine optionale - Bestellbezeichnung an)
  2. ordering=[-]user - Bestellung nach dem vollständigen Namen des Benutzers, der die Bestellung aufgegeben hat
  3. ordering=[-]total_quantity - Bestellen Sie nach der Gesamtmenge der Produkte in der Bestellung

Der grundlegende Ansatz mit ordering_fields , der auf ViewSet $ konfiguriert ist, ermöglicht nur eine Bestellung basierend auf Modellfeldern, p.1. Glücklicherweise können wir eine fortgeschrittenere Methode zum Unterklassen von filters.OrderingFilter verwenden, wie sie auch in https://django-filter.readthedocs.io/en/master/ref/filters.html#orderingfilter beschrieben ist:

# 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

Aber auch bei Verwendung dieser erweiterten Methode scheint django-filter einen Feldnamen zu erfordern. Die einzige Möglichkeit, dieses Problem zu umgehen, besteht darin, das gefilterte QuerySet mit annotate zu bearbeiten:

# ... 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

Einige können sagen, dass die Verwendung .annotate hier eine schlechte Idee ist, da man .annotate nicht zuverlässig mit .filter mischen kann, wie in https://docs.djangoproject.com/en/2.1 beschrieben .annotate -Aufrufen keine Aggregation verwenden, also sind wir soweit gut. (_Ich werde später auf die Aggregation zurückkommen_)

Ein weiteres Problem mit dem neuesten Codebeispiel ist, dass sich der Code wiederholt und besser in eine Basisklasse extrahiert werden sollte, die dann bequem vererbt werden könnte. Fürs Erste habe ich meine eigene Klasse OrderingFilter geschrieben, die eine vollständige Alternative zu der von django-filter bereitgestellten ist. Der vollständige Quellcode ist unten. Sie werden vielleicht bemerken, dass es viel von django-filter ausleiht:

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

Mit dieser Klasse wird OrdersOrderingFilter schön und prägnant:

# ... 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'),
            ),
        })

Daraus ergibt sich die Frage, mit der ich hierher gekommen bin:

_Was halten Sie von der Bereitstellung der Möglichkeit, die Reihenfolge mit einem Django-ORM-Ausdruck in django_filters.rest_framework.OrderingFilter anzugeben?_

Aber bevor Sie antworten, möchte ich Sie daran erinnern, dass wir nur die Seiten 1 und 2 gelöst haben. Um p.3 zu lösen, benötigen wir einen Aggregationsausdruck:

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

Wir können diesen Ausdruck nicht in .annotate verwenden, da die Ergebnisse einer Kombination mit der QuserySet-Filterung unvorhersehbar sind. Wir können diesen Ausdruck auch nicht direkt in einem Aufruf von Query.order(...) verwenden, weil

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

S.3 ist also außer Reichweite und erfordert eine Lösung, die alles durchdringt, beginnend mit dem ViewSet. Es hat mehr mit Django REST Framework zu tun als mit Django-Filtern. Brauchen wir tatsächlich Ausdrucksunterstützung in django_filters.rest_framework.OrderingFilter ohne Aggregationsausdrucksunterstützung? Dies könnte Benutzer von django-filter nur verwirren, ohne viel Nutzen zu bringen.

Ich freue mich auf Ihre Meinung. Ich weiß, es sind viele Informationen, die es zu verdauen gilt. Hoffentlich hilft mein Testprojekt: https://github.com/earshinov/django_sample/tree/master/django_sample/ordering_by_expression

ImprovemenFeature

Hilfreichster Kommentar

ach - fing an, darauf zu antworten, aber mein Computer ging kaputt.

Ich halte die hier vorgeschlagenen Änderungen für sinnvoll. Das Paar model_field - parameter_name war zu der Zeit sinnvoll, da wir normalerweise die exponierten Parameter/Formulare/usw. aus dem Modell ableiten, aber es gibt keinen Grund, warum dies notwendig ist. Das Austauschen der Zuordnung wäre sinnvoll und ermöglicht es uns, komplexere Ausdrücke zu nutzen.

Außerdem glaube ich nicht, dass der Abwertungsprozess wahnsinnig schwierig wäre, und ich würde mich freuen, dabei zu helfen. Grundsätzlich würden fields und field_labels in params und param_labels übergehen. Es ist einfach genug, von einem zum anderen zu konvertieren, während die Abwärtskompatibilität erhalten bleibt und eine Verfallswarnung für Benutzer auslöst, die die alten Argumente verwenden.

Eine zu berücksichtigende Sache ist die automatische Konvertierung komplexer Ausdrücke für den absteigenden Fall ( param vs. -param ). zB, wenn die aufsteigende .asc(nulls_last=True) . ist, sollte die Umkehrung davon .desc(nulls_first=True) sein, oder sollten Nullen unabhängig von der Sortierrichtung zuletzt bleiben?

Alle 14 Kommentare

Hallo @earshinov. Danke für den Bericht. Sehr interessant. Lassen Sie mich darüber nachdenken. Rufen Sie mich im März an, wenn ich bis dahin nicht geantwortet habe. 🙂

Eine andere Idee: Es ist möglich, noch einen Schritt weiter zu gehen und einem Filter zu erlauben, nicht einen, sondern mehrere Ausdrücke oder Feldnamen anzugeben.

Vorher (Option 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'),
            ),
        })

Nachher (Option 2):

class OrderOrderingFilter(ddf.OrderingFilter):

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

In beiden Fällen ordnet ordering=user die Daten zuerst nach dem Vornamen des Benutzers und dann nach dem Nachnamen des Benutzers, aber Option 2 ist einfacher zu schreiben und vielleicht effizienter, wenn es um DB-Abfragen geht.

Hallo @carltongibson ,

Du hast mich gebeten, dich in Match anzupingen. Es ist schon Mai :)

Um auf meinen Vorschlag zum Ordnen nach Ausdrücken (einem oder mehreren) zurückzukommen, denke ich wirklich, dass dies ein fehlender Teil des Puzzles ist.

Ich sehe vier grundlegende Tabellenoperationen:
A. Serialisierung
B. ~Seitenumbruch~ (für diese Diskussion irrelevant)
C. filtern
D. Sortierung

Mit DRF und Django-Filter ist es möglich, all dies mit Unterstützung für verwandte Modelle zu implementieren, mit Ausnahme von sorting :

A. Für die Serialisierung haben wir verschachtelte Serialisierer . Wenn wir ein aggregiertes Feld zurückgeben müssen (denken Sie an count ), können wir den Abfragesatz .annotate() verwenden:

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

C. Zum Filtern mit Django-Ausdrücken kann ein benutzerdefinierter Filter implementiert werden (siehe Beispiel unten). Das Filtern nach einem aggregierten Feld ( submodel_count ) ist mit den gewöhnlichen Skalarfiltern ( filters.NumberFilter ) möglich.

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})
        )

D. Es gibt keine Lösung zum Sortieren :-(

Hier ist die vollständige Implementierung unseres benutzerdefinierten OrderingFilter , das wir bisher verwendet haben:

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

_Update_: Implementierung von normalize_fields und anderen Hilfsmethoden hinzugefügt.

Gut. 😀

Danke für den Ping.

Tbh, ich hatte keinen Moment Zeit, darüber nachzudenken.

Haben Sie bei all Ihren großartigen Gedanken hier einen Vorschlag für eine kleine Änderung, die wir vornehmen könnten? (Vielleicht ist es einfach einfacher, mit einer PR voranzukommen).

Ja, es ist nicht sofort ersichtlich, wie man solche Änderungen abwärtskompatibel macht, ich muss darüber nachdenken. Erwarten Sie nicht bald einen PR (wird an Orten, an denen das Internet knapp ist, im Urlaub sein).

Das ist gut. 🙂

Hier gibt es keine Eile. Besser wir denken nach, wenn überhaupt.

@carltongibson , Okay, wir haben ein wenig nachgedacht, konnten aber keinen akzeptablen Weg finden, die alte und neue Implementierung von OrderingFilter zu kombinieren.

Wenn wir alles in eine Klasse packen wollen, besteht das größte Problem darin, dass in der alten Implementierung die Felder model_field - parameter_name Paare speichern, während es in der neuen Implementierung umgekehrt ist parameter_name - model_field (muss es sein, denn statt model_field kann ein Ausdruck übergeben werden, der nur in einem Wert, aber nicht in einem Schlüssel in einem Dictionary gespeichert werden kann) .

Technisch gesehen ist es möglich, dieses Problem zu lösen, indem das Wörterbuch „auf die alte Art“ interpretiert wird, wenn es nur Zeichenketten enthält, und ansonsten „auf die neue Art“. In diesem Fall muss der Benutzer Wörterbucheinträge "umdrehen", wenn eine Notwendigkeit zum Ordnen nach Ausdrücken entsteht. Und achten Sie darauf, die Wörterbucheinträge zurückzudrehen, wenn der Sortierausdruck entfernt wird ... Das klingt für mich nach einer schrecklichen Benutzererfahrung. Es wird auch die Implementierung kompliziert machen.

Glauben Sie, dass es möglich ist, den neuen OrderingFilter zusammen mit dem alten unter einem anderen Namen einzuführen, z. B. ExpressionOrderingFilter ?

Das ist großartig, genau in dem Moment, in dem ich es brauchte! Danke @earshinov!

Wie würde ich etwas tun wie:

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

Mit Ihrem Bestellfilter?

    o = ExpressionOrderingFilter(

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

    )

scheint super zu funktionieren!
Jetzt müssen wir nur noch herausfinden, wie man mehrere Filter kombiniert, jang. 'Preis' und 'Aktie' in einem.

ach - fing an, darauf zu antworten, aber mein Computer ging kaputt.

Ich halte die hier vorgeschlagenen Änderungen für sinnvoll. Das Paar model_field - parameter_name war zu der Zeit sinnvoll, da wir normalerweise die exponierten Parameter/Formulare/usw. aus dem Modell ableiten, aber es gibt keinen Grund, warum dies notwendig ist. Das Austauschen der Zuordnung wäre sinnvoll und ermöglicht es uns, komplexere Ausdrücke zu nutzen.

Außerdem glaube ich nicht, dass der Abwertungsprozess wahnsinnig schwierig wäre, und ich würde mich freuen, dabei zu helfen. Grundsätzlich würden fields und field_labels in params und param_labels übergehen. Es ist einfach genug, von einem zum anderen zu konvertieren, während die Abwärtskompatibilität erhalten bleibt und eine Verfallswarnung für Benutzer auslöst, die die alten Argumente verwenden.

Eine zu berücksichtigende Sache ist die automatische Konvertierung komplexer Ausdrücke für den absteigenden Fall ( param vs. -param ). zB, wenn die aufsteigende .asc(nulls_last=True) . ist, sollte die Umkehrung davon .desc(nulls_first=True) sein, oder sollten Nullen unabhängig von der Sortierrichtung zuletzt bleiben?

OK, super @rpkilby.

Schön, dass wir hier vorankommen. Mir geht es vor allem darum, dass wir richtig dokumentieren, was wir tun. Benutzer finden die df-Dokumente bereits ein wenig _knapp_ sollen wir sagen 🙂 — Ich freue mich, API hinzuzufügen, aber wir müssen sicherstellen, dass es klar ist.

So etwas muss keine Ein-Mann-Anstrengung sein.

@rpkilby , @carltongibson , Wenn Sie meine Hilfe bei der Integration meiner Änderungen in das Projekt benötigen, denke ich, dass ich etwas Zeit investieren kann. Aber ich brauche eine Wegbeschreibung. Wo und wie fange ich an?

Hallo @earshinov. Ich würde mit dem Entwurf der Dokumente beginnen. Was ist die Geschichte, die wir erzählen? Ab da ändert sich der Code. Eine PR, auch wenn uns nur ein Entwurf etwas zu erzählen gibt.

Auch Testfälle, wenn Sie sie haben. Sie müssen möglicherweise an die endgültigen Änderungen angepasst werden, aber es wäre sehr hilfreich, die verschiedenen Fälle zu durchdenken (z. B. Umwandlung .asc in .desc , Handhabung nulls_fist / nulls_last Argumente usw.).

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen