☰
Django模型设计:ORM查询与数据库迁移实战指南
2026/10/11 12:38:34 网站建设 项目流程

我刚接触 Django 那会儿,最头疼的就是数据库。写过一阵子原生 SQL 的人都知道,建表、关联、查数据,每一步都要小心谨慎,字符串拼接错了连报错都看不懂。后来转用 Django 的 Models 和 ORM,才真正体会到什么叫「把数据库操作变成写代码的一部分」。这篇文章就围绕我在 99 天 Python 学习计划里第 60 天的实战记录,把 Django 模型设计、ORM 查询、迁移管理这些核心内容掰开揉碎讲清楚。无论你是刚学 Python 的新手,还是写过几个 Flask 小项目想进阶的人,这篇文章都能帮你少走弯路。


1. 为什么 Django 把数据库操作叫「艺术」

1.1 从 SQL 到 ORM:换一种思考方式

传统写数据库的方式是 SQL。SQL 本身没问题,功能强大、表达力强,但问题在于它和 Python 代码是两套语言体系。你在 Python 里有个对象User,到了数据库里变成一张表user,字段叫username、email。每次操作都要在两种思维模式之间切换,而且一旦表结构变了,所有相关的 SQL 都得跟着改,工作量巨大。

Django 的 ORM(Object-Relational Mapping,对象关系映射)解决的就是这个痛点。它让你用纯 Python 代码定义数据模型,然后由框架自动翻译成 SQL 去操作数据库。你在代码里操作的是普通的 Python 类、Python 对象,不需要关心底层用的是 MySQL 还是 PostgreSQL,也不需要手写那些重复性极高的增删改查语句。

举个最直观的例子。用原生 SQL 创建一个用户表:

CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(150) NOT NULL UNIQUE, email VARCHAR(254) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );

用 Django 模型,同样的效果只需要:

from django.db import models class User(models.Model): username = models.CharField(max_length=150, unique=True) email = models.EmailField() created_at = models.DateTimeField(auto_now_add=True)

不需要CREATE TABLE,不需要管主键,Django 会帮你自动生成id字段,自动把类名转换成合适的表名。这就是 ORM 的价值:把数据库操作的复杂度封装起来,让我可以专注于业务逻辑本身。

1.2 模型(Models)是什么,它和 ORM 是什么关系

很多初学者分不清 Models 和 ORM 的区别,这里我用一个生活化的类比说明。假如你要开一家餐厅,菜单就是「模型」,它定义了有什么菜、每道菜有什么配料;而后厨的做菜流程就是「ORM」,它负责把菜单上的菜变成实际的菜品端上桌。

在 Django 里,Models 是数据结构的定义层,你用它声明有哪些数据、每个数据长什么样、数据和数据之间是什么关系;ORM 是操作层,你通过它实现对数据库的增删改查,不用直接写 SQL。两者是一体的,模型是静态的结构声明,ORM 是动态的数据操作。

平时我们说的「Django 模型(Models)与 ORM」,其实指的是同一套体系:用模型定义结构,用 ORM 操作数据。理解了这个关系,后面学起来就顺了。


2. 模型设计的基石:字段类型与约束

2.1 常用字段类型的选择逻辑

Django 的模型字段类型非常丰富,但常用的就那么几种。初学者最容易犯的错误是「什么字段都用 CharField」,这个习惯要不得。选错字段类型,轻则影响存储效率,重则导致数据校验出问题。

我根据自己的实操经验,把最常用的字段类型整理了一张表:

字段类型适用场景注意事项
CharField短文本,如用户名、手机号、标题必须指定 max_length,否则报错
TextField长文本,如文章正文、备注不要设置 max_length,虽然设置了也不报错但不生效
IntegerField整数,如年龄、数量范围受数据库限制,超大数考虑 BigIntegerField
FloatField浮点数,如价格有精度问题,涉及金额建议用 DecimalField
DecimalField精确小数,如价格、利率必须指定 max_digits 和 decimal_places
BooleanField布尔值,如是否启用默认 False,配合 null=True 有坑(下面会讲)
DateTimeField时间点,如创建时间、发布时间auto_now_add 表示创建时自动填,auto_now 表示每次修改自动更新
DateField日期,如生日不包含时间部分
FileField文件上传实际存的是文件路径字符串
ImageField图片上传需要安装 Pillow 库

