Django学习之旅--第13课:Django模型关系进阶与查询优化实战

发布于:2025-07-24 ⋅ 阅读:(16) ⋅ 点赞:(0)

在Django开发中,模型关系设计与查询性能直接决定了系统的扩展性和效率。当业务场景从简单的数据存储升级为复杂的关联分析(如订单统计、用户行为分析)时,基础的模型关系和查询方式已无法满足需求。本节课将深入讲解模型关系的高级用法、复杂查询技巧与性能优化策略,通过实战案例展示如何设计灵活的关联结构并高效处理数据关联操作。

一、模型关系高级用法

Django的模型关系(外键、多对多、一对一)是构建复杂数据结构的基础,掌握其高级配置能显著提升数据模型的灵活性。

1.1 外键关系(ForeignKey)深度应用

外键用于关联两个模型(如Order关联Service),通过自定义配置可实现更精细的关系管理。

1.1.1 自定义反向查询名称

默认情况下,外键的反向查询名称为模型名_set(如service.order_set.all()),通过related_name可自定义更直观的名称:

# orders/models.py
from django.db import models
from services.models import Service

class Order(models.Model):
    service = models.ForeignKey(
        Service,
        on_delete=models.CASCADE,
        related_name='orders',  # 自定义反向名称:service.orders.all()
        verbose_name="关联服务"
    )
    # 其他字段:用户、价格、状态等
    user = models.ForeignKey('users.User', on_delete=models.CASCADE)
    total_price = models.DecimalField(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=1, choices=[('P', '待支付'), ('O', '处理中'), ('C', '已完成')])

# 使用示例:查询某个服务的所有订单
service = Service.objects.get(id=1)
orders = service.orders.filter(status='C')  # 直接通过related_name查询

优势:反向查询更直观,代码可读性更高(service.ordersservice.order_set更易理解)。

1.1.2 级联删除策略

外键的on_delete参数控制当主表记录被删除时,关联表的行为。不同场景需选择不同策略:

策略 描述 适用场景
CASCADE 删除主对象时,关联对象一并删除 订单关联服务:服务删除后,关联的历史订单无意义,应一并删除
PROTECT 阻止删除主对象(触发ProtectedError 服务关联分类:分类有服务时不允许删除,避免数据孤立
SET_NULL 外键设为NULL(需设置null=True 用户关联头像:用户删除后,头像记录保留但关联用户设为NULL
SET_DEFAULT 外键设为默认值(需设置default 订单关联优惠:优惠删除后,订单优惠设为"无优惠"默认值
SET(func) 外键设为函数返回值 动态获取默认值(如关联最新的替代服务)
DO_NOTHING 不做任何操作(依赖数据库约束) 需数据库级联控制的特殊场景(谨慎使用)

示例:保护分类不被随意删除(避免服务关联的分类丢失):

# services/models.py
class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Service(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,  # 分类有服务时禁止删除
        related_name='services'
    )
    # 其他字段...

1.2 多对多关系(ManyToManyField)高级配置

多对多关系用于两个模型的多向关联(如ServiceTag),默认通过中间表维护关系,复杂场景可自定义中间模型存储额外关联信息。

1.2.1 使用中间模型存储关联信息

默认的多对多关系仅记录关联,若需存储"谁添加的标签"、"添加时间"等信息,需通过through指定中间模型:

# services/models.py
from django.db import models
from django.contrib.auth.models import User

class Service(models.Model):
    name = models.CharField(max_length=200)
    # 多对多关联标签,通过ServiceTag中间模型
    tags = models.ManyToManyField(
        'Tag',
        through='ServiceTag',  # 指定中间模型
        through_fields=('service', 'tag'),  # 中间模型的外键字段
        related_name='services'
    )

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)  # 标签名唯一

# 中间模型:存储关联的额外信息
class ServiceTag(models.Model):
    service = models.ForeignKey(Service, on_delete=models.CASCADE)  # 关联服务
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)  # 关联标签
    added_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)  # 谁添加的
    added_at = models.DateTimeField(auto_now_add=True)  # 添加时间

    class Meta:
        unique_together = ('service', 'tag')  # 避免服务重复关联同一标签

使用中间模型操作关联

# 添加标签(含添加人信息)
service = Service.objects.get(id=1)
tag = Tag.objects.get(name="设计")
ServiceTag.objects.create(
    service=service,
    tag=tag,
    added_by=request.user  # 记录操作人
)

