Django-Filter 终极指南:构建强大的数据过滤系统

Django-Filter 终极指南:构建强大的数据过滤系统

【免费下载链接】django-filter A generic system for filtering Django QuerySets based on user selections 【免费下载链接】django-filter 项目地址: https://gitcode.com/gh_mirrors/dj/django-filter

还在为 Django 项目中的复杂数据过滤需求而烦恼吗?每次都需要手动编写繁琐的查询逻辑?django-filter 为你提供了优雅的解决方案!本文将带你从零开始,全面掌握这个强大的 Django 过滤库。

🎯 读完本文你将获得

  • django-filter 核心概念与架构深度解析
  • 基础到高级的 FilterSet 配置实战指南
  • Django REST Framework 无缝集成技巧
  • 自定义过滤逻辑与高级用例实现
  • 性能优化与最佳实践总结

📦 快速安装与环境配置

首先安装 django-filter:

pip install django-filter

在 Django 项目的 settings.py 中添加配置:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_filters',  # 添加 django-filter
]

# 可选:配置默认查找表达式
FILTERS_DEFAULT_LOOKUP_EXPR = 'icontains'

🔧 核心概念:FilterSet 架构解析

django-filter 的核心是 FilterSet 类,它类似于 Django 的 ModelForm,但专门用于数据过滤。让我们通过一个完整的示例来理解其架构:

mermaid

🚀 基础实战:构建第一个过滤系统

数据模型定义

假设我们有一个电商产品模型:

from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)
    description = models.TextField()

class Manufacturer(models.Model):
    name = models.CharField(max_length=100)
    country = models.CharField(max_length=50)

