تا الان APIهای ساختید که همه داده‌ها را یکجا برمی‌گردانند. مثلاً GET /api/articles/ لیست همه مقالات را نشان می‌دهد. بدون هیچ انتخابی. بدون هیچ ترتیبی. حالا فرض کنید دیتابیس شما هزار تا مقاله دارد. کاربر نمی‌خواهد همه را یکجا ببیند. فقط می‌خواهد مقالات خودش را ببیند. یا آخرین مقالات را اول ببیند. یا بین عناوین جستجو کند. اینجا فیلتر، جستجو و مرتب‌سازی drf وارد می‌شوند.

  • فیلتر یعنی به کاربر اجازه دهید فقط زیرمجموعه‌ای از داده‌ها را ببیند. مثلاً فقط مقالات یک نویسنده خاص.
  • جستجو یعنی کاربر یک کلمه وارد کند، سیستم آن کلمه را در عنوان یا محتوا پیدا کند و نتایج مرتبط را برگرداند.
  • مرتب‌سازی یعنی کاربر مشخص کند بر اساس چه فیلدی و به چه ترتیبی نتایج نشان داده شود. جدیدترین اول، یا قدیمی‌ترین اول.

در این درس، سه روش برای پیاده‌سازی این قابلیت‌ها یاد می‌گیرید.

اول با request.query_params ساده شروع می‌کنیم. بعد سراغ django-filter می‌رویم که حرفه‌ای‌تر است. در انتها با SearchFilter و OrderingFilter آشنا می‌شوید که جستجو و مرتب‌سازی را به صورت خودکار انجام می‌دهند. این قابلیت‌ها برای هر API که داده‌های زیادی دارد، ضروری هستند. کاربران عادی انتظار دارند بتوانند داده‌ها را فیلتر و مرتب کنند.

برای این درس، همان مدل Article قبل را داریم. فقط چند مقاله نمونه به آن اضافه می‌کنیم تا تست کردن راحت‌تر باشد.

فیلتر ساده با request.query_params

مشکل بدون فیلتر

فرض کنید API مقالات شما همه رکوردها را برمی‌گرداند.

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

کاربر درخواست می‌دهد: GET /api/articles/ و هزار تا مقاله می‌گیرد. چه بخواهد چه نخواهد.

اما بیشتر مواقع، کاربر فقط زیرمجموعه‌ای از داده‌ها را می‌خواهد. مثلاً فقط مقالات خودش. یا فقط مقالات یک نویسنده خاص.

راه حل ساده: request.query_params

قبلاً یاد گرفتید که پارامترهای آدرس را با request.query_params بخوانید. همان قابلیت را اینجا هم استفاده می‌کنیم.

کاربر پارامتری به آدرس اضافه می‌کند. مثلاً ?author=ali. ما این پارامتر را می‌خوانیم و کوئری را فیلتر می‌کنیم.

مثال عملی:

from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer

class ArticleListView(ListAPIView):
    serializer_class = ArticleSerializer
    
    def get_queryset(self):
        queryset = Article.objects.all()
        
        # خواندن پارامتر author از آدرس
        author = self.request.query_params.get('author')
        
        if author:
            queryset = queryset.filter(author=author)
        
        return queryset

حالا کاربر می‌تواند بنویسد:

GET /api/articles/?author=ali

و فقط مقالاتی که نویسنده آنها ali است را دریافت کند.

چند نمونه دیگر

فیلتر بر اساس وضعیت انتشار:

status = self.request.query_params.get('status')
if status:
    queryset = queryset.filter(status=status)

آدرس: GET /api/articles/?status=draft

فیلتر بر اساس تاریخ (بعد از یک تاریخ خاص):

published_after = self.request.query_params.get('published_after')
if published_after:
    queryset = queryset.filter(published_at__gte=published_after)

آدرس: GET /api/articles/?published_after=2024-01-01

چند فیلتر همزمان:

def get_queryset(self):
    queryset = Article.objects.all()
    
    author = self.request.query_params.get('author')
    status = self.request.query_params.get('status')
    
    if author:
        queryset = queryset.filter(author=author)
    if status:
        queryset = queryset.filter(status=status)
    
    return queryset

آدرس: GET /api/articles/?author=ali&status=published

مزایا و معایب این روش

مزایا:

  • ساده و سریع. بدون نصب کتابخانه اضافه.
  • کنترل کامل دارید. هر منطقی که بخواهید می‌نویسید.
  • برای پروژه‌های کوچک و فیلترهای ساده عالی است.

معایب:

  • برای هر فیلد تازه باید کد بنویسید.
  • اگر فیلدهای زیادی داشته باشید، کد بلند و تکراری می‌شود.
  • پشتیبانی از شرایط پیچیده (مثل قیمت کمتر از ۱۰۰) دست و پاگیر است.
  • اعتبارسنجی پارامترها را خودتان باید انجام دهید.

نکته مهم: خالی بودن پارامتر

حتماً بررسی کنید که کاربر واقعاً پارامتر را فرستاده یا نه. اگر بنویسید:

author = self.request.query_params.get('author')
queryset = queryset.filter(author=author)

و کاربر پارامتر author را نفرستاده باشد، مقدار author برابر None می‌شود. فیلتر با None هیچ رکوردی برنمی‌گرداند. نتیجه یک لیست خالی است که احتمالاً نمی‌خواهید.

راه درست همان مثال اول است: اول بررسی کنید پارامتر وجود دارد، بعد فیلتر را اعمال کنید.

چه موقع از این روش استفاده کنیم؟

این روش برای پروژه‌های کوچک و متوسط خوب است. وقتی فقط یک یا دو فیلتر نیاز دارید. یا منطق فیلتر شما خیلی خاص است و با کتابخانه‌های آماده راحت نیستید.

برای پروژه‌های بزرگتر، در زیرعنوان بعدی با django-filter آشنا می‌شوید که کار را خیلی حرفه‌ای‌تر انجام می‌دهد.

تمرین این بخش

به API مقالات خود یک فیلتر category اضافه کنید. کاربر بتواند با ?category=technology فقط مقالات مربوط به آن دسته را ببیند. (قبلاً فیلد category را به مدل اضافه کنید.)

استفاده از django-filter (نصب و تنظیم)

در زیرعنوان قبل، فیلتر ساده را با request.query_params یاد گرفتید. برای دو سه تا فیلتر، آن روش خوب است.

اما وقتی پروژه بزرگ می‌شود، نگهداری آن سخت می‌شود. باید برای هر فیلد جدید کد بنویسید. اعتبارسنجی دستی کنید. و شرایط پیچیده مثل «قیمت کمتر از ۱۰۰» را خودتان مدیریت کنید.

