87_Python Django模型与数据库

Python Django模型与数据库:ORM查询、关联关系与迁移进阶

文章目录

前言

数据库操作是Web开发的灵魂,而Django的**ORM(对象关系映射)**将复杂的SQL操作转化为优雅的Python代码。你无需手写SQL,却能处理绝大部分数据库场景------从简单的CRUD到复杂的多表关联查询。本文在上篇Django入门的基础上,深入探讨模型定义的高级技巧、ORM查询的各类方法、数据库迁移的最佳实践,以及三种核心关联关系的实战应用。

ORM的价值不只是"不用写SQL" :更重要的价值是通过Python对象操作数据库,IDE可以提供代码补全和类型检查,重构时改模型名会自动更新所有引用。此外,Django ORM的查询集(QuerySet)是惰性求值的------Article.objects.filter(status='published') 在真正需要数据之前不会执行SQL。这意味着你可以链式叠加多个filter、exclude、order_by,Django只生成一条最优化的SQL语句。这种设计极大减少了数据库访问次数。

一、重新审视模型定义

1.1 字段选项详解

python 复制代码
from django.db import models

class Product(models.Model):
    # CharField 的常用参数
    name = models.CharField(
        max_length=100,
        unique=True,         # 唯一约束
        db_index=True,       # 创建数据库索引
        verbose_name='商品名称'
    )
    
    # decimal_places 控制小数位数,max_digits 控制总位数
    price = models.DecimalField(
        max_digits=10, decimal_places=2,
        verbose_name='价格'
    )
    
    # blank 控制表单验证,null 控制数据库层面
    description = models.TextField(
        blank=True,        # 表单中可以为空
        null=True,         # 数据库中可以为NULL
        verbose_name='描述'
    )
    
    # default 设置默认值
    is_active = models.BooleanField(
        default=True,
        verbose_name='是否上架'
    )
    
    # auto_now_add 只在创建时设置
    created_at = models.DateTimeField(auto_now_add=True)
    
    # auto_now 每次保存都更新
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        db_table = 'shop_product'           # 自定义表名
        ordering = ['-created_at']           # 默认排序
        verbose_name = '商品'
        verbose_name_plural = '商品'
        indexes = [
            models.Index(fields=['name', 'is_active']),  # 复合索引
        ]

db_indexMeta.indexes 的区别:前者创建单列索引(简单直接),后者支持多列复合索引和多列排序。

1.2 三种关联关系

关系型数据库的核心就是表之间的关联。Django通过三个字段类型来表达:

一对多(ForeignKey)

最常见的关系。 方对应方。例如:一个作者可以写多篇文章。

python 复制代码
class Author(models.Model):
    name = models.CharField(max_length=50)
    email = models.EmailField()
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,  # 作者删除时,他的文章也删除
        related_name='posts',      # 反向查询的名称
    )

on_delete 的选项非常关键:

选项 含义
CASCADE 级联删除,关联对象也被删除
PROTECT 阻止删除,抛出ProtectedError
SET_NULL 设为NULL(需null=True)
SET_DEFAULT 设为默认值(需default)
DO_NOTHING 什么都不做(依赖数据库约束)
多对多(ManyToManyField)

例如:一篇文章可以有多个标签,一个标签可以对应多篇文章。

python 复制代码
class Tag(models.Model):
    name = models.CharField(max_length=30, unique=True)

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        Tag,
        related_name='posts',
        blank=True
    )

Django会自动创建一张中间表来存储多对多关系。你也可以手动指定中间表来存储额外信息:

python 复制代码
class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        Tag,
        through='PostTag',  # 自定义中间表
        related_name='posts'
    )