class Product(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock_quantity = models.IntegerField(default=0)
    is_available = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    manufacturer = models.ForeignKey(Manufacturer, on_delete=models.CASCADE)
    
    def __str__(self):
        return self.name

基础 FilterSet 实现

import django_filters
from .models import Product

class ProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = ['name', 'price', 'is_available', 'category', 'manufacturer']

视图层集成

from django.shortcuts import render
from django.views.generic import ListView
from .models import Product
from .filters import ProductFilter

# 函数视图方式
def product_list(request):
    product_filter = ProductFilter(request.GET, queryset=Product.objects.all())
    return render(request, 'products/list.html', {'filter': product_filter})

# 类视图方式
class ProductListView(ListView):
    model = Product
    template_name = 'products/list.html'
    context_object_name = 'products'
    
    def get_queryset(self):
        queryset = super().get_queryset()
        self.filterset = ProductFilter(self.request.GET, queryset=queryset)
        return self.filterset.qs
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['filter'] = self.filterset
        return context

模板渲染

<!-- products/list.html -->
{% extends "base.html" %}

{% block content %}
<h2>产品筛选</h2>

<!-- 筛选表单 -->
<form method="get" class="row g-3">
    <div class="col-md-3">
        {{ filter.form.name.label_tag }}
        {{ filter.form.name }}
    </div>
    <div class="col-md-2">
        {{ filter.form.price.label_tag }}
        {{ filter.form.price }}
    </div>
    <div class="col-md-2">
        {{ filter.form.is_available.label_tag }}
        {{ filter.form.is_available }}
    </div>
    <div class="col-md-3">
        {{ filter.form.category.label_tag }}
        {{ filter.form.category }}
    </div>
    <div class="col-md-2 align-self-end">
        <button type="submit" class="btn btn-primary">筛选</button>
        <a href="?" class="btn btn-secondary">重置</a>
    </div>
</form>

<hr>

<!-- 筛选结果 -->
<div class="row mt-4">
    {% for product in filter.qs %}
    <div class="col-md-4 mb-4">
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">{{ product.name }}</h5>
                <p class="card-text">{{ product.description|truncatewords:20 }}</p>
                <p class="text-primary">¥{{ product.price }}</p>
                <p class="text-muted">
                    库存: {{ product.stock_quantity }} | 
                    分类: {{ product.category.name }}
                </p>
            </div>
        </div>
    </div>
    {% empty %}
    <div class="col-12">
        <div class="alert alert-info">没有找到匹配的产品</div>
    </div>
    {% endfor %}
</div>
{% endblock %}

🎛️ 高级配置:灵活控制过滤行为

1. 精确控制字段查找表达式

class AdvancedProductFilter(django_filters.FilterSet):
    # 自定义字段名称和查找表达式
    product_name = django_filters.CharFilter(
        field_name='name', 
        lookup_expr='icontains',
        label='产品名称包含'
    )
    
    min_price = django_filters.NumberFilter(
        field_name='price', 
        lookup_expr='gte',
        label='最低价格'
    )
    
    max_price = django_filters.NumberFilter(
        field_name='price', 
        lookup_expr='lte',
        label='最高价格'
    )
    
    # 日期范围过滤
    created_after = django_filters.DateFilter(
        field_name='created_at',
        lookup_expr='gte',
        label='创建时间之后'
    )
    
    created_before = django_filters.DateFilter(
        field_name='created_at',
        lookup_expr='lte',
        label='创建时间之前'
    )
    
    # 关联模型过滤
    manufacturer_country = django_filters.CharFilter(
        field_name='manufacturer__country',
        lookup_expr='iexact',
        label='制造商国家'
    )
    
    class Meta:
        model = Product
        fields = {
            'stock_quantity': ['exact', 'gt', 'lt'],
            'category': ['exact'],
        }

2. Meta.fields 的多种配置方式

class ProductFilter(django_filters.FilterSet):
    
    class Meta:
        model = Product
        
        # 方式1:简单字段列表(默认使用exact查找)
        fields = ['name', 'price', 'category']
        
        # 方式2:字典配置,支持多个查找表达式
        fields = {
            'name': ['exact', 'icontains', 'istartswith'],
            'price': ['exact', 'gt', 'lt', 'gte', 'lte'],
            'created_at': ['exact', 'year', 'year__gt', 'year__lt'],
            'stock_quantity': ['exact', 'gt', 'lt'],
        }
        
        # 方式3:混合使用
        fields = ['name', 'is_available', 
                 {'price': ['gt', 'lt'], 
                  'created_at': ['year__gt', 'year__lt']}]

3. 自定义过滤方法

class CustomProductFilter(django_filters.FilterSet):
    search = django_filters.CharFilter(method='custom_search', label='综合搜索')
    
    class Meta:
        model = Product
        fields = ['category', 'manufacturer']
    
    def custom_search(self, queryset, name, value):
        """
        自定义搜索方法:同时搜索名称、描述和制造商名称
        """
        return queryset.filter(
            models.Q(name__icontains=value) |
            models.Q(description__icontains=value) |
            models.Q(manufacturer__name__icontains=value)
        )
    
    @property
    def qs(self):
        """
        重写qs属性,添加额外的过滤逻辑
        """
        queryset = super().qs
        
        # 示例:只返回有库存的产品
        if not self.request.user.is_staff:
            queryset = queryset.filter(stock_quantity__gt=0)
            
        return queryset

🔗 Django REST Framework 集成

1. 基础 DRF 集成

# filters.py
from django_filters import rest_framework as filters
from .models import Product

class ProductFilter(filters.FilterSet):
    min_price = filters.NumberFilter(field_name="price", lookup_expr='gte')
    max_price = filters.NumberFilter(field_name="price", lookup_expr='lte')
    in_stock = filters.BooleanFilter(field_name='stock_quantity', 
                                   method='filter_in_stock')
    
    class Meta:
        model = Product
        fields = ['category', 'manufacturer', 'is_available']
    
    def filter_in_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock_quantity__gt=0)
        return queryset.filter(stock_quantity=0)

# views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer
from .filters import ProductFilter

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    filter_backends = [DjangoFilterBackend]
    filterset_class = ProductFilter
    
    # 或者使用简写方式
    # filterset_fields = ['category', 'manufacturer', 'is_available']

2. 配置默认过滤后端

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

3. 自定义过滤后端

from django_filters.rest_framework import DjangoFilterBackend

class CustomFilterBackend(DjangoFilterBackend):
    def get_filterset_kwargs(self, request, queryset, view):
        kwargs = super().get_filterset_kwargs(request, queryset, view)
        
        # 添加额外的上下文信息
        kwargs['request'] = request
        kwargs['user'] = request.user
        
        return kwargs

# 在视图中使用
class ProductViewSet(viewsets.ModelViewSet):
    filter_backends = [CustomFilterBackend]
    filterset_class = ProductFilter

🎨 自定义过滤器与部件

1. 创建自定义过滤器

from django_filters import Filter
from django.db.models import Q