这里重点说一下DateTimeField的两个参数,auto_now_add和auto_now。我第一次用的时候把两个参数弄反了,结果创建时间和更新时间全乱了。简单记法:auto_now_add是「只在创建时写入一次」,auto_now是「每次保存都更新」。实际开发中,created_at用auto_now_add=True,updated_at用auto_now=True,这是最常见的搭配。

2.2 字段约束:null、blank、default 的微妙区别

null、blank、default这三个参数是新手最容易混淆的。我自己刚学的时候就理解错了null和blank,导致数据库里存了一堆空字符串,排查半天。

  • null=True表示数据库该字段允许为 NULL,这是数据库层面的约束。
  • blank=True表示表单填写时允许为空,这是验证层面的约束,和数据库无关。
  • default=某个值表示不传值时使用的默认值。

三者关系一句话总结:如果你希望某个字段「可以不填」,通常要同时设置blank=True;如果是非字符串字段,还要考虑null=True。但是字符串字段(CharField、TextField)建议不要设null=True,因为 Django 习惯用空字符串表示「空」,设了null=True反而会出现「要么是 NULL 要么是空字符串」的混乱局面。

提示:BooleanField 如果要允许空值,不要用null=True,用NullBooleanField(新版 Django 中是BooleanField(null=True))。否则会出现「既不是 True 也不是 False 也不是 NULL」的诡异状态。

2.3 主键、外键与数据库索引

每个模型默认会自动加一个id自增主键,这在大多数场景下是够用的。但有些业务场景需要自定义主键,比如订单号、业务编码这类有业务含义的字段。这时候要显式声明:

class Order(models.Model): order_no = models.CharField(max_length=32, primary_key=True) # 其他字段...

外键(ForeignKey)是关系型数据库的核心,Django 里也极其常用。一个简单的外键定义:

class Article(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(User, on_delete=models.CASCADE)

这里的on_delete必须指定,它的作用是在关联的用户被删除时如何处理文章。最常见的选项有:

  • CASCADE:用户删了,他的文章也一并删除,适合「从属关系」强的场景。
  • SET_NULL:用户删了,文章保留,但作者字段置空,前提是null=True。
  • PROTECT:有文章的用户禁止删除,保护数据完整性。
  • SET_DEFAULT:用户删了,文章归属变为默认值用户。

关于索引,Django 里可以在字段上直接指定db_index=True,或者用Meta类的indexes定义联合索引。我自己建索引的原则是:频繁用于filter或order_by的字段就要加索引,但不要为了「万一用得到」而乱加,索引越多写入越慢,这是典型的空间换时间。


3. ORM 核心操作精讲:增删改查的正确姿势

3.1 创建数据:三种方式各有适用场景

Django ORM 创建数据有三种常见方式,我逐一说明它们的使用场景和坑。

第一种是create方法,最简洁:

user = User.objects.create(username='张三', email='zhangsan@example.com')

一行代码完成「创建对象 + 保存数据库」,适合一次性插入。

第二种是先实例化再save,分两步走:

user = User(username='李四', email='lisi@example.com') user.save()

这种方式的好处是在save()之前可以做一些额外处理,比如给字段赋值、检查逻辑,适合需要中途干预的场景。

第三种是get_or_create,这是我最推荐在业务逻辑中使用的:

user, created = User.objects.get_or_create( username='王五', defaults={'email': 'wangwu@example.com'} )

它先尝试查询,查不到就创建。返回值里有个created布尔值,告诉你这次是「查到了」还是「新建了」。这个 API 在避免重复数据时特别好用,却经常被新手忽略。

批量插入数据时,千万别在循环里逐个save()。那性能简直灾难。正确做法是用bulk_create:

users = [User(username=f'用户{i}', email=f'user{i}@example.com') for i in range(1000)] User.objects.bulk_create(users)

我在实际项目中批量导入几万条数据,用循环save()可能要几分钟,用bulk_create几秒钟就搞定,差距巨大。

3.2 查询数据:filter、exclude、get 的正确用法

查询是 ORM 最常用的操作,也是最能体现功底的地方。先说说三个基础方法的区别:

  • get:只返回一条记录,找不到或找到多条都会抛异常。
  • filter:返回符合条件的所有记录,结果是 QuerySet,找不到时返回空 QuerySet 而不是报错。
  • exclude:返回不符合条件的所有记录,相当于 SQL 里的NOT IN或!=。

get方法有个常见陷阱。新手喜欢这么写:

user = User.objects.get(username='张三')

如果数据库里恰好有两个同名「张三」,程序直接抛MultipleObjectsReturned。更麻烦的是查不到时会抛DoesNotExist。所以正确的姿势是先在预期内处理异常,或者干脆用filter().first():

user = User.objects.filter(username='张三').first()

.first()返回第一条记录,不存在时返回None,不会抛异常。这在不确定数据是否存在的时候非常实用。

条件查询的写法也要注意。Django ORM 用的是双下划线语法,看似简单但表达力很强:

# 精确匹配 User.objects.filter(username='张三') # 模糊匹配 User.objects.filter(username__contains='张') User.objects.filter(username__startswith='张') User.objects.filter(username__endswith='三') # 范围匹配 Article.objects.filter(pub_date__range=(start_date, end_date)) # 空值判断 Article.objects.filter(pub_date__isnull=True) # 值在某列表中 User.objects.filter(id__in=[1, 2, 3]) # 大小比较 Article.objects.filter(view_count__gte=100)

这里面__contains翻译成 SQL 就是LIKE '%张%',__gte是>=,__lte是<=。记不住的话就去查官方文档,但常用的几个一定要刻在脑子里,写起来效率完全不一样。

3.3 链式过滤与惰性求值的原理

ORM 的 QuerySet 是惰性求值的,这意味着你在写filter的时候并没有真正执行 SQL,直到你「消费」这个 QuerySet(比如遍历、取第一个、转成列表)时才真正查数据库。这个特性极其重要,它给了你组装复杂查询条件的能力:

qs = Article.objects.all() if keyword: qs = qs.filter(title__contains=keyword) if category: qs = qs.filter(category=category) # 一直到这里,上面可能还没有真正执行 SQL articles = list(qs) # 到这一步才真正查库

这种写法在业务中太常见了。搜索条件可能有也可能没有,可以按需拼接查询条件,最后一下子执行。如果不懂惰性求值,你可能就写出一堆重复的查询分支,代码又丑又慢。

链式过滤的另一个好处是每一个filter返回的都是新的 QuerySet,不会修改原来的。这个特性避免了一个隐藏 bug:如果在原 QuerySet 上直接操作发现会影响后面的代码逻辑,那大概率是因为用错了方法(比如直接在 QuerySet 上循环修改对象属性)。

3.4 更新与删除:小心批量操作

更新数据有两种场景。单个对象更新很简单:

user = User.objects.get(id=1) user.email = 'new@example.com' user.save()

但要更新多条记录,千万别循环save()。正确姿势是使用update方法:

User.objects.filter(is_active=False).update(status='disabled')

这一行 SQL 就完成批量更新,性能极高。注意update是 QuerySet 的方法,直接作用在数据库上,不会经过模型的save()逻辑(比如某些自动填充字段不会触发)。

还有一个进阶技巧是用F表达式,它可以在数据库层面完成字段自增操作。比如给每篇文章的浏览量加 1:

from django.db.models import F Article.objects.filter(id=1).update(view_count=F('view_count') + 1)

如果不这样写,就得先把view_count查出来,加 1,再写回去。这中间有竞态风险,多个请求同时操作时会丢失更新。F表达式把运算推到数据库执行,避免了这个问题。

删除数据同样有单条和批量之分。单条用obj.delete(),批量用QuerySet.delete()。还有一个坑要注意:QuerySet.delete()会一并删除外键关联的CASCADE对象。有些场景希望「只删自己,不动别人」,可以考虑把外键的on_delete设为SET_NULL或PROTECT来规避风险。


4. 进阶查询能力:聚合、分组、Q 对象与预取

4.1 聚合与分组:在 ORM 里做数据统计

很多新手面对「统计某个分类下有多少文章」这类需求,第一反应是「把数据全部查出来,然后用 Python 循环统计」。这在数据量小的时候没问题,但数据一多就非常低效。正确思路是让数据库帮你算,ORM 提供了完整的聚合能力。

常用聚合函数有Count、Sum、Avg、Max、Min。来个实战例子:

from django.db.models import Count # 统计每篇文章的评论数 articles = Article.objects.annotate(comment_count=Count('comment')) # 统计每个作者的文章总数 author_stats = User.objects.annotate(article_count=Count('article')) # 联合使用过滤与聚合 result = Article.objects.filter(is_published=True).aggregate( total_views=Sum('view_count'), avg_views=Avg('view_count'), max_views=Max('view_count'), )

annotate和aggregate容易混淆。我的理解是:annotate是「分组后给每一条记录加一个统计字段」,结果还是 QuerySet;aggregate是「对整个结果集进行汇总」,返回的是一个字典。你要的行级别统计用annotate,总体汇总用aggregate。

4.2 复杂条件查询:Q 对象的逻辑组合

当查询条件涉及「或」关系时,直接写filter就不够了。比如要查「标题包含 Python 或 作者是张三」的文章,用filter(title__contains='Python', author='张三')表达的是「并且」关系,完全不对。这时候要用到 Q 对象:

from django.db.models import Q articles = Article.objects.filter( Q(title__contains='Python') | Q(author__name='张三') )

|表示或,&表示与,~表示非。多个 Q 对象还可以组合:

articles = Article.objects.filter( Q(category='教程') | Q(category='经验'), is_published=True )

这里有个小细节:在同一个filter里,逗号分隔的条件是「与」关系,Q 对象和普通字段条件可以混用。但注意,一旦用了 Q 对象,普通关键字参数要么全放前面,要么全放后面,否则会报语法错误。这个坑我踩过一次,花了不少时间才搞明白。

4.3 select_related 与 prefetch_related:解决 N+1 查询

N+1 查询问题是 ORM 性能的头号杀手。举个典型场景:显示文章列表,每篇文章要显示作者名。如果这样写:

articles = Article.objects.all() # 查一次文章 for article in articles: print(article.author.username) # 每篇文章再查一次作者,N 次

总共执行了 1 + N 次 SQL。文章少还好,如果文章有 100 篇、1000 篇,性能和坐火箭一样往下掉。

解决办法是用select_related(适用于外键一对一关系):

articles = Article.objects.select_related('author').all()

这样 Django 会用 SQLJOIN把文章和作者一次性查出来,总共只执行一次查询。

prefetch_related则适用于多对多关系和外键反向关系。比如查所有作者及其文章:

authors = User.objects.prefetch_related('article_set').all()

它会先查作者,再查文章,然后在内存里自动关联。虽然执行了两次查询,但总量可控。

我在实际项目中就遇到过接口响应从 5 秒优化到 0.3 秒的案例,核心就是加了两行select_related和prefetch_related。这也再次说明,ORM 的性能关键不在于你能不能写,而在于你知不知道那些让你「少查数据库」的工具。


5. 数据迁移:模型从定义到数据库的桥

5.1 迁移的本质是什么

模型写好了,怎么变成数据库里的真实表?靠的就是迁移(Migration)。我初学时的直觉是「模型定义了,表就自动建好了」,其实不是。模型只是 Python 代码,数据库里的表需要靠迁移命令来生成和同步。

Django 的迁移机制非常优雅。核心操作就三条命令:

python manage.py makemigrations python manage.py migrate python manage.py showmigrations

makemigrations会读取你的模型定义和上一次迁移记录,生成一个迁移脚本文件(在应用的migrations目录下);migrate则把这些迁移脚本真正应用到数据库。showmigrations可以查看哪些迁移已应用、哪些还没执行。

迁移文件是纯 Python 代码,它记录了模型结构的变化历史。这带来一个巨大的好处:团队协作时,别人拉取你的代码后,只需要执行migrate,就能复现完全一样的数据库结构;部署上线时,也只需要在服务器上跑migrate就行。这比手动在每台机器上执行 SQL 脚本靠谱多了。

5.2 迁移的常见坑与化解办法

迁移操作看似简单,实际用起来还是有几个老生常谈的坑。

第一个坑是「改了模型,忘记生成迁移」导致报错。系统提示字段不存在或者数据库缺列,第一反应就应该是检查有没有跑makemigrations。

第二个坑是「迁移文件和别人的冲突」。并行开发时,两个人各自在本地生成了迁移文件,合并代码后会出现迁移依赖冲突。解决办法通常是把两个迁移文件中的一个删除,重新生成,或者找出两个迁移依赖的共同祖先,手动调整dependencies字段。

第三个坑是「删字段要谨慎」。生产环境的数据表,一个字段删了,数据就没了。如果只是暂时不用,建议注释掉字段而非直接删除;如果必须删,先备份数据。我自己的一贯做法是重大结构变更前先python manage.py dumpdata备份。这个看起来「笨」的习惯,救过我不少次。

注意:迁移的依赖关系不能随意打乱。如果你对多个应用的模型进行重命名、关联调整,尽量一次性生成一个合理的迁移,不要「做一个、迁一个」地零碎操作,否则依赖链长了你根本分不清谁先谁后。

5.3 数据迁移与结构迁移

除了结构迁移,Django 还支持数据迁移。比如你要给所有已有用户填充一个默认头像,或者根据旧字段的值计算新字段,这就要写数据迁移脚本了:

# 生成一个空迁移文件 python manage.py makemigrations app_name --empty

然后在生成的迁移文件里写RunPython数据操作函数:

from django.db import migrations def fill_default_avatar(apps, schema_editor): User = apps.get_model('app_name', 'User') for user in User.objects.all(): if not user.avatar: user.avatar = 'default.jpg' user.save() class Migration(migrations.Migration): dependencies = [] operations = [ migrations.RunPython(fill_default_avatar), ]

注意这里获取模型用的是apps.get_model而不是直接从models导入,这是历史数据的问题——在迁移执行时,模型的状态是当时的状态,不是现在的。这个细节写错会导致迁移报错或者数据不对,我踩过一次后永久记住了。


6. 模型关系实战:一对一、一对多、多对多

6.1 三种关系的定义与使用场景

Django 模型的三种关系对应现实业务里的三种关联形态。我用自己的理解帮你理清。

一对一(OneToOneField):比如用户和用户资料。一个用户只能有一份资料,一份资料只属于一个用户。扩展用户模型时常用这个,比如:

class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) bio = models.TextField(blank=True) avatar = models.ImageField(upload_to='avatars/', blank=True)