اینجا django-filter وارد می‌شود.

django-filter چیست؟

یک کتابخانه رسمی و محبوب برای فیلتر کردن در جنگو و DRF. همان کاری که شما با چند خط کد دستی انجام می‌دادید، با چند خط تنظیم انجام می‌دهد.

خیلی از پروژه‌های بزرگ از آن استفاده می‌کنند. چون کد را تمیز و قابل نگهداری می‌کند.

قدم اول: نصب

در ترمینال و در محیط مجاز خود، دستور زیر را وارد کنید:

pip install django-filter

قدم دوم: اضافه کردن به settings.py

دو جا باید تنظیم کنید.

اول: اضافه کردن به INSTALLED_APPS

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    # ... سایر اپ‌ها
    'rest_framework',
    'django_filters',  # این خط را اضافه کنید
    'myapp',
]

دوم: تنظیم filter backend در REST_FRAMEWORK

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
    ]
}

این تنظیم یعنی همه ویوهای شما به طور پیش‌فرض قابلیت فیلتر شدن دارند.

قدم سوم: استفاده در ویو

حالا در ویوی خود مشخص می‌کنید کاربر روی چه فیلدهایی می‌تواند فیلتر اعمال کند.

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.generics import ListAPIView
from .models import Article
from .serializers import ArticleSerializer

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['author', 'status', 'category']

با این چند خط، کاربر می‌تواند درخواست‌های زیر بدهد:

GET /api/articles/?author=ali
GET /api/articles/?status=published
GET /api/articles/?category=technology
GET /api/articles/?author=ali&status=published

بدون اینکه شما یک خط کد اضافه بنویسید.

فیلترهای پیشرفته با FilterSet

filterset_fields فقط فیلتر ساده و دقیق (exact match) انجام می‌دهد. اما کاربران معمولاً نیازهای پیشرفته‌تری دارند. مثلاً:

  • جستجوی متنی با contains (قسمتی از کلمه)
  • فیلتر عددی با gt (بزرگتر از) و lt (کوچکتر از)
  • فیلتر روی فیلدهای خارجی (ForeignKey)

برای این کارها باید یک FilterSet سفارشی بسازید.

ساخت فایل filters.py:

در اپ خود یک فایل جدید به اسم filters.py بسازید:

import django_filters
from .models import Article

class ArticleFilter(django_filters.FilterSet):
    # فیلتر عنوان با جستجوی جزیی (contains)
    title = django_filters.CharFilter(lookup_expr='icontains')
    
    # فیلتر قیمت با بزرگتر از و کوچکتر از
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    # فیلتر روی فیلد خارجی
    category_name = django_filters.CharFilter(field_name='category__name', lookup_expr='icontains')
    
    class Meta:
        model = Article
        fields = ['author', 'status', 'title', 'price_min', 'price_max', 'category_name']

استفاده در ویو:

from .filters import ArticleFilter

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ArticleFilter  # به جای filterset_fields

حالا کاربر می‌تواند فیلترهای پیشرفته استفاده کند:

# جستجو در عنوان
GET /api/articles/?title=django

# بازه قیمت
GET /api/articles/?price_min=100&price_max=500

# فیلتر روی نام دسته‌بندی (ForeignKey)
GET /api/articles/?category_name=technology

تنظیم سراسری در مقابل محلی

دو راه برای تنظیم DjangoFilterBackend دارید:

راه اول: سراسری (در settings.py)

همانطور که بالاتر دیدیم. همه ویوها به طور خودکار قابلیت فیلتر پیدا می‌کنند.

راه دوم: محلی (در هر ویو جداگانه)

اگر نخواهید همه ویوها قابلیت فیلتر داشته باشند، تنظیم سراسری را نکنید و فقط در ویوهای خاص بنویسید:

class ArticleListView(ListAPIView):
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['author']

خطاهای رایج

اول: خطای ImportError یا DjangoFilterBackend کار نمی‌کند

یعنی django-filter را نصب نکرده‌اید یا به INSTALLED_APPS اضافه نکرده‌اید.

دوم: فیلترها اعمال نمی‌شوند

بررسی کنید filter_backends = [DjangoFilterBackend] را در ویو اضافه کرده‌اید. یا filterset_fields یا filterset_class را درست نوشته‌اید.

سوم: فیلتر روی فیلد خارجی کار نمی‌کند

در filterset_fields نمی‌توانید مستقیماً category__name بنویسید. برای این کار حتماً از FilterSet سفارشی استفاده کنید.

چه موقع از کدام روش استفاده کنیم؟

تعداد فیلترها روش پیشنهادی
۱-۲ فیلتر ساده request.query_params دستی
چند فیلتر ساده filterset_fields
فیلترهای پیشرفته (بازه، جستجوی جزیی، فیلد خارجی) FilterSet سفارشی

فیلتر روی فیلدهای خارجی (ForeignKey)

تا الان یاد گرفتید روی فیلدهای ساده مثل author یا status فیلتر بزنید. اما در دنیای واقعی، مدل‌ها به هم مربوط هستند.

به مثال زیر دقت کنید.

فرض کنید مدل Article دارید و هر مقاله به یک Category متصل است. حالا کاربر می‌گوید: «مقالات دسته تکنولوژی را به من نشان بده».

چطور باید این کار را کرد؟

مشکل اصلی

اگر فقط بنویسید:

filterset_fields = ['category']

کاربر باید شناسه عددی دسته را بداند. مثلاً ?category=5. این برای کاربر خوب نیست. او اسم دسته را می‌داند، نه شماره آن.

ما می‌خواهیم کاربر با اسم دسته فیلتر کند. مثلاً ?category_name=technology.

راه حل: FilterSet سفارشی

برای فیلتر روی فیلدهای خارجی، باید یک FilterSet بسازید و field_name را با __ (double underscore) مشخص کنید.

ساختار مدل‌ها:

# models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    author = models.CharField(max_length=100)

ساخت FilterSet:

# filters.py
import django_filters
from .models import Article

class ArticleFilter(django_filters.FilterSet):
    # فیلتر بر اساس اسم دسته (ForeignKey)
    category_name = django_filters.CharFilter(
        field_name='category__name', 
        lookup_expr='icontains'
    )
    
    # فیلتر بر اساس اسلاگ دسته
    category_slug = django_filters.CharFilter(
        field_name='category__slug',
        lookup_expr='exact'
    )
    
    class Meta:
        model = Article
        fields = ['author', 'status']

توضیح field_name='category__name':

category اسم فیلد خارجی در مدل Article است

name اسم فیلدی در مدل Category است که می‌خواهیم روی آن فیلتر کنیم

__ (دو خط زیر) یعنی از این رابطه عبور کن و برو به فیلد name

