Django-filter: [OrderingFilter] سؤال / طلب ميزة: السماح بتحديد تعبير Django ORM العشوائي للطلب

تم إنشاؤها على ٨ يناير ٢٠١٩  ·  14تعليقات  ·  مصدر: carltongibson/django-filter

أهلا! بادئ ذي بدء ، أود أن أشكرك على هذا المشروع ، الذي أثبت أنه مفيد للغاية بالاشتراك مع إطار عمل Django REST في تطبيقنا.

أعاني حاليًا من مشكلة التسلسل والتصفية والطلب حسب حقول النماذج ذات الصلة. في هذا العدد ، أود التركيز فقط على الطلب.

دعني أوضح نواياي بالنموذج التالي:

# 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 - اطلب حسب الكمية الإجمالية للمنتجات في الطلب

الأسلوب الأساسي مع ordering_fields الذي تم تكوينه على ViewSet يسمح فقط بالطلب بناءً على حقول النموذج ، الصفحة 1. لحسن الحظ ، يمكننا استخدام طريقة أكثر تقدمًا لتصنيف filters.OrderingFilter كما هو موضح جيدًا في https://django-filter.readthedocs.io/en/master/ref/filters.html#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 يتطلب اسم حقل. الطريقة الوحيدة للتحايل على هذه المشكلة هي عن طريق annotate 'في مجموعة QuerySet التي تمت تصفيتها:

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

يمكن للبعض أن يقول إن استخدام .annotate هنا فكرة سيئة لأنه لا يمكن للمرء أن يخلط .annotate مع .filter كما هو موضح في https://docs.djangoproject.com/en/2.1 / topic / db / aggregation / # order -of-annotate-and-filter-clauses. ومع ذلك ، فأنت بخير طالما أنك لا تستخدم التجميع في مكالماتك .annotate ، لذلك نحن جيدون حتى الآن. (_ سأعود بالفعل إلى التجميع لاحقًا_)

هناك مشكلة أخرى تتعلق بأحدث نموذج التعليمات البرمجية وهي أن الكود متكرر ومن الأفضل استخراجه في فئة أساسية يمكن بعد ذلك توريثها بسهولة. في الوقت الحالي ، قمت بكتابة صنف OrderingFilter الخاص بي ، والذي يعد بديلاً كاملاً للصف الذي يوفره django-filter. كود المصدر الكامل أدناه. قد تلاحظ أنه يستعير كثيرًا من مرشح django:

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 ORM في django_filters.rest_framework.OrderingFilter ؟ _

ولكن قبل أن تجيب ، دعني أذكرك بأننا قد حللنا الصفحتين 1 و 2 فقط. لحل الصفحة 3 ، سنحتاج إلى تعبير تجميعي:

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

لا يمكننا استخدام هذا التعبير في .annotate ، لأن نتائج مزجه مع تصفية QuserySet ستكون غير متوقعة. لا يمكننا استخدام هذا التعبير مباشرة في استدعاء 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))

لذا فإن الصفحة 3 بعيدة المنال ، وتتطلب حلاً يخترق كل شيء بدءًا من ViewSet. يتعلق الأمر بإطار Django REST أكثر من عوامل تصفية django. هل نحتاج فعلاً إلى دعم التعبير في django_filters.rest_framework.OrderingFilter بدون دعم تعبير التجميع؟ هذا قد يربك مستخدمي django-filter فقط دون تحقيق الكثير من الفوائد.

أنا أتطلع لسماع رأيك. أعلم أن هناك الكثير من المعلومات التي يجب هضمها. آمل أن يساعد مشروعي الاختباري: https://github.com/earshinov/django_sample/tree/master/django_sample/ordering_by_expression

ImprovemenFeature

التعليق الأكثر فائدة

ach - بدأت الاستجابة لهذا ولكن جهاز الكمبيوتر الخاص بي توقف.

أعتقد أن التغييرات المقترحة هنا معقولة. كان الزوج model_field - parameter_name منطقيًا في ذلك الوقت نظرًا لأننا عادةً ما نستمد المعلمات / النماذج المكشوفة / إلخ من النموذج ، ولكن لا يوجد سبب لضرورة ذلك. سيكون تبديل التعيين منطقيًا ، ويسمح لنا بالاستفادة من التعبيرات الأكثر تعقيدًا.