一对多(ForeignKey):比如作者和文章。一个作者有多篇文章,一篇文章只属于一个作者。这是最常见的关联,也是外键的标准用法。

多对多(ManyToManyField):比如文章和标签。一篇文章可以打多个标签,一个标签可以属于多篇文章。Django 会自动生成一张中间表来管理关联关系,不需要你手动建表:

class Article(models.Model): title = models.CharField(max_length=200) tags = models.ManyToManyField('Tag', related_name='articles') class Tag(models.Model): name = models.CharField(max_length=50)

6.2 反向查询与 related_name 的坑

Django 的关系是双向的。正向查询很好理解:article.author。反向查询则是从被关联对象出发,找到所有关联自己的对象。

如果你定义了Article.author = ForeignKey(User),那么默认情况下用user.article_set.all()可以查到该用户的所有文章。这个默认的反向查询名是「模型名小写 +_set」。

但默认名字丑且不好记,更推荐主动指定related_name:

class Article(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='articles')

这样反向查询就优雅得多:

user.articles.all()

related_name还有个隐藏好处:在select_related、prefetch_related和查询过滤中,它决定了引用的名字。不设置时,反向过滤要写article__author;设置了related_name='articles'后,可以更直观地表达语义。

注意:一个模型上不能出现两个相同名字的反向查询关联。如果同一个模型有多个外键指向同一个目标模型,必须给每个外键设置不同的related_name,否则启动就报错。