class MultiFieldSearchFilter(Filter):
    def __init__(self, fields=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields = fields or []
    
    def filter(self, qs, value):
        if not value:
            return qs
        
        query = Q()
        for field in self.fields:
            query |= Q(**{f"{field}__icontains": value})
        
        return qs.filter(query)

# 使用自定义过滤器
class ProductFilter(django_filters.FilterSet):
    global_search = MultiFieldSearchFilter(
        fields=['name', 'description', 'manufacturer__name'],
        label='全局搜索'
    )
    
    class Meta:
        model = Product
        fields = []

2. 自定义部件(Widget)

from django import forms
from django_filters.widgets import RangeWidget

class DateRangeWidget(forms.MultiWidget):
    def __init__(self, attrs=None):
        widgets = [
            forms.DateInput(attrs={'placeholder': '开始日期', 'type': 'date'}),
            forms.DateInput(attrs={'placeholder': '结束日期', 'type': 'date'})
        ]
        super().__init__(widgets, attrs)
    
    def decompress(self, value):
        if value:
            return [value.start, value.stop]
        return [None, None]

class DateRangeFilter(django_filters.DateFromToRangeFilter):
    widget = DateRangeWidget

📊 性能优化与最佳实践

1. 查询优化策略

class OptimizedProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = ['category', 'manufacturer', 'is_available']
    
    @property
    def qs(self):
        queryset = super().qs
        
        # 使用select_related减少查询次数
        queryset = queryset.select_related('category', 'manufacturer')
        
        # 使用prefetch_related处理多对多关系
        # queryset = queryset.prefetch_related('tags')
        
        # 只选择需要的字段
        queryset = queryset.only(
            'name', 'price', 'stock_quantity', 
            'category__name', 'manufacturer__name'
        )
        
        return queryset

2. 分页与缓存策略

from django.core.cache import cache
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

class ProductListView(ListView):
    model = Product
    paginate_by = 20
    filterset_class = ProductFilter
    
    @method_decorator(cache_page(60 * 15))  # 缓存15分钟
    def dispatch(self, *args, **kwargs):
        return super().dispatch(*args, **kwargs)
    
    def get_queryset(self):
        # 生成缓存键
        cache_key = f"products_{hash(frozenset(self.request.GET.items()))}"
        
        # 尝试从缓存获取
        cached_qs = cache.get(cache_key)
        if cached_qs is not None:
            return cached_qs
        
        # 执行过滤
        queryset = super().get_queryset()
        self.filterset = self.filterset_class(self.request.GET, queryset=queryset)
        filtered_qs = self.filterset.qs
        
        # 缓存结果
        cache.set(cache_key, filtered_qs, 60 * 15)
        
        return filtered_qs

3. 安全性考虑

class SecureProductFilter(django_filters.FilterSet):
    class Meta:
        model = Product
        fields = {
            'price': ['exact', 'lt', 'gt'],
            'created_at': ['exact', 'lt', 'gt'],
        }
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # 限制查询范围,防止数据泄露
        user = getattr(self.request, 'user', None)
        if user and not user.is_staff:
            self.queryset = self.queryset.filter(is_available=True)

🧪 测试策略

from django.test import TestCase
from django.urls import reverse
from .models import Product, Category, Manufacturer
from .filters import ProductFilter

class ProductFilterTests(TestCase):
    def setUp(self):
        self.category = Category.objects.create(name="电子产品")
        self.manufacturer = Manufacturer.objects.create(
            name="测试厂商", country="中国"
        )
        
        self.product1 = Product.objects.create(
            name="智能手机",
            price=2999.00,
            stock_quantity=10,
            category=self.category,
            manufacturer=self.manufacturer
        )
        
        self.product2 = Product.objects.create(
            name="笔记本电脑",
            price=5999.00,
            stock_quantity=0,
            category=self.category,
            manufacturer=self.manufacturer
        )
    
    def test_name_filter(self):
        # 测试名称过滤
        filterset = ProductFilter(
            {'name': '手机'}, 
            queryset=Product.objects.all()
        )
        self.assertEqual(filterset.qs.count(), 1)
        self.assertEqual(filterset.qs.first().name, "智能手机")
    
    def test_price_range_filter(self):
        # 测试价格范围过滤
        filterset = ProductFilter(
            {'price__gt': 3000, 'price__lt': 7000},
            queryset=Product.objects.all()
        )
        self.assertEqual(filterset.qs.count(), 1)
        self.assertEqual(filterset.qs.first().name, "笔记本电脑")
    
    def test_in_stock_filter(self):
        # 测试库存过滤
        filterset = ProductFilter(
            {'stock_quantity__gt': 0},
            queryset=Product.objects.all()
        )
        self.assertEqual(filterset.qs.count(), 1)
        self.assertEqual(filterset.qs.first().name, "智能手机")

📈 实战案例:电商平台商品筛选系统

需求分析

mermaid

完整实现代码

# filters.py
import django_filters
from django.db import models
from .models import Product, Category, Manufacturer

class EcommerceProductFilter(django_filters.FilterSet):
    # 基础字段过滤
    name = django_filters.CharFilter(lookup_expr='icontains', label='商品名称')
    min_price = django_filters.NumberFilter(field_name='price', lookup_expr='gte', label='最低价')
    max_price = django_filters.NumberFilter(field_name='price', lookup_expr='lte', label='最高价')
    
    # 多选过滤
    categories = django_filters.ModelMultipleChoiceFilter(
        field_name='category',
        queryset=Category.objects.all(),
        label='商品分类'
    )
    
    manufacturers = django_filters.ModelMultipleChoiceFilter(
        field_name='manufacturer',
        queryset=Manufacturer.objects.all(),
        label='品牌'
    )
    
    # 库存状态过滤
    in_stock = django_filters.BooleanFilter(
        field_name='stock_quantity',
        method='filter_in_stock',
        label='仅显示有货'
    )
    
    # 排序选项
    ORDERING_CHOICES = [
        ('price', '价格从低到高'),
        ('-price', '价格从高到低'),
        ('name', '名称A-Z'),
        ('-name', '名称Z-A'),
        ('-created_at', '最新上架'),
    ]
    
    ordering = django_filters.OrderingFilter(
        choices=ORDERING_CHOICES,
        label='排序方式'
    )
    
    class Meta:
        model = Product
        fields = []
    
    def filter_in_stock(self, queryset, name, value):
        if value:
            return queryset.filter(stock_quantity__gt=0)
        return queryset
    
    @property
    def qs(self):
        queryset = super().qs
        
        # 优化查询性能
        queryset = queryset.select_related('category', 'manufacturer')
        queryset = queryset.only(
            'name', 'price', 'stock_quantity', 'image',
            'category__name', 'manufacturer__name'
        )
        
        return queryset

# views.py
from django.views.generic import ListView
from django.core.paginator import Paginator
from .models import Product
from .filters import EcommerceProductFilter

class ProductListView(ListView):
    model = Product
    template_name = 'ecommerce/product_list.html'
    context_object_name = 'products'
    paginate_by = 24
    
    def get_queryset(self):
        queryset = super().get_queryset()
        self.filterset = EcommerceProductFilter(self.request.GET, queryset=queryset)
        return self.filterset.qs
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['filter'] = self.filterset
        
        # 添加分页信息
        paginator = Paginator(self.filterset.qs, self.paginate_by)
        page_number = self.request.GET.get('page')
        page_obj = paginator.get_page(page_number)
        context['page_obj'] = page_obj
        
        return context

🎯 总结与展望

django-filter 是一个功能强大且灵活的 Django 过滤库,通过本文的学习,你应该已经掌握了:

核心优势

  • 声明式配置:类似 ModelForm 的简洁 API
  • 高度可扩展:支持自定义过滤器和部件
  • DRF 无缝集成:完美的 REST API 支持
  • 性能优化:内置查询优化机制

最佳实践

  1. 合理使用 Meta.fields:根据需求选择列表或字典配置
  2. 优化查询性能:使用 select_related 和 prefetch_related
  3. 安全性考虑:限制敏感数据的访问
  4. 用户体验:提供清晰的标签和占位符文本

未来发展方向

  • 支持更复杂的数据类型过滤
  • 增强前端 JavaScript 集成
  • 提供更多的内置过滤器类型
  • 改进性能监控和调试工具

通过合理运用 django-filter,你可以轻松构建出功能强大、用户体验优秀的筛选系统,大幅提升开发效率和项目质量。


提示:如果本文对你有帮助,请点赞收藏支持!如有任何问题,欢迎在评论区讨论交流。

【免费下载链接】django-filter A generic system for filtering Django QuerySets based on user selections 【免费下载链接】django-filter 项目地址: https://gitcode.com/gh_mirrors/dj/django-filter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值