أيضًا ، لا أعتقد أن عملية الإيقاف ستكون صعبة للغاية ، وسأكون سعيدًا بالمساعدة في ذلك. بشكل أساسي ، سيتحول fields و field_labels إلى params و param_labels . من السهل التحويل من واحد إلى الآخر ، مع الاحتفاظ بالتوافق مع الإصدارات السابقة ورفع إشعار الإيقاف للمستخدمين الذين يستخدمون الوسائط القديمة.

شيء واحد يجب مراعاته هو التحويل التلقائي للتعبيرات المعقدة للحالة التنازلية ( param مقابل -param ). على سبيل المثال ، إذا كان الارتفاع هو .asc(nulls_last=True) . ، فهل يجب أن يكون معكوس هذا .desc(nulls_first=True) ، أم يجب أن تظل القيم الخالية في النهاية بغض النظر عن اتجاه الفرز؟

ال 14 كومينتر

مرحباearshinov. شكرا على التقرير. مثير جدا. اسمحوا لي أن أفكر في ذلك. أرسل لي في مارس إذا لم أقم بالرد بحلول ذلك الوقت. 🙂

فكرة أخرى: من الممكن المضي قدمًا والسماح لمرشح بتحديد ليس تعبيرًا واحدًا أو أسماء حقول متعددة.

قبل (الخيار 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 أسهل في الكتابة ، وربما يكون أكثر فاعلية عندما يتعلق الأمر باستعلامات قاعدة البيانات.

مرحبا carltongibson ،

طلبت مني الاتصال بك في المباراة. قد يكون بالفعل :)

بالعودة إلى اقتراحي بشأن الترتيب حسب التعابير (واحد أو متعدد) ، أعتقد حقًا أنه جزء مفقود من اللغز.

أرى أربع عمليات جدول أساسية:
أ. التسلسل
ب. ~ ترقيم الصفحات ~ (غير ذي صلة بهذه المناقشة)
ج. الفلتره
د. فرز

باستخدام 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 ، يمكن للمرء تنفيذ مرشح مخصص (انظر المثال أدناه). التصفية حسب الحقل المجمع ( submodel_count ) ممكنة باستخدام عوامل التصفية القياسية ( filters.NumberFilter ).

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

_Update_: يتضمن تنفيذ normalize_fields وطرق مساعدة أخرى.

جيد. 😀

شكرا على ping.

Tbh لم تتح لي لحظة للتفكير في هذا الأمر.

في كل أفكارك الرائعة هنا ، هل لديك اقتراح لتغيير طفيف يمكننا إجراؤه؟ (ربما يكون من الأسهل المضي قدمًا في العلاقات العامة).

نعم ، ليس من الواضح على الفور كيفية إجراء مثل هذه التغييرات المتوافقة مع الإصدارات السابقة ، وسأضطر إلى التفكير فيها. لا تتوقع إقامة علاقات عامة قريبًا (ستكون في إجازة في الأماكن التي يندر فيها الإنترنت).

هذا جيد. 🙂

ليس هناك اندفاع هنا. من الأفضل أن نفكر مليًا ، على كل حال.

carltongibson ، حسنًا ، فكرنا قليلاً ، لكن لم نجد طريقة مقبولة للجمع بين التطبيق القديم والجديد لـ OrderingFilter .

إذا كنا سنضع كل شيء في فئة واحدة ، فإن المشكلة الأكبر هي أنه في التطبيق القديم ، تخزن الحقول ديكت model_field - parameter_name أزواج ، بينما في التطبيق الجديد يكون العكس هو parameter_name - model_field (يجب أن يكون كذلك ، لأنه بدلاً من model_field ، يمكن تمرير تعبير ، والذي يمكن تخزينه فقط في قيمة ، ولكن ليس في مفتاح في قاموس) .