# 查询服务的标签及添加信息
service_tags = ServiceTag.objects.filter(
    service=service
).select_related('tag', 'added_by')
for st in service_tags:
    print(f"标签: {st.tag.name}, 添加人: {st.added_by.username}")

1.3 一对一关系(OneToOneField)应用场景

一对一关系用于两个模型的唯一关联(如UserUserProfile),常见于扩展用户信息或拆分复杂模型。

1.3.1 扩展用户模型

Django内置User模型字段有限,通过一对一关系可扩展用户资料(不修改原模型):

# users/models.py
from django.db import models
from django.contrib.auth.models import User

class UserProfile(models.Model):
    # 一对一关联User,用户删除时资料也删除
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='profile'  # 反向查询:user.profile
    )
    bio = models.TextField(blank=True)  # 个人简介
    website = models.URLField(blank=True)  # 个人网站
    social_media = models.JSONField(default=dict)  # 社交媒体账号(JSON格式)

# 使用示例:获取用户的扩展资料
user = User.objects.get(username="test")
print(user.profile.bio)  # 直接通过related_name访问
1.3.2 代理模型(Proxy Model)

若需为模型添加额外方法但不修改表结构,可使用代理模型(不创建新表,仅扩展功能):

# users/models.py
class PremiumUser(User):
    class Meta:
        proxy = True  # 声明为代理模型(不创建新表)

    def get_premium_discount(self):
        """ premium用户专属折扣计算 """
        return 0.8  # 8折优惠

# 使用示例:对普通User实例调用代理模型方法
user = User.objects.get(username="premium_user")
premium_user = PremiumUser.objects.get(username="premium_user")
print(premium_user.get_premium_discount())  # 0.8

二、复杂查询技巧

当业务需求涉及多条件筛选、字段比较或关联判断时,基础的filter()已无法满足,需使用Q对象F表达式子查询等高级工具。

2.1 Q对象:组合复杂条件

Q对象用于构建AND/OR逻辑的复杂查询条件(基础filter()仅支持AND):

from django.db.models import Q
from services.models import Service

# 示例1:查找价格低于1000元 或 高于5000元的服务
cheap_or_expensive = Service.objects.filter(
    Q(price__lt=1000) | Q(price__gt=5000)  # OR逻辑
)

# 示例2:查找"设计"类服务(价格500-2000元) 或 "开发"类服务
design_or_dev = Service.objects.filter(
    (Q(name__icontains="设计") & Q(price__gte=500) & Q(price__lte=2000)) |  # AND组合
    Q(category__code="DV")  # 分类为"开发"
)

注意Q对象需导入from django.db.models import Q,且|表示OR&表示AND~表示NOT

2.2 F表达式:字段间比较

F表达式用于模型字段间的比较(如"折扣价低于原价的80%"),避免Python层面的二次查询:

from django.db.models import F
from services.models import Service

# 示例1:查找折扣价 < 原价*0.8 的服务
discounted_services = Service.objects.filter(
    discount_price__lt=F('price') * 0.8  # F('price')表示引用自身price字段
)

# 示例2:批量更新订单总价(含100元服务费)
from orders.models import Order
Order.objects.filter(status='P').update(
    total_price=F('service__price') + 100  # 引用关联模型的price字段
)

优势F表达式直接在数据库层面执行计算,避免先查询再更新的"竞态条件"(如多用户同时操作时的数据不一致)。

2.3 子查询与Exists:关联存在性判断

当需要判断"是否存在关联记录"(如"有订单的服务"、“有5星评价的服务”)时,使用Exists子查询更高效:

from django.db.models import Exists, OuterRef
from services.models import Service
from orders.models import Order
from reviews.models import Review

# 示例1:查找有订单的服务(比service.orders.exists()更高效)
services_with_orders = Service.objects.filter(
    Exists(Order.objects.filter(service_id=OuterRef('id')))  # 子查询:订单的service_id=服务id
)

# 示例2:查找有5星评价的服务
services_with_5star = Service.objects.filter(
    Exists(Review.objects.filter(
        service_id=OuterRef('id'),
        rating=5  # 评分=5
    ))
)

优势Exists仅判断存在性,不返回具体关联数据,比annotate(count=Count('reviews'))更高效。