حالا کاربر می‌تواند:

GET /api/articles/?category_name=tech
GET /api/articles/?category_name=تکنولوژی
GET /api/articles/?category_slug=technology

lookup_expr چیست؟

lookup_expr مشخص می‌کند فیلتر چگونه اعمال شود:

lookup_expr معنی مثال
exact دقیقاً برابر ?category_name=tech
icontains شامل (بدون حساسیت به کوچکی و بزرگی) ?category_name=tec
istartswith شروع شود با ?category_name=tec
iexact برابر (بدون حساسیت) ?category_name=TECH

برای فیلدهای خارجی، icontains خیلی کاربرد دارد. چون کاربر قرار نیست اسم دقیق دسته را حفظ باشد.

فیلتر روی چند سطح (چند رابطه)

اگر رابطه شما عمیق‌تر باشد، باز هم می‌توانید فیلتر کنید.

فرض کنید هر دسته به یک Section متصل است:

Article → Category → Section

و می‌خواهید مقالات یک بخش خاص را فیلتر کنید:

section_name = django_filters.CharFilter(
    field_name='category__section__name',
    lookup_expr='icontains'
)

هر __ شما را یک سطح عمیق‌تر می‌برد. محدودیتی در تعداد رابطه‌ها وجود ندارد.

فیلتر روی فیلد خارجی با FilterSet سفارشی و Generic View

# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.generics import ListAPIView
from .models import Article
from .serializers import ArticleSerializer
from .filters import ArticleFilter

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ArticleFilter  # استفاده از FilterSet سفارشی

فیلتر روی فیلد خارجی در ViewSet

from rest_framework.viewsets import ReadOnlyModelViewSet

class ArticleViewSet(ReadOnlyModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ArticleFilter

یک نکته مهم برای کاربر

اگر کاربر مقدار اشتباه وارد کند (مثلاً اسم دسته‌ای که وجود ندارد)، چه اتفاقی می‌افتد؟

django-filter یک کوئری خالی برمی‌گرداند ([]). نه خطای ۵۰۰، نه ۴۰۴. فقط می‌گوید چیزی پیدا نشد. این رفتار درست است. چون کاربر پارامتر اشتباه فرستاده، اما خود درخواست اشتباه نیست.

تمرین:

مدل Author جداگانه بسازید با فیلدهای name و email. سپس مدل Article را تغییر دهید تا به جای author از نوع CharField، از ForeignKey به Author استفاده کند.

یک FilterSet بنویسید که کاربر بتواند:

  • با ایمیل نویسنده فیلتر کند (?author_email=ali@example.com)
  • با نام نویسنده جستجوی جزیی کند (?author_name=ali)

فیلتر با URL (شرایطی مثل price__lt=100)

تا الان یاد گرفتید روی فیلدها فیلتر دقیق (exact) بزنید. یعنی ?author=ali دقیقاً آن چیزی که هست را پیدا می‌کند.

اما خیلی از مواقع کاربر نیازهای دیگری دارد. مثلاً بگوید «کتاب‌هایی که قیمتشان کمتر از ۱۰۰ هزار تومان است» یا «مقالاتی که بعد از تاریخ خاصی منتشر شده‌اند».

این یعنی فیلتر با شرایط مقایسه‌ای. __lt یعنی کوچکتر از (Less Than). __gt یعنی بزرگتر از (Greater Than).

فرمت کلی
در django-filter، شرایط مقایسه‌ای با اضافه کردن __ و اسم شرط به انتهای نام فیلد مشخص می‌شوند.

?field_name__condition=value

چند مثال:

?price__lt=100        قیمت کمتر از ۱۰۰
?price__gt=100        قیمت بیشتر از ۱۰۰
?price__lte=100       قیمت کمتر یا برابر ۱۰۰
?price__gte=100       قیمت بیشتر یا برابر ۱۰۰
?published_at__gte=2024-01-01   منتشر شده بعد از اول ژانویه ۲۰۲۴
?title__contains=django    عنوان شامل کلمه django
?title__icontains=Django   عنوان شامل Django (بدون حساسیت به کوچکی و بزرگی)

پیاده‌سازی با filterset_fields

ساده‌ترین راه. کافی است فیلد را در filterset_fields قرار دهید. خود django-filter شرایط مقایسه‌ای را پشتیبانی می‌کند.

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ['price', 'stock', 'published_at']

حالا کاربر میتواند:

GET /api/products/?price__lt=100
GET /api/products/?price__gt=50&price__lt=200
GET /api/products/?published_at__gte=2024-01-01
GET /api/products/?stock__gt=0

بدون اینکه شما یک خط کد اضافه بنویسید.

محدودیت filterset_fields

filterset_fields فقط فیلدهای ساده را پشتیبانی می‌کند. نمی‌توانید برای هر فیلد شرط خاصی تعریف کنید یا lookup_expr پیش‌فرض را تغییر دهید.

برای کنترل بیشتر، باید از FilterSet سفارشی استفاده کنید.

پیاده‌سازی با FilterSet سفارشی

# filters.py
import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    # فیلترهای مقایسه‌ای روی قیمت
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    # فیلتر بازه تاریخ
    published_after = django_filters.DateFilter(field_name='published_at', lookup_expr='gte')
    published_before = django_filters.DateFilter(field_name='published_at', lookup_expr='lte')
    
    # فیلتر متنی با جستجوی جزیی
    title_contains = django_filters.CharFilter(field_name='title', lookup_expr='icontains')
    
    class Meta:
        model = Product
        fields = ['category', 'price_min', 'price_max', 'published_after', 'published_before']

توضیح خط‌ها:

NumberFilter برای فیلدهای عددی مثل قیمت، تعداد، سن

DateFilter برای فیلدهای تاریخ

CharFilter برای فیلدهای متنی

lookup_expr مشخص می‌کند چه شرطی اعمال شود (gte، lte، icontains، و غیره)

حالا کاربر می‌تواند:

GET /api/products/?price_min=50&price_max=200
GET /api/products/?published_after=2024-01-01
GET /api/products/?title_contains=django

جدول lookup_exprهای پرکاربرد

lookup_expr معنی نوع فیلد مثال
exact دقیقاً برابر همه ?title__exact=book
iexact برابر (بدون حساسیت) رشته ?title__iexact=Book
contains شامل رشته ?title__contains=py
icontains شامل (بدون حساسیت) رشته ?title__icontains=Py
startswith شروع با رشته ?title__startswith=the
endswith پایان با رشته ?title__endswith=ing
gt بزرگتر از عدد، تاریخ ?price__gt=100
gte بزرگتر یا مساوی عدد، تاریخ ?price__gte=100
lt کوچکتر از عدد، تاریخ ?price__lt=100
lte کوچکتر یا مساوی عدد، تاریخ ?price__lte=100
range در بازه عدد، تاریخ ?price__range=100,200
year سال مساوی تاریخ ?published_at__year=2024
month ماه مساوی تاریخ ?published_at__month=12
isnull تهی است همه ?author__isnull=True

مثال عملی: API محصولات با فیلتر قیمت و تاریخ

# filters.py
class ProductFilter(django_filters.FilterSet):
    price = django_filters.NumberFilter()
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    created_after = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
    
    class Meta:
        model = Product
        fields = ['category', 'price', 'price_min', 'price_max', 'created_after']
# views.py
class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ProductFilter

درخواست‌های ممکن از کاربر:

# قیمت دقیق ۱۰۰
GET /api/products/?price=100

# قیمت بین ۵۰ تا ۲۰۰
GET /api/products/?price_min=50&price_max=200

# محصولات یک دسته خاص با قیمت کمتر از ۳۰۰
GET /api/products/?category=electronics&price_max=300

# محصولاتی که بعد از تاریخ خاص ساخته شده‌اند
GET /api/products/?created_after=2024-06-01

چه موقع از filterset_fields و چه موقع از FilterSet؟

شرایط راه‌حل
فیلتر ساده و دقیق (exact) filterset_fields
نیاز به شرایط مقایسه‌ای (ltgt) FilterSet
نیاز به lookup_expr متفاوت برای هر فیلد FilterSet
نیاز به اعتبارسنجی یا تغییر مقدار قبل از فیلتر FilterSet
فیلتر روی فیلدهای خارجی (ForeignKey) FilterSet

نکته مهم: مستندسازی خودکار

وقتی از FilterSet استفاده می‌کنید، صفحه Browsable API به صورت خودکار فیلترهای موجود را نمایش می‌دهد. کاربر می‌تواند ببیند چه فیلترهایی در دسترس است و چطور از آن‌ها استفاده کند.

تمرین این بخش

یک API برای مدل Order (سفارش‌ها) با فیلدهای total_price، created_at، و status بسازید. با FilterSet سفارشی، قابلیت‌های زیر را اضافه کنید:

  • فیلتر بازه قیمت (total_price_min و total_price_max)
  • فیلتر بازه تاریخ (created_after و created_before)
  • فیلتر وضعیت (دقیق)

SearchFilter و OrderingFilter (جستجو و مرتب‌سازی)

تا الان فقط فیلتر کردن را یاد گرفتیم. کاربر می‌توانست بگوید «مقالات نویسنده علی را به من نشان بده». اما دو قابلیت مهم دیگر هنوز جا مانده است.

اول: جستجو. کاربر می‌گوید «کلمه جنگو در عنوان مقاله».

دوم: مرتب‌سازی. کاربر می‌گوید «مقالات را از جدیدترین به قدیمی‌ترین مرتب کن».

DRF برای هر دوی اینها دو کلاس آماده دارد. SearchFilter و OrderingFilter.

SearchFilter – جستجوی متنی

با این کلاس، کاربر می‌تواند یک عبارت جستجو را به آدرس اضافه کند و سیستم آن عبارت را در فیلدهای مشخص شده جستجو کند.

قدم اول: تنظیمات

اگر قبلاً DEFAULT_FILTER_BACKENDS را در settings.py تنظیم کرده‌اید، SearchFilter را به آن اضافه کنید:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ]
}