6.3 中间表与多对多进阶操作

默认的ManyToManyField自动生成的中间表只存「两个模型的主键」,够用但不够灵活。有时候要在关联关系上存额外信息,比如「用户收藏文章的时间」「用户参与活动的状态」,那就得自己定义中间表。

用through参数来实现:

class UserArticle(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) article = models.ForeignKey(Article, on_delete=models.CASCADE) collected_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user_article' class Article(models.Model): collectors = models.ManyToManyField( User, through='UserArticle', related_name='collected_articles', )

创建这种关系时,不能直接article.collectors.add(user),而要显式创建中间表记录:

UserArticle.objects.create(user=user, article=article)

这种写法在多对多关系需要附加业务字段时几乎是必需品。我做过一个「用户收藏文章」的功能,最初用默认的ManyToManyField实现,后面需求变成了「要显示收藏时间」,被迫重构。所以建议从设计之初就想清楚中间表上需不需要存额外的字段。


7. 性能调优:ORM 代码的体检与优化

7.1 只看必要字段:values、values_list、only/defer

ORM 默认会查出整行所有字段。如果你的表有 20 个字段,而业务只需要其中 2 个,查全字段就是浪费。Django 提供了几个优化工具。

values返回字典列表:

User.objects.values('id', 'username') # 结果: [{'id': 1, 'username': '张三'}, ...]