三、聚合与注解:数据分析基础

聚合(aggregate)和注解(annotate)是处理数据统计的核心工具:聚合用于计算全局汇总值(如"平均价格"),注解用于为每个对象添加计算字段(如"每个服务的订单数")。

3.1 基本聚合函数

常用聚合函数:Count(计数)、Avg(平均值)、Sum(总和)、Max(最大值)、Min(最小值)。

from django.db.models import Count, Avg, Sum, Max, Min
from services.models import Service

# 示例1:全局统计
stats = Service.objects.aggregate(
    total=Count('id'),  # 服务总数
    avg_price=Avg('price'),  # 平均价格
    max_price=Max('price'),  # 最高价格
    total_revenue=Sum('orders__total_price')  # 关联订单的总销售额
)
# stats结果:{'total': 100, 'avg_price': 1500.0, ...}

# 示例2:单值聚合(如服务总数)
service_count = Service.objects.count()  # 等价于Service.objects.aggregate(c=Count('id'))['c']

3.2 分组聚合(按字段分组统计)

通过values()指定分组字段,结合annotate()实现分组统计(如"按分类统计服务数量"):

# 按分类统计服务数量和平均价格
from django.db.models import Count, Avg

category_stats = Service.objects.values('category__name')  # 按分类名称分组
    .annotate(
        service_count=Count('id'),  # 每组的服务数
        avg_price=Avg('price')  # 每组的平均价格
    )
    .order_by('-service_count')  # 按服务数降序

# 结果示例:
# [
#   {'category__name': '设计', 'service_count': 30, 'avg_price': 1200.0},
#   {'category__name': '开发', 'service_count': 25, 'avg_price': 2000.0},
#   ...
# ]

关键values()必须放在annotate()之前,用于指定分组字段;annotate()为每个分组添加统计字段。

3.3 条件聚合(按条件统计)

使用CaseWhen实现条件统计(如"统计不同状态的订单数量"):

from django.db.models import Case, When, IntegerField, Sum, Count
from orders.models import Order

# 示例1:统计不同状态的订单数量
status_counts = Order.objects.aggregate(
    pending=Sum(Case(When(status='P', then=1), output_field=IntegerField())),
    processing=Sum(Case(When(status='O', then=1), output_field=IntegerField())),
    completed=Sum(Case(When(status='C', then=1), output_field=IntegerField()))
)
# 结果:{'pending': 20, 'processing': 15, 'completed': 100}

# 示例2:按价格区间分组统计服务数量
price_ranges = Service.objects.annotate(
    range=Case(
        When(price__lt=1000, then='低价'),
        When(price__gte=1000, price__lt=5000, then='中价'),
        When(price__gte=5000, then='高价'),
        default='未知',
        output_field=models.CharField()
    )
).values('range').annotate(count=Count('id'))
# 结果:[{'range': '低价', 'count': 40}, ...]

四、查询性能优化:避免N+1问题

关联查询时,若不优化会导致"N+1查询问题"(1次查主表,N次查关联表),通过select_relatedprefetch_related可显著提升效率。

4.1 select_related:优化一对一/外键关联

select_related通过SQLJOIN一次性加载主表和关联表数据,适用于一对一外键关系(多对一):

# 优化前:查询10个订单,每个订单访问service会触发1次查询(共11次)
orders = Order.objects.all()[:10]
for order in orders:
    print(order.service.name)  # 每次循环触发新查询

# 优化后:1次查询加载所有订单及关联服务(共1次)
orders = Order.objects.select_related('service')[:10]  # 预加载外键关联
for order in orders:
    print(order.service.name)  # 无额外查询

支持多层关联

# 预加载"订单→服务→服务商"多层关联
orders = Order.objects.select_related(
    'service',  # 第一层:订单关联的服务
    'service__provider'  # 第二层:服务关联的服务商
)[:10]
# 访问多层关联无额外查询
for order in orders:
    print(order.service.provider.name)

4.2 prefetch_related:优化多对多/反向关联

prefetch_related通过Python代码合并查询结果,适用于多对多反向关联(如"服务的所有标签"、“服务的所有订单”):

# 优化前:查询10个服务,每个服务访问tags触发1次查询(共11次)
services = Service.objects.all()[:10]
for service in services:
    print([tag.name for tag in service.tags.all()])  # 每次循环触发新查询