قدم دوم: استفاده در ویو

from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.generics import ListAPIView
from .models import Article
from .serializers import ArticleSerializer

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ['title', 'content', 'author__username']

حالا کاربر می‌تواند درخواست بدهد:

GET /api/articles/?search=django

سیستم کلمه django را در فیلدهای title، content و author__username جستجو می‌کند. هر مقاله‌ای که حداقل در یکی از این فیلدها عبارت را داشته باشد، در نتیجه می‌آید.

انواع lookup در SearchFilter

وقتی می‌نویسید search_fields = ['title']، جستجو به صورت icontains انجام می‌شود. یعنی شامل عبارت باشد، بدون حساسیت به حروف کوچک و بزرگ.

می‌توانید رفتار جستجو را با نشانه‌گذاری فیلدها تغییر دهید:

نشانه معنی مثال
^ شروع با (startswith) '^title'
= دقیقاً برابر (exact) '=title'
@ جستجوی تمام متن (full-text search) '@title'
$ جستجوی عبارتی (regex) '$title'

مثال:

search_fields = ['^title', '=author__username']

یعنی:

عنوان باید با عبارت جستجو شروع شود

نام کاربری نویسنده دقیقاً برابر عبارت باشد

نکته مهم: حساسیت به حروف

جستجوی SearchFilter به صورت پیش‌فرض به حروف کوچک و بزرگ حساس نیست. یعنی django و Django و DJANGO همه یکسان دیده می‌شوند. این همان چیزی است که کاربران معمولی انتظار دارند.

OrderingFilter – مرتب‌سازی

با این کلاس، کاربر می‌تواند تعیین کند نتایج بر اساس چه فیلدی و به چه ترتیبی مرتب شوند.

قدم اول: تنظیمات
همان تنظیمات بالا کافی است.

قدم دوم: استفاده در ویو