من الناحية الفنية ، من الممكن التغلب على هذه المشكلة من خلال تفسير القاموس "بالطريقة القديمة" إذا كان يحتوي على سلاسل فقط ، و "بالطريقة الجديدة" بخلاف ذلك. في هذه الحالة ، سيتعين على المستخدم "قلب" إدخالات القاموس عند الحاجة إلى الترتيب حسب التعبير. وكن حريصًا على "قلب" مدخلات القاموس مرة أخرى ، في حالة إزالة تعبير الطلب ... هذا يبدو وكأنه تجربة مستخدم رهيبة بالنسبة لي. كما أنه سيجعل التنفيذ معقدًا.

هل تعتقد أنه من الممكن تقديم OrderingFilter الجديد مع الاسم القديم باسم مختلف ، مثل ExpressionOrderingFilter ؟

هذا شيء عظيم ، بالضبط في اللحظة التي كنت في حاجة إليها! شكراearshinov!

كيف أفعل شيئًا مثل:

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

مع مرشح الطلب الخاص بك؟

    o = ExpressionOrderingFilter(

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

    )

يبدو أنه يعمل بشكل رائع!
الآن الشيء الوحيد المتبقي هو معرفة كيفية دمج المرشحات المتعددة ، ak. "السعر" و "الأسهم" في واحد.

ach - بدأت الاستجابة لهذا ولكن جهاز الكمبيوتر الخاص بي توقف.

أعتقد أن التغييرات المقترحة هنا معقولة. كان الزوج model_field - parameter_name منطقيًا في ذلك الوقت نظرًا لأننا عادةً ما نستمد المعلمات / النماذج المكشوفة / إلخ من النموذج ، ولكن لا يوجد سبب لضرورة ذلك. سيكون تبديل التعيين منطقيًا ، ويسمح لنا بالاستفادة من التعبيرات الأكثر تعقيدًا.

أيضًا ، لا أعتقد أن عملية الإيقاف ستكون صعبة للغاية ، وسأكون سعيدًا بالمساعدة في ذلك. بشكل أساسي ، سيتحول fields و field_labels إلى params و param_labels . من السهل التحويل من واحد إلى الآخر ، مع الاحتفاظ بالتوافق مع الإصدارات السابقة ورفع إشعار الإيقاف للمستخدمين الذين يستخدمون الوسائط القديمة.

شيء واحد يجب مراعاته هو التحويل التلقائي للتعبيرات المعقدة للحالة التنازلية ( param مقابل -param ). على سبيل المثال ، إذا كان الارتفاع هو .asc(nulls_last=True) . ، فهل يجب أن يكون معكوس هذا .desc(nulls_first=True) ، أم يجب أن تظل القيم الخالية في النهاية بغض النظر عن اتجاه الفرز؟

حسنًا ، رائعrpkilby.

سعيد لرؤيتنا نتحرك إلى الأمام هنا. شاغلي الرئيسي هو أننا نوثق بشكل صحيح ما نقوم به. يجد المستخدمون بالفعل مستندات df قليلاً _terse_ فهل نقول 🙂 - يسعدنا إضافة واجهة برمجة التطبيقات ولكننا نحتاج إلى التأكد من أنها واضحة.

مثل هذا الشيء لا يجب أن يكون جهد شخص واحد.

rpkilby ، carltongibson ، إذا كنت بحاجة إلى مساعدتي في دمج تغييراتي في المشروع ، أعتقد أنه يمكنني تخصيص بعض الوقت. لكني بحاجة لاتجاهات. أين وكيف أبدأ؟

مرحبًاearshinov. سأبدأ بصياغة المستندات. ما القصة التي نحكيها؟ من هناك يتغير الرمز. العلاقات العامة ، حتى لو كانت مجرد مسودة تعطينا شيئًا نتحدث عنه.

أيضًا ، اختبر الحالات إذا كانت لديك. قد يحتاجون إلى تعديل لمطابقة التغييرات النهائية ، ولكن التفكير في الحالات المختلفة سيكون مفيدًا للغاية (على سبيل المثال ، تحويل .asc إلى .desc ، التعامل مع nulls_fist / nulls_last وسيطات ، وما إلى ذلك).

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات

القضايا ذات الصلة

chromakey picture chromakey  ·  5تعليقات

jwineinger picture jwineinger  ·  3تعليقات

sassanh picture sassanh  ·  4تعليقات

edmorley picture edmorley  ·  3تعليقات

GuillaumeCisco picture GuillaumeCisco  ·  3تعليقات