# 优化后:2次查询(服务+标签)
services = Service.objects.prefetch_related('tags')[:10]  # 预加载多对多关联
for service in services:
    print([tag.name for tag in service.tags.all()])  # 无额外查询

自定义预取条件

from django.db.models import Prefetch

# 仅预加载"已完成"状态的订单
services = Service.objects.prefetch_related(
    Prefetch(
        'orders',  # 关联名称
        queryset=Order.objects.filter(status='C'),  # 筛选条件
        to_attr='completed_orders'  # 自定义属性名(默认是orders)
    )
)[:10]

# 使用自定义属性
for service in services:
    print(f"已完成订单数:{len(service.completed_orders)}")

五、实战:订单分析系统

结合模型关系和查询技巧,实现一个订单数据分析系统,包括分类销售额统计、服务商排名和月度报告。

5.1 按服务类别统计销售额

# analytics/views.py
from django.db.models import Sum, F, Count
from django.shortcuts import render
from services.models import Service

def category_sales(request):
    # 按分类统计销售额、订单数
    categories = Service.objects.values('category__name')
        .annotate(
            total_sales=Sum(F('orders__total_price')),  # 分类总销售额
            order_count=Count('orders')  # 分类总订单数
        )
        .filter(total_sales__gt=0)  # 排除无销售的分类
        .order_by('-total_sales')  # 按销售额降序

    # 计算总销售额和各分类占比
    total_sales = sum(cat['total_sales'] for cat in categories)
    for cat in categories:
        cat['percentage'] = round(cat['total_sales'] / total_sales * 100, 1)  # 占比(%)

    return render(request, 'analytics/category_sales.html', {
        'categories': categories,
        'total_sales': total_sales
    })

模板展示:通过表格或饼图展示各分类销售额及占比,帮助商家了解热门分类。

5.2 服务商收入排名

# analytics/views.py
from django.db.models import Sum, Count
from django.shortcuts import render
from users.models import User

def provider_ranking(request):
    # 服务商收入排名(取前10)
    providers = User.objects.filter(
        is_service_provider=True  # 筛选服务商用户
    ).annotate(
        total_income=Sum('services__orders__total_price'),  # 总收入
        order_count=Count('services__orders')  # 总订单数
    ).filter(
        total_income__gt=0  # 排除无收入的服务商
    ).order_by('-total_income')[:10]  # 按收入降序

    return render(request, 'analytics/provider_ranking.html', {
        'providers': providers
    })

5.3 月度销售报告(含环比增长)

# analytics/views.py
from django.db.models import Sum, Count
from django.db.models.functions import TruncMonth
from django.shortcuts import render
from orders.models import Order

def monthly_sales_report(request):
    # 按月统计销售额和订单数
    monthly_sales = Order.objects.annotate(
        month=TruncMonth('created_at')  # 按月份分组(截断到月份)
    ).values('month').annotate(
        total_sales=Sum('total_price'),  # 月度销售额
        order_count=Count('id')  # 月度订单数
    ).order_by('-month')  # 按月份降序

    # 计算环比增长率(当前月较上月的增长百分比)
    prev_sales = None
    for month in monthly_sales:
        if prev_sales:
            # 增长率 = (当前销售额 - 上月销售额) / 上月销售额 * 100%
            month['growth_rate'] = round(
                (month['total_sales'] - prev_sales) / prev_sales * 100, 1
            )
        else:
            month['growth_rate'] = None  # 第一个月无增长率
        prev_sales = month['total_sales']

    return render(request, 'analytics/monthly_report.html', {
        'monthly_sales': monthly_sales
    })

六、高级关系设计模式

针对特殊业务场景,Django支持自关联、通用关系和多表继承等高级设计模式,满足复杂数据结构需求。

6.1 自关联:模型关联自身

适用于"父子关系"场景(如"服务套餐包含子服务"、“评论的回复”):