class ArticleListView(ListAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ['title', 'content']
    ordering_fields = ['created_at', 'title', 'author__username']
    ordering = ['-created_at']  # مرتب‌سازی پیش‌فرض

حالا کاربر می‌تواند درخواست بدهد:

# مرتب‌سازی صعودی (از قدیم به جدید)
GET /api/articles/?ordering=created_at

# مرتب‌سازی نزولی (از جدید به قدیم)
GET /api/articles/?ordering=-created_at

# مرتب‌سازی بر اساس چند فیلد
GET /api/articles/?ordering=-created_at,title

علامت - یعنی نزولی (از بزرگ به کوچک). بدون علامت یعنی صعودی (از کوچک به بزرگ).

مرتب‌سازی پیش‌فرض

اگر کاربر پارامتری نفرستد، مرتب‌سازی بر اساس ordering = ['-created_at'] انجام می‌شود. یعنی جدیدترین اول.

محدود کردن فیلدهای قابل مرتب‌سازی

اگر ordering_fields را تنظیم نکنید، کاربر می‌تواند روی هر فیلدی که مدل دارد مرتب‌سازی کند. گاهی این زیاد است.

بهتر است خودتان مشخص کنید:

ordering_fields = ['created_at', 'title', 'price']

حالا کاربر نمی‌تواند روی content (که متن بلندی است) مرتب‌سازی کند.

ترکیب فیلتر، جستجو و مرتب‌سازی

این سه قابلیت کاملاً با هم هماهنگ هستند. کاربر می‌تواند یک درخواست بفرستد که هم فیلتر داشته باشد، هم جستجو، هم مرتب‌سازی.

GET /api/articles/?author=ali&search=django&ordering=-created_at

یعنی: مقالات نویسنده علی را پیدا کن، بین آنها کلمه django را در عنوان یا محتوا جستجو کن، و نتیجه را از جدید به قدیم مرتب کن.

ترتیب اجرا:

  • اول فیلترها اعمال می‌شوند (author=ali)
  • بعد جستجو روی نتایج فیلتر شده (search=django)
  • در آخر مرتب‌سازی (ordering=-created_at)

مثال کامل: API محصولات با جستجو و مرتب‌سازی

# views.py
from rest_framework.filters import SearchFilter, OrderingFilter
from rest_framework.generics import ListAPIView
from .models import Product
from .serializers import ProductSerializer

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ['name', 'description', 'category__name']
    ordering_fields = ['price', 'created_at', 'name', 'stock']
    ordering = ['name']  # مرتب‌سازی پیش‌فرض بر اساس اسم

درخواست‌های ممکن:

# جستجو در نام و توضیحات
GET /api/products/?search=laptop

# ارزان‌ترین اول
GET /api/products/?ordering=price

# گران‌ترین اول
GET /api/products/?ordering=-price

# جستجو + مرتب‌سازی
GET /api/products/?search=laptop&ordering=-price

# جستجو در یک دسته خاص + مرتب‌سازی
GET /api/products/?category=electronics&search=laptop&ordering=price

تفاوت فیلتر و جستجو

خیلی از تازه‌کارها این دو را قاطی می‌کنند.

فیلتر (DjangoFilterBackend): مقادیر دقیق یا شرایط مقایسه‌ای. مثل ?category=books، ?price__lt=100

جستجو (SearchFilter): یک عبارت متنی که در چند فیلد جستجو می‌شود. مثل ?search=django

هر دو را می‌توانید با هم استفاده کنید.

عیب‌یابی خطاهای رایج

جستجو کار نمی‌کند:

  • SearchFilter را به filter_backends اضافه کرده‌اید؟
  • search_fields را تعریف کرده‌اید؟
  • کاربر از پارامتر search استفاده کرده؟ (نه q یا چیز دیگر)

مرتب‌سازی کار نمی‌کند:

  • OrderingFilter را به filter_backends اضافه کرده‌اید؟
  • ordering_fields یا ordering را تعریف کرده‌اید؟
  • کاربر از پارامتر ordering استفاده کرده؟

تمریناین بخش

یک API برای مدل Product بسازید با قابلیت‌های زیر:

  • جستجو در name و description
  • مرتب‌سازی بر اساس price و created_at و name
  • مرتب‌سازی پیش‌فرض بر اساس -created_at (جدیدترین اول)
  • فیلتر دسته‌بندی با django-filter (مثل ?category=books)

همه را در یک ویو پیاده‌سازی کنید. سپس در Postman تست کنید که آیا می‌توانید ترکیبی از فیلتر، جستجو و مرتب‌سازی را یکجا استفاده کنید.

ترکیب فیلتر، جستجو و مرتب‌سازی در یک ویو

تا الان هر کدام از این قابلیت‌ها را جداگانه یاد گرفتید.

فیلتر با DjangoFilterBackend، جستجو با SearchFilter، مرتب‌سازی با OrderingFilter.

اما در دنیای واقعی، کاربران همه اینها را با هم می‌خواهند. یک صفحه لیست محصولات را در نظر بگیرید. کاربر می‌خواهد:

  • فقط محصولات دسته «الکترونیک» را ببیند (فیلتر)
  • بین آنها کلمه «لپ‌تاپ» را جستجو کند (جستجو)
  • و نتیجه را از گران به ارزان مرتب کند (مرتب‌سازی)

در این بخش یاد می‌گیرید چطور هر سه را در یک ویو جمع کنید.

قدم اول: تنظیمات در settings.py

ساده‌ترین راه این است که هر سه را به DEFAULT_FILTER_BACKENDS اضافه کنید:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ]
}

با این کار، هر ویوی که از ListAPIView یا ViewSet ارث‌بری کند، به طور خودکار هر سه قابلیت را خواهد داشت.

قدم دوم: پیاده‌سازی ویو

حالا یک ویو می‌نویسیم که هم فیلتر دارد، هم جستجو، هم مرتب‌سازی.

from rest_framework.generics import ListAPIView
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    # فیلترها
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['category', 'status', 'brand']
    
    # جستجو
    search_fields = ['name', 'description', 'brand__name']
    
    # مرتب‌سازی
    ordering_fields = ['price', 'created_at', 'name', 'stock']
    ordering = ['-created_at']  # مرتب‌سازی پیش‌فرض

قدم سوم: تست ترکیبی

حالا کاربر می‌تواند یک درخواست ترکیبی بفرستد:

GET /api/products/?category=electronics&search=laptop&ordering=-price

این درخواست یعنی:

  • اول محصولات دسته electronics را پیدا کن (فیلتر)
  • بین آنها کلمه laptop را در نام، توضیحات یا نام برند جستجو کن (جستجو)
  • نتیجه را بر اساس قیمت از گران به ارزان مرتب کن (مرتب‌سازی)

مثال کامل با FilterSet سفارشی

اگر فیلترهای شما پیچیده‌تر است و نیاز به FilterSet سفارشی دارید:

# filters.py
import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    class Meta:
        model = Product
        fields = ['category', 'status', 'brand', 'price_min', 'price_max']

فایل views.py:

# views.py
from rest_framework.generics import ListAPIView
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_class = ProductFilter  # استفاده از FilterSet سفارشی
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'created_at', 'name']
    ordering = ['-created_at']

حالا کاربر می‌تواند:

# ترکیب فیلتر بازه قیمت + جستجو + مرتب‌سازی
GET /api/products/?price_min=100&price_max=500&search=phone&ordering=-price

مثال با ViewSet (ModelViewSet)

این روش برای ViewSet هم دقیقاً به همین شکل کار می‌کند:

from rest_framework.viewsets import ModelViewSet
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend

class ProductViewSet(ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_fields = ['category', 'status']
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'created_at']
    ordering = ['-created_at']

ترتیب اجرای قابلیت‌ها

وقتی کاربر یک درخواست ترکیبی می‌فرستد، DRF این مراحل را به ترتیب انجام می‌دهد:

  • فیلتر (DjangoFilterBackend) – اول کوئری را محدود می‌کند
  • جستجو (SearchFilter) – روی نتایج فیلتر شده جستجو می‌کند
  • مرتب‌سازی (OrderingFilter) – در آخر نتایج را مرتب می‌کند