values_list返回元组列表:

User.objects.values_list('username', flat=True) # 结果: ['张三', '李四', ...]

两兄弟的区别在于返回格式不同。另外一个更接近模型的方案是only和defer:

  • only('name', 'email'):只取指定字段,其他字段在需要时才查。
  • defer('content'):暂时不取指定字段,需要时再查。

这些方法在处理「列表页只需要摘要信息、详情页才要全文」的场景特别好用。注意不要滥用,因为它们会生成更复杂的 SQL,而且还可能触发额外的查询(没取的字段在访问时会造成一次补充查询)。

7.2 大结果集的分批处理:iterator

如果你要遍历几万条记录做某些操作,直接for obj in qs会一次性把所有对象加载到内存,内存占用非常高。这时候用iterator()分批从数据库游标读取:

for article in Article.objects.all().iterator(chunk_size=500): # 处理逻辑

注意iterator()返回的是迭代器,不能再用filter等方法链式操作,但遍历本身内存占用很低。我处理过 20 万条历史数据迁移,用了iterator加bulk_create,内存稳定在几十 MB 级别,而不优化的话可能直接 OOM。

7.3 用 Django Debug Toolbar 定位慢查询

学 ORM 优化有个核心环节:你得先看到底执行了什么 SQL,执行了几次,花了多少时间。只看 ORM 代码很难发现问题。

这时候要上神器 Django Debug Toolbar,装好之后在每个页面底部会有一个调试面板,能看到:

  • 总共执行了多少条 SQL
  • 每条 SQL 的内容和耗时
  • 重复查询次数
  • 哪些地方触发了 N+1 查询

装它很简单,三步:

pip install django-debug-toolbar

然后在settings.py里配置:

INSTALLED_APPS = ['debug_toolbar', ...] MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware', ...]

最后在urls.py添加路由:

if settings.DEBUG: import debug_toolbar urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns

我到现在都认为,Django 项目性能排查的第一步永远是「打开 Debug Toolbar 看 SQL 面板」。写完一个页面之后扫一眼,有没有多余查询、有没有重复查询,一清二楚。


8. 常见问题与排查技巧实录

8.1 字段不存在导致的表结构不一致