# services/models.py
class ServicePackage(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    # 自关联:套餐可包含子套餐(外键指向自身)
    parent = models.ForeignKey(
        'self',
        on_delete=models.CASCADE,
        related_name='sub_packages',
        null=True,
        blank=True
    )

# 使用示例:查询套餐的所有子套餐
package = ServicePackage.objects.get(name="高级设计套餐")
sub_packages = package.sub_packages.all()  # 通过related_name查询子套餐

6.2 通用关系:关联任意模型

当需要为多个模型添加通用功能(如"评论"可关联"服务"、“订单”、“案例”)时,使用GenericForeignKey

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType

class Comment(models.Model):
    # 通用关联的核心字段
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)  # 关联模型类型
    object_id = models.PositiveIntegerField()  # 关联模型的ID
    content_object = GenericForeignKey('content_type', 'object_id')  # 通用外键

    # 评论内容
    text = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

# 使用示例:为服务和订单添加评论
service = Service.objects.get(id=1)
order = Order.objects.get(id=1)

# 给服务添加评论
Comment.objects.create(
    content_object=service,
    text="这个服务很棒!"
)

# 给订单添加评论
Comment.objects.create(
    content_object=order,
    text="订单处理很快"
)

# 查询服务的所有评论
service_comments = Comment.objects.filter(
    content_type=ContentType.objects.get_for_model(Service),
    object_id=service.id
)

七、性能优化实践

除了查询优化,还可通过工具监控、批量操作和索引优化进一步提升性能。

7.1 使用django-query-profiler监控查询

django-query-profiler可记录查询次数、耗时和堆栈信息,帮助定位慢查询:

# 安装
pip install django-query-profiler
# settings.py
MIDDLEWARE = [
    # ...其他中间件
    'query_profiler.middleware.QueryProfilerMiddleware',  # 添加监控中间件
]

QUERY_PROFILER = {
    'SHOW_TRACEBACKS': True,  # 显示查询堆栈
    'TRACEBACK_ROOT': BASE_DIR,  # 项目根目录
}

访问页面时,控制台会输出查询详情(次数、耗时、SQL语句),便于针对性优化。

7.2 批量操作减少数据库交互

使用bulk_createbulk_update批量处理数据,减少数据库请求次数:

# 批量创建标签关联(1次请求替代N次)
from services.models import ServiceTag

# 准备数据
tag_relations = [
    ServiceTag(service=service1, tag=tag1, added_by=user),
    ServiceTag(service=service1, tag=tag2, added_by=user),
    ServiceTag(service=service2, tag=tag1, added_by=user),
]

# 批量创建
ServiceTag.objects.bulk_create(tag_relations)

# 批量更新(如批量设置服务为"推荐")
from services.models import Service
services = Service.objects.filter(category__code="DV")
for service in services:
    service.is_featured = True
Service.objects.bulk_update(services, ['is_featured'])  # 仅更新指定字段

7.3 索引优化:加速查询

为频繁筛选、排序的字段添加索引,减少数据库扫描时间:

# services/models.py
class Service(models.Model):
    # 对频繁筛选的字段添加索引
    name = models.CharField(max_length=200, db_index=True)  # 频繁搜索
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)  # 频繁排序
    price = models.DecimalField(max_digits=10, decimal_places=2, db_index=True)  # 频繁筛选

    class Meta:
        # 复合索引:优化多字段联合查询(如按状态+创建时间筛选)
        indexes = [
            models.Index(fields=['is_active', 'created_at']),
        ]

八、总结与实战任务

本节课深入讲解了模型关系的高级配置、复杂查询技巧与性能优化策略,核心成果包括:

  • ✅ 掌握外键、多对多、一对一关系的高级用法(反向名称、中间模型、级联策略);
  • ✅ 学会使用Q对象、F表达式、子查询处理复杂条件;
  • ✅ 通过聚合与注解实现数据分析;
  • ✅ 用select_related/prefetch_related解决N+1查询问题;
  • ✅ 设计高级关系模式(自关联、通用关系)。

实战任务

  1. 服务标签系统:实现服务与标签的多对多关系,支持标签筛选和热门标签云展示;
  2. 推荐系统:基于服务标签相似度推荐相关服务,结合用户订单历史推荐个性化内容;
  3. 数据分析仪表盘:集成销售趋势图、用户增长曲线,支持数据导出为Excel。

下一课预告:将学习Django Admin后台的精进技巧,打造专业的管理界面,进一步提升数据管理效率。

通过本节课的学习,你已具备设计复杂数据模型和高效处理关联查询的能力,这是构建企业级Django应用的核心竞争力。在实际项目中,需结合业务场景灵活选择关系类型和查询方式,并持续通过工具监控和优化性能。