این ترتیب کاملاً منطقی است. چون جستجو روی دامنه کوچک‌تری انجام می‌شود و مرتب‌سازی در انتها روی نتیجه نهایی اعمال می‌گردد.

نکته مهم: performance

اگر دیتابیس شما خیلی بزرگ است، ترکیب فیلتر و جستجو ممکن است کند شود. چند راهکار:

  • روی فیلدهایی که مرتباً فیلتر می‌شوند (category، status) ایندکس (index) بگذارید.
  • برای جستجوی متنی روی فیلدهای بزرگ (description) از Full-Text Search استفاده کنید.
  • از ابزارهایی مثل django-debug-toolbar استفاده کنید تا ببینید کدام کوئری کند است.

مثال واقعی: API فروشگاه اینترنتی

class ProductListView(ListAPIView):
    queryset = Product.objects.filter(is_active=True)
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    
    # فیلترها
    filterset_fields = ['category', 'brand', 'color', 'is_in_stock']
    
    # جستجو
    search_fields = ['name', 'description', 'brand__name', 'category__name']
    
    # مرتب‌سازی
    ordering_fields = ['price', 'created_at', 'sales_count', 'rating']
    ordering = ['-sales_count']  # پر فروش‌ترین اول

درخواست‌های ممکن از کاربر:

# جستجوی ساده
GET /api/products/?search=phone

# فیلتر + جستجو
GET /api/products/?category=electronics&search=laptop

# فیلتر بازه قیمت + جستجو + مرتب‌سازی
GET /api/products/?category=electronics&price_min=200&price_max=500&search=wireless&ordering=price

# مرتب‌سازی بر اساس امتیاز (بالاترین اول)
GET /api/products/?ordering=-rating

عیب‌یابی: چرا فیلتر یا جستجوی من کار نمی‌کند؟

مشکل راه‌حل
فیلترها اعمال نمی‌شوند DjangoFilterBackend را به filter_backends اضافه کرده‌اید؟
جستجو کار نمی‌کند SearchFilter را اضافه کرده‌اید؟ search_fields را تعریف کرده‌اید؟
مرتب‌سازی کار نمی‌کند OrderingFilter را اضافه کرده‌اید؟ ordering_fields را تعریف کرده‌اید؟
همه چیز تنظیم است اما کار نمی‌کند سرور را ریستارت کنید. تغییرات settings.py نیاز به ریستارت دارد.

تمرین برای این بخش

یک API برای مدل Order (سفارش‌ها) با فیلدهای user، total_price، status (پرداخت شده، در حال پردازش، ارسال شده)، و created_at بسازید.

قابلیت‌های زیر را اضافه کنید:

  • فیلتر بر اساس status و user
  • جستجو در user__username و user__email
  • مرتب‌سازی بر اساس total_price و created_at
  • مرتب‌سازی پیش‌فرض بر اساس -created_at

سپس یک درخواست ترکیبی در Postman تست کنید که هم فیلتر دارد، هم جستجو، هم مرتب‌سازی.

تنظیمات پیش‌فرض در پروژه

تا الان در هر ویو جداگانه filter_backends، search_fields و ordering_fields را تعریف می‌کردیم. این روش خوب است اما یک دردسر دارد.

اگر ده ویو داشته باشید که همه نیاز به جستجو و مرتب‌سازی دارند، باید ده بار کدهای تکراری بنویسید. یک جا را فراموش کنید، آن ویو از بقیه عقب می‌ماند.

خوشبختانه DRF به شما اجازه می‌دهد این تنظیمات را یک بار برای کل پروژه تعریف کنید.

قدم اول: تنظیمات سراسری در settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ]
}

با این سه خط، هر ویوی که از ListAPIView یا ViewSet ارث‌بری کند، به طور خودکار هر سه قابلیت را دارد.

یعنی در ویو دیگر نیازی نیست بنویسید:

filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]

این خط دیگر اضافی است. چون تنظیمات از پروژه می‌آید.

قدم دوم: تنظیمات مخصوص هر ویو

تنظیمات سراسری به این معنی نیست که همه ویوها یکسان رفتار می‌کنند. شما هنوز هم باید مشخص کنید که هر ویو روی چه فیلدهایی فیلتر کند، جستجو کند و مرتب‌سازی کند

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    # اینها را هنوز باید بنویسید (مخصوص هر ویو)
    filterset_fields = ['category', 'status']
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'created_at']
    ordering = ['-created_at']

filter_backends را حذف کرده‌ایم. چون از تنظیمات سراسری می‌آید. بقیه موارد را باید خودتان مشخص کنید.

تنظیمات سراسری برای پیمایش (Pagination)

معمولاً در کنار فیلتر و جستجو، صفحه‌بندی را هم سراسری تنظیم می‌کنید:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}

حالا همه ویوهای لیست، خودکار ۲۰ آیتم در هر صفحه نشان می‌دهند.

چه موقع تنظیمات سراسری خوب است و چه موقع بد؟

مزایا:

  • کد کمتر و تمیزتر
  • یکسان بودن رفتار همه ویوها
  • اگر بعداً تصمیم بگیرید فیلتر جدیدی اضافه کنید، یک جا تغییر می‌دهید

معایب:

  • نمی‌توانید یک ویو خاص را از این قابلیت‌ها محروم کنید (به راحتی)
  • گاهی اضافه بودن این قابلیت‌ها برای بعضی ویوها بی‌معنی است

غیرفعال کردن قابلیت‌ها در یک ویو خاص

فرض کنید یک ویو دارید که نباید هیچ فیلتر، جستجو یا مرتب‌سازی داشته باشد. مثلاً یک API ساده که فقط آخرین مقالات را نشان می‌دهد.

چطور تنظیمات سراسری را برای این ویو خاص کنار بگذارید؟

class LatestArticlesView(ListAPIView):
    queryset = Article.objects.all().order_by('-created_at')[:5]
    serializer_class = ArticleSerializer
    filter_backends = []  # خالی کردن لیست

با filter_backends = [] می‌گویید هیچ فیلتر، جستجو و مرتب‌سازی برای این ویو اعمال نشود.

تنظیمات سراسری + تنظیمات محلی

اگر filter_backends را در ویو تعریف کنید، تنظیمات سراسری را override می‌کند. یعنی دیگر تنظیمات سراسری اعمال نمی‌شوند.

class MyView(ListAPIView):
    filter_backends = [SearchFilter]  # فقط جستجو، نه فیلتر و مرتب‌سازی

اگر می‌خواهید تنظیمات سراسری را نگه دارید و فقط یک قابلیت اضافه کنید، باید همه را لیست کنید:

from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter

class MyView(ListAPIView):
    # اضافه کردن همه قابلیت‌ها + یک قابلیت اضافی
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter, CustomFilter]

یک مثال کامل: تنظیمات پروژه فروشگاهی
settings.py:

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',
        'rest_framework.filters.SearchFilter',
        'rest_framework.filters.OrderingFilter',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20,
}

views.py:

class ProductListView(ListAPIView):
    queryset = Product.objects.filter(is_active=True)
    serializer_class = ProductSerializer
    filterset_fields = ['category', 'brand', 'in_stock']
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'created_at', 'sales_count']
    ordering = ['-sales_count']

class OrderListView(ListAPIView):
    queryset = Order.objects.all()
    serializer_class = OrderSerializer
    filterset_fields = ['status', 'user']
    search_fields = ['user__username', 'user__email']
    ordering_fields = ['total_price', 'created_at']
    ordering = ['-created_at']

class UserListView(ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    search_fields = ['username', 'email', 'first_name', 'last_name']
    ordering_fields = ['date_joined', 'username']
    ordering = ['-date_joined']

توجه کنید که filter_backends را در هیچکدام از ویوها ننوشتیم. همه از تنظیمات سراسری استفاده می‌کنند. فقط موارد خاص هر ویو را مشخص کرده‌ایم.

نکته مهم: فیلتر و جستجو با هم تداخل ندارند

یک سوال رایج: «اگر هم filterset_fields داشته باشم و هم search_fields، کدام اول اجرا می‌شود؟»

ترتیب اجرا همان ترتیبی است که در DEFAULT_FILTER_BACKENDS نوشته شده. در مثال ما:

  • اول DjangoFilterBackend (فیلتر دقیق)
  • بعد SearchFilter (جستجوی متنی)
  • بعد OrderingFilter (مرتب‌سازی)

این ترتیب منطقی است. اول دامنه را با فیلترهای دقیق محدود می‌کنیم، بعد روی آن دامنه جستجو می‌کنیم، بعد مرتب می‌کنیم.

یک اشتباه رایج

برخی توسعه‌دهندگان تازه‌کار DEFAULT_FILTER_BACKENDS را تنظیم می‌کنند اما فراموش می‌کنند django_filters را به INSTALLED_APPS اضافه کنند.

نتیجه: خطای django_filters not found.

دو جا را چک کنید:

INSTALLED_APPS = [
    ...
    'django_filters',  # اینجا
]

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': [
        'django_filters.rest_framework.DjangoFilterBackend',  # و اینجا
        ...
    ]
}

تمرین

پروژه خودتان را بردارید. تنظیمات سراسری فیلتر، جستجو و مرتب‌سازی را به settings.py اضافه کنید.

سپس دو ویو متفاوت بنویسید:

  • یکی برای لیست محصولات با فیلتر category و جستجو در name
  • یکی برای لیست کاربران با جستجو در username و email
  • هیچکدام را filter_backends ننویسید. ببینید آیا قابلیت‌ها کار می‌کنند یا نه.

کدام روش را انتخاب کنیم؟ (مقایسه)

در این درس، چهار روش مختلف برای فیلتر کردن یاد گرفتید.

حالا ممکن است این سؤال برایتان پیش آمده باشد: کدام روش برای پروژه من مناسب است؟

در این بخش، یک راهنمای عملی به شما می‌دهم تا بدون سردرگمی، بهترین روش را انتخاب کنید.

چهار روش در یک نگاه

روش پیچیدگی کنترل حجم کد

مناسب برای

request.query_params دستی کم زیاد زیاد پروژه‌های خیلی کوچک، نیازهای خاص
filterset_fields خیلی کم کم خیلی کم فیلترهای ساده و دقیق
FilterSet سفارشی متوسط زیاد متوسط شرایط مقایسه‌ای، بازه‌ها، فیلد خارجی
SearchFilter + OrderingFilter کم متوسط کم جستجوی متنی و مرتب‌سازی

راهنمای قدم به قدم برای تصمیم‌گیری

سؤال اول: چه نوع فیلتری نیاز دارید؟

اگر فقط فیلتر دقیق (exact) می‌خواهید: ?author=ali

→ بروید سراغ filterset_fields. ساده‌ترین و سریع‌ترین گزینه.

اگر فیلتر مقایسه‌ای نیاز دارید: ?price__lt=100، ?created_at__gte=2024-01-01

→ بروید سراغ FilterSet سفارشی.

اگر روی فیلد خارجی (ForeignKey) فیلتر می‌کنید: ?category_name=technology

→ بروید سراغ FilterSet سفارشی.

سؤال دوم: آیا جستجوی متنی نیاز دارید؟

اگر کاربر باید بتواند یک کلمه را در عنوان یا محتوا جستجو کند:

→ از SearchFilter استفاده کنید. همراه با search_fields.

سؤال سوم: آیا مرتب‌سازی نیاز دارید؟

اگر کاربر باید بتواند نتایج را بر اساس قیمت، تاریخ، یا امتیاز مرتب کند:

→ از OrderingFilter استفاده کنید. همراه با ordering_fields.

سناریوهای واقعی

سناریو ۱: وبلاگ شخصی کوچک

  • تعداد مقالات: کمتر از ۵۰۰
  • نیازها: نمایش مقالات بر اساس دسته‌بندی و نویسنده

فیلتر مقایسه‌ای یا جستجوی پیچیده نیاز نیست

پیشنهاد: filterset_fields کافی است. ساده و بدون دردسر.

filterset_fields = ['category', 'author']

سناریو ۲: فروشگاه اینترنتی متوسط

  • تعداد محصولات: چند هزار
  • نیازها: فیلتر بازه قیمت، جستجو در نام و توضیحات، مرتب‌سازی بر اساس قیمت و امتیاز

پیشنهاد: ترکیب FilterSet سفارشی برای قیمت + SearchFilter + OrderingFilter

class ProductFilter(django_filters.FilterSet):
    price_min = django_filters.NumberFilter(field_name='price', lookup_expr='gte')
    price_max = django_filters.NumberFilter(field_name='price', lookup_expr='lte')
    
    class Meta:
        model = Product
        fields = ['category', 'brand']

class ProductListView(ListAPIView):
    filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
    filterset_class = ProductFilter
    search_fields = ['name', 'description']
    ordering_fields = ['price', 'rating', 'created_at']

سناریو ۳: پنل مدیریت با داده‌های زیاد

  • تعداد رکوردها: ده‌ها هزار
  • نیازها: فیلتر روی فیلدهای زیاد، جستجوی پیشرفته، مرتب‌سازی روی همه فیلدها