class PostTag(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_at = models.DateTimeField(auto_now_add=True)  # 额外字段
    
    class Meta:
        unique_together = ['post', 'tag']  # 联合唯一
一对一(OneToOneField)

例如:每个用户有一个专属的资料扩展表。

python 复制代码
from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    avatar = models.URLField(blank=True)
    birthday = models.DateField(null=True, blank=True)

二、ORM查询:从基础到进阶

Django ORM提供了一套链式调用的查询API,语法流畅且Pythonic。

2.1 基础查询

Django ORM的查询API设计得非常接近自然语言------Article.objects.filter(status='published') 可以读作"从Article对象中过滤出状态为已发布的"。这种链式调用风格(filter之后可以继续.exclude、.order_by、.annotate)让你像搭积木一样构建查询,每一步都返回一个QuerySet对象。

QuerySet的惰性求值 (面试高频题):qs = Article.objects.filter(status='published') 这行代码不会立刻执行SQL查询。QuerySet只在实际需要数据时才执行------比如你遍历它、调用len()、或者把它转为列表时。这意味着 qs = qs.filter(view_count__gt=100); qs = qs.exclude(author=admin_user) 连续叠加多个过滤条件,Django只会生成一条合并后的SQL。这是ORM性能优化的基础认知。

python 复制代码
# 获取所有对象
all_articles = Article.objects.all()

# 过滤查询
published = Article.objects.filter(status='published')

# 排除查询
not_draft = Article.objects.exclude(status='draft')

# 获取单个对象(不存在或多于一个会抛异常)
article = Article.objects.get(pk=1)

# 获取或创建
article, created = Article.objects.get_or_create(
    title='默认标题',
    defaults={'content': '默认内容', 'status': 'draft'}
)

# 计数
count = Article.objects.filter(status='published').count()

# 是否存在
has_draft = Article.objects.filter(status='draft').exists()

2.2 字段查询(Field Lookups)

Django通过双下划线语法实现丰富的查询条件:

python 复制代码
# 等于(默认)
Article.objects.filter(status='published')

# 不等于
Article.objects.exclude(status='draft')

# __gt / __gte / __lt / __lte
Article.objects.filter(view_count__gt=100)   # 大于100
Article.objects.filter(view_count__gte=50)    # 大于等于

# __in
Article.objects.filter(category__in=[1, 2, 3])

# __range
Article.objects.filter(created_at__range=['2025-01-01', '2025-06-01'])

# __contains / __icontains (不区分大小写)
Article.objects.filter(title__icontains='django')

# __startswith / __endswith
Article.objects.filter(title__startswith='Python')

# __isnull
Article.objects.filter(description__isnull=True)

# 日期查询
Article.objects.filter(created_at__year=2025)
Article.objects.filter(created_at__month=5)

2.3 关联查询

python 复制代码
# 正向查询:通过外键过滤
articles = Article.objects.filter(category__name='技术')

# 反向查询:通过 related_name
categories = Category.objects.filter(articles__status='published')

# 跨越多层关联
articles = Article.objects.filter(category__parent__name='编程')

# 反向查询统计
from django.db.models import Count
categories = Category.objects.annotate(
    article_count=Count('articles')
).filter(article_count__gt=0)

2.4 聚合与分组

python 复制代码
from django.db.models import Count, Sum, Avg, Max, Min, Q, F

# 聚合函数
result = Article.objects.aggregate(
    total=Count('id'),
    avg_views=Avg('view_count'),
    max_views=Max('view_count')
)
# 返回: {'total': 150, 'avg_views': 235.5, 'max_views': 1200}

# 按分类分组统计
Category.objects.annotate(
    article_count=Count('articles'),
    total_views=Sum('articles__view_count')
).values('name', 'article_count', 'total_views')

# F表达式:引用字段的值
Article.objects.update(view_count=F('view_count') + 1)

# Q对象:复杂逻辑查询
from django.db.models import Q

articles = Article.objects.filter(
    Q(status='published') & 
    (Q(title__icontains='django') | Q(content__icontains='django'))
)

Q对象 支持 &(AND)、|(OR)、~(NOT)运算符,可以构建任意复杂的查询条件。

2.5 性能优化

python 复制代码
# select_related:一次性查询外键关联对象(JOIN方式)
articles = Article.objects.select_related('category', 'author')

# prefetch_related:预取多对多/反向一对多关联(额外查询)
articles = Article.objects.prefetch_related('tags')

# only / defer:只查询/延迟查询指定字段
articles = Article.objects.only('title', 'created_at')
articles = Article.objects.defer('content')  # 延迟加载大字段
  • select_related 适用于ForeignKey和OneToOneField,使用SQL JOIN一次查出
  • prefetch_related 适用于ManyToManyField和反向ForeignKey,执行额外查询后在Python中拼接

三、模型方法与管理器

3.1 自定义模型方法

python 复制代码
class Article(models.Model):
    # ... 字段定义 ...
    
    def is_popular(self):
        """是否热门文章"""
        return self.view_count > 1000
    
    def short_content(self, length=100):
        """截取摘要"""
        return self.content[:length] + '...' if len(self.content) > length else self.content
    
    def publish(self):
        """发布文章"""
        self.status = 'published'
        self.save(update_fields=['status'])

3.2 自定义管理器

python 复制代码
class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published')
    
    def popular(self):
        return self.get_queryset().filter(view_count__gt=1000)

class Article(models.Model):
    # ... 字段定义 ...
    
    objects = models.Manager()              # 默认管理器
    published = PublishedManager()          # 自定义管理器

# 使用
Article.published.all()           # 所有已发布的文章
Article.published.popular()       # 热门文章
Article.objects.all()             # 所有文章(包括草稿)

四、数据库迁移进阶

4.1 迁移的日常操作

数据库迁移(Migration)是Django最强大的功能之一------它让你像管理代码版本一样管理数据库结构的变更。每次模型改动后,运行 makemigrations 生成迁移文件,migrate 应用到数据库。迁移文件是纯Python代码,可以提交到Git,团队成员拉取后执行 migrate 就能保持数据库结构一致------彻底告别"我这边表结构和你不一样"的协作噩梦。

迁移的核心价值 :在没有ORM的传统开发中,DBA需要手动执行ALTER TABLE语句修改数据库结构,而且开发环境和生产环境的变更必须同步。Django的迁移系统将这个过程自动化、可追溯、可回滚------你甚至可以 migrate blog 0002 回滚到数据库的历史状态。

bash 复制代码
# 创建迁移
python manage.py makemigrations blog

# 查看迁移计划(不执行)
python manage.py migrate --plan

# 查看迁移SQL
python manage.py sqlmigrate blog 0003

# 执行到某个迁移
python manage.py migrate blog 0002

# 回滚迁移
python manage.py migrate blog 0001

# 重置迁移历史
python manage.py migrate blog zero

4.2 数据迁移(Data Migration)

有时修改模型需要同时迁移已有数据。创建一个空迁移然后手动填写:

bash 复制代码
python manage.py makemigrations blog --empty --name add_default_category
python 复制代码
# blog/migrations/xxxx_add_default_category.py
from django.db import migrations

def create_default_category(apps, schema_editor):
    Category = apps.get_model('blog', 'Category')
    Category.objects.create(name='未分类')

def reverse_func(apps, schema_editor):
    Category = apps.get_model('blog', 'Category')
    Category.objects.filter(name='未分类').delete()

class Migration(migrations.Migration):
    dependencies = [
        ('blog', '0002_previous_migration'),
    ]
    
    operations = [
        migrations.RunPython(create_default_category, reverse_func),
    ]

RunPython 可以执行自定义的Python函数来操作数据,是数据迁移的核心工具。

4.3 迁移注意事项

  1. 迁移文件要提交到版本控制,团队成员共享同一套迁移历史
  2. 不要手动修改已应用的迁移文件,应创建新的迁移
  3. 生产环境迁移前要备份数据库
  4. 大数据量迁移要分批处理,避免锁表时间过长

总结

本文深入讲解了Django ORM的核心能力:模型定义 阶段要正确选择字段类型和关联关系;查询阶段 利用双下划线语法、Q对象和聚合函数写出高效的查询语句;关联关系 需要理解ForeignKey、ManyToManyField和OneToOneField的语义和适用场景;数据库迁移 是开发的忠实伙伴,既能让版本控制追踪数据库变更,也能安全地执行数据迁移。掌握了这些,你就能游刃有余地应对绝大部分数据库场景了。下一篇我们将转向Python单元测试,学习如何用unittest保障代码质量。

✅ 亮点总结

  • Django ORM支持二十余种字段类型和丰富的参数选项,几乎能映射所有常见的数据建模需求
  • 双下划线查询语法(field__lookup)是Django ORM的精髓------精准过滤、范围查询、跨表关联一气呵成
  • Q对象通过 |(或)、&(与)、~(非)组合实现复杂逻辑查询,告别拼接SQL字符串
  • ForeignKey、ManyToManyField和OneToOneField三种关联关系覆盖了一对多、多对多和一对一的全部场景
  • 数据库迁移系统让schema变更可版本控制、可回溯、可自动生成,大幅降低数据库管理的心智负担

适用场景

  • 复杂业务建模:电商系统中商品、订单、用户之间的多层级关联关系建模
  • 多表关联查询:在一个查询中跨多张表筛选数据,利用 select_related()prefetch_related() 优化性能
  • 数据库版本管理:团队协作中通过迁移文件追踪每次数据库结构变更,确保环境一致性

扩展方向

  • 数据库索引与查询优化:学习 db_indexindex_togetherexplain() 分析,提升高并发下的查询性能
  • 原生SQL与ORM混合:在ORM力不从心时使用 raw()connection.cursor() 执行复杂SQL
  • 推荐阅读下一篇《Python单元测试unittest》,学习如何为Django模型和视图编写测试用例

Django ORM的设计哲学是"能用Python表达的,绝不用SQL"。但偶尔的原生SQL查询也是可以的------通过 Article.objects.raw()connection.cursor()

相关推荐
程序员小远1 小时前
性能测试之性能调优
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·性能测试
—Miss. Z—1 小时前
计算机二级MySQL选择题考点Ⅱ
数据库·mysql·oracle
敖行客 Allthinker1 小时前
IM 融合专题:后端架构师的核心修炼
java·开发语言·数据库
金銀銅鐵1 小时前
[Python] 借助图形化界面探索模n运算的规律
python·数学
Hesionberger1 小时前
快速求解完全平方数的最少数量
开发语言·数据结构·python·算法·leetcode·c#
翔云1234561 小时前
MySQL 中 CURRENT_TIMESTAMP 到底取的是 INSERT 时间,还是 COMMIT 时间?
数据库·mysql
矜持的左手1 小时前
硬件故障后数据文件大小不对故障处理—Oracle碎片扫描恢复
数据库·oracle
xywww1685 小时前
大模型 API 选型实战:GPT、Gemini、Claude 接入时该看哪些指标?
运维·服务器·人工智能·python·gpt·langchain
欢呼的太阳10 小时前
数据库设计Step by Step (10)——范式化
服务器·数据库·oracle