现象:某个字段在代码里明明定义了,但操作时报「no such column」或「Unknown column」。
原因:改了模型之后忘了生成或应用迁移。
排查:先跑python manage.py makemigrations,如果提示没有变化,再跑python manage.py showmigrations,看迁移状态是否是「未应用」。
解决:执行python manage.py migrate,或者分两步先makemigrations再migrate。

8.2 多对多关系新增报错「Field 'id' expected a number」

现象:调用article.tags.add(tag)时抛出类型错误。
原因:中间表自定义了through,但直接调用add()方法不被支持,因为 Django 没法自动填充中间表额外字段。
解决:改用显式创建中间表记录,如UserArticle.objects.create(...),或者给through中间表的额外字段设置default值,让add()可以直接调用。

8.3 get() 返回多条数据导致的 MultipleObjectsReturned

现象:明明设计为唯一的数据,用get查询时报多条记录。
原因:要么数据库本身数据有重复,要么唯一约束没建好。
排查:检查模型字段是否有unique=True;检查历史数据中是否有脏数据。
解决:如果业务允许重复,改用filter().first();如果业务不允许,先清理重复数据,再在模型上设置unique=True或UniqueConstraint,从根上防止。

8.4 ORM 查询性能突然变差

现象:接口响应越来越慢,数据库 CPU 飙升。
排查:先用 Debug Toolbar 看 SQL 数量和耗时,重点看有没有 N+1 查询、有没有大表全表扫描。
解决:给高频过滤字段加db_index=True;用select_related和prefetch_related减少查询数量;分页限制单次返回的数据量。

8.5 迁移冲突,多个迁移文件互相依赖

现象:执行migrate时提示「Conflicting migrations detected」。
原因:多人并行开发,各自基于同一个迁移版本生成了新迁移。
解决:优先和同事确认谁的迁移先合并,把后合并的迁移文件删除后重新makemigrations;如果已经产生复杂依赖,可以手动修改迁移文件的dependencies字段,指向共同的已有迁移。


9. 实战经验总结:我踩过的坑与我的建议

关于 Django 模型和 ORM,最后分享几点个人体会。

第一,设计模型时多花半小时,胜过后期重构十小时。字段命名、关联关系、related_name、on_delete策略,这些在设计阶段就定好,后期改的代价极大。我见过最惨痛的经历是数据模型初期图省事全用CharField,结果后面做统计和日期操作时才发现字段类型不对,一堆数据没法直接用,只能重新清洗。

第二,ORM 不是万能药,但你先把 ORM 用熟。不要为了「炫技」写过度的原生 SQL,也不要因为「ORM 有坑」就全盘拒绝它。99% 的业务场景,Django ORM 都覆盖了,而且写法统一、维护成本低。我在项目中混用过一段时间的原生 SQL,后期的维护难度确实比 ORM 高不少。能用 ORM 写出来的,优先用 ORM。

第三,养成看 SQL 的习惯。ORM 是「翻译官」,它翻译出来的 SQL 到底长什么样、效率如何,值得你看看。用 Debug Toolbar 或者直接打印str(qs.query)就能看到生成的 SQL。看得多了,你会发现哪些写法会生成低效 SQL,哪些写法会让数据库走索引,这个感觉只能靠积累。

第四,每做一次结构大调整之前,先备份数据。不是吓唬你,生产环境的数据是无价的。改模型之前python manage.py dumpdata > backup.json,换表名之前先确认没有外键引用它。这些动作几秒钟就能完成,关键时刻能救命。

最后,我想说的是:Django 的 ORM 是一套被大量生产环境验证过的成熟体系,你遇到的绝大多数数据库操作难题,网上都有人踩过、解决过。你不需要成为 SQL 大师才能用好 Django,你只需要把模型设计和 ORM 查询这两个基本功打扎实,就能支撑大部分项目的开发工作。学完这一天内容之后,建议你立刻在自己的项目里把常见的增删改查、一对多、多对多、聚合查询都过一遍,遇到问题再看一遍这篇文章。慢慢你就会发现,数据操作确实是门艺术,而你已经掌握了打开这扇门的钥匙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询