پیشنهاد: FilterSet کامل + تنظیمات سراسری در پروژه

# filters.py
class UserFilter(django_filters.FilterSet):
    date_joined_after = django_filters.DateFilter(field_name='date_joined', lookup_expr='gte')
    date_joined_before = django_filters.DateFilter(field_name='date_joined', lookup_expr='lte')
    username_contains = django_filters.CharFilter(field_name='username', lookup_expr='icontains')
    
    class Meta:
        model = User
        fields = ['is_active', 'is_staff', 'groups']

جدول مقایسه بر اساس نیاز

نیاز من روش پیشنهادی دلیل
فیلتر ساده روی ۱-۲ فیلد filterset_fields کمترین کدنویسی
فیلتر بازه (قیمت، تاریخ) FilterSet با NumberFilter/DateFilter نیاز به lookup_expr خاص
فیلتر روی فیلد خارجی FilterSet با field_name='foreign__field' نیاز به عبور از رابطه
جستجوی متنی در عنوان و محتوا SearchFilter از قبل آماده است
مرتب‌سازی روی چند فیلد OrderingFilter از قبل آماده است
ترکیب همه موارد تنظیمات سراسری + FilterSet کامل بهترین عملکرد و کنترل

توصیه من برای شروع

اگر تازه با DRF آشنا شده‌اید، این مسیر را دنبال کنید:

  • مرحله اول: فقط با filterset_fields شروع کنید. ساده است و خیلی از نیازها را پوشش می‌دهد.
  • مرحله دوم: وقتی به فیلتر بازه قیمت یا تاریخ نیاز پیدا کردید، سراغ FilterSet سفارشی بروید.
  • مرحله سوم: جستجو و مرتب‌سازی را با SearchFilter و OrderingFilter اضافه کنید.
  • مرحله چهارم: وقتی پروژه بزرگ شد و چندین ویو داشتید، تنظیمات سراسری را در settings.py فعال کنید.

یک هشدار مهم

از request.query_params دستی استفاده نکنید. مگر اینکه منطق فیلتر شما خیلی خاص باشد و با ابزارهای آماده نشود.

چرا؟ چون باید خطاهای اعتبارسنجی را خودتان مدیریت کنید. چون برای هر فیلد جدید باید کد بنویسید. چون کد شما با بزرگ شدن پروژه غیرقابل نگهداری می‌شود.

django-filter و SearchFilter و OrderingFilter هزاران ساعت کار و تست پشت سر خود دارند. به آنها اعتماد کنید.

خلاصه

  • ساده و سریع: filterset_fields
  • پیشرفته و قابل کنترل: FilterSet سفارشی
  • جستجوی متنی: SearchFilter
  • مرتب‌سازی: OrderingFilter
  • برای کل پروژه: تنظیمات سراسری در settings.py

هیچکدام از این روش‌ها اشتباه نیست. فقط هر کدام جای خود را دارد. با پروژه کوچک شروع کنید و هر وقت نیاز پیدا کردید، روش پیشرفته‌تر را اضافه کنید. لازم نیست از روز اول همه چیز را داشته باشید.

جمع‌بندی درس فیلتر، جستجو و مرتب‌سازی

در این درس، با قابلیت‌هایی آشنا شدید که هر API واقعی به آنها نیاز دارد.

شروع کردیم با request.query_params. روش ساده اما دستی. برای پروژه‌های خیلی کوچک و یک بار مصرف خوب است. اما وقتی پروژه رشد می‌کند، نگهداری آن سخت می‌شود.

بعد سراغ django-filter رفتیم. کتابخانه رسمی و قدرتمندی که فیلتر کردن را حرفه‌ای می‌کند. یاد گرفتید با filterset_fields ساده شروع کنید. بعد برای نیازهای پیشرفته‌تر، FilterSet سفارشی بنویسید.

فیلتر با شرایط مقایسه‌ای را یاد گرفتید. price__lt=100، created_at__gte=2024-01-01. اینها همان چیزهایی هستند که کاربران عادی انتظار دارند.

فیلتر روی فیلدهای خارجی (ForeignKey) را هم اضافه کردیم. با field_name='category__name'، کاربر می‌تواند با اسم دسته‌بندی فیلتر کند، نه با شماره آن.

بعد نوبت جستجو و مرتب‌سازی رسید. SearchFilter کلمه مورد نظر کاربر را در فیلدهای مشخص شده جستجو می‌کند. OrderingFilter هم به کاربر اجازه می‌دهد نتایج را بر اساس هر فیلدی که می‌خواهد مرتب کند.

ترکیب همه اینها را در یک ویو یاد گرفتید. کاربر می‌تواند یک درخواست بفرستد که هم فیلتر دارد، هم جستجو، هم مرتب‌سازی. بدون اینکه شما کد اضافه‌ای بنویسید.

تنظیمات سراسری را در settings.py اضافه کردیم تا مجبور نباشید در هر ویو filter_backends را تکرار کنید.

و در انتها، یک راهنمای عملی دادیم تا بدانید هر کدام از این روش‌ها را چه موقع استفاده کنید.

از این درس چه چیزی باید به خاطر بسپارید؟

۱. برای فیلترهای ساده، filterset_fields کافی است. برای شرایط مقایسه‌ای و فیلدهای خارجی، FilterSet سفارشی بنویسید.

۲. SearchFilter و OrderingFilter را حتماً بلد باشید. تقریباً در هر پروژه‌ای به آنها نیاز خواهید داشت.

۳. تنظیمات سراسری در settings.py کار شما را در پروژه‌های بزرگ خیلی راحت می‌کند.

۴. از request.query_params دستی فقط برای منطق‌های خیلی خاص استفاده کنید. در غیر این صورت، از ابزارهای آماده استفاده کنید.

درس بعدی چیست؟

حالا که کاربر می‌تواند داده‌ها را فیلتر، جستجو و مرتب کند، یک مشکل دیگر باقی مانده است.

اگر دیتابیس شما هزاران رکورد داشته باشد، برگرداندن همه آنها در یک پاسخ منطقی نیست. هم سرور را سنگین می‌کند، هم کاربر مجبور است منتظر بماند.

اینجا صفحه‌بندی (Pagination) وارد می‌شود.

در درس بعدی، یاد می‌گیرید چطور پاسخ API خود را به صفحات کوچک تقسیم کنید. کاربر بتواند صفحه اول را بگیرد، بعد صفحه دوم، و الی آخر. بدون اینکه یکباره همه داده‌ها منتقل شوند.

برای درس بعدی، همین پروژه فعلی را نگه دارید. فقط چند ده رکورد نمونه به آن اضافه کنید تا صفحه‌بندی را به خوبی تست کنید.