1. Django ORM 到底解决了什么问题
很多刚接触 Django 的人,第一次看到models.py里定义一个类就能自动生成数据库表,第一反应都是“这也太魔法了吧”。我当初也是这个反应。但用久了你会发现,Django ORM 真正解决的,是让写 Python 的人不用再手写 SQL 就能操作数据库这件事。
在没有 ORM 的世界里,你要查一条用户数据,得先连数据库、拼 SQL 字符串、执行、解析结果集、手动映射成对象。字段一多,SQL 就长得没法看;数据库一换,SQL 方言还得重写。Django ORM 把这套流程抽象成了 Python 类和对象:一个类对应一张表,一个属性对应一个字段,一个实例对应一行记录。你操作对象,它帮你翻译成 SQL。
这篇文章我打算把 Django ORM 从零到跑通讲一遍,重点放在三件事上:ORM 是什么、怎么配置、怎么连 MySQL 并建表。适合刚学完 Python 基础、想上手 Django 做点真实项目的人,也适合之前只用过 SQLite、想换成 MySQL 的开发者。我会把每一步为什么这么做讲清楚,而不是只丢一段配置让你抄。
先说结论:Django 默认用 SQLite,开箱即用,但真实项目里 MySQL 更常见。切换数据库的核心就两块——改settings.py里的DATABASES配置,以及装好对应的数据库驱动。剩下的建表工作,交给makemigrations和migrate两条命令就行。
2. 理解 Django ORM 的核心设计思路
2.1 从“写 SQL”到“写类”的思维转变
传统开发里,我们脑子里想的是“我要建一张 user 表,有 id、name、email 三个字段”。Django ORM 里,你想的是“我要定义一个 User 类,它有 name、email 两个属性,id 它自动给我”。这个转变看着小,实际影响很大。
Django 的模型(Model)是数据的唯一权威来源。你定义好模型,Django 就能根据它生成表结构、生成管理后台、生成表单、做数据校验。也就是说,模型不只是“数据库表的映射”,它还是整个应用数据层的中心。这就是为什么 Django 官方一直强调 DRY 原则——Don't Repeat Yourself,你只在一个地方定义数据结构,其他地方都从它派生。
我个人的体会是:模型设计得好不好,直接决定后面写代码顺不顺。字段类型选错、关系没理清,后面改起来要迁移、要处理历史数据,非常痛苦。所以建模阶段多花十分钟想清楚,比后面返工划算得多。
2.2 ORM 帮你屏蔽了哪些数据库差异
不同数据库的 SQL 方言是有差别的。比如自增主键,MySQL 用AUTO_INCREMENT,PostgreSQL 用SERIAL,SQLite 又是另一套。字符串类型、日期函数、分页语法,各家都不太一样。
Django ORM 在中间做了一层翻译。你写User.objects.filter(name="张三"),它会根据你配置的数据库类型,翻译成对应的 SQL。这意味着同一套模型代码,理论上可以跑在 SQLite、MySQL、PostgreSQL 上,切换数据库时业务代码基本不用动。
但这里有个坑要提前说:“理论上”不等于“完全无痛”。不同数据库对字段长度、索引、事务隔离级别的支持不一样,切换时还是要注意。比如 MySQL 的VARCHAR必须指定长度,而 PostgreSQL 的VARCHAR可以不指定。Django 会帮你处理大部分,但涉及数据库特有功能时,还是得看文档。
2.3 为什么真实项目更倾向 MySQL
SQLite 是个文件数据库,零配置、单文件、方便携带,做小工具、做原型、做本地开发非常爽。但它有几个硬伤:并发写入能力弱(写操作会锁整个库)、没有用户权限体系、不适合多机部署。
真实项目一旦上线,往往要面对多用户并发、要备份、要做主从、要远程连接,这时候 MySQL 就上场了。MySQL 生态成熟、资料多、运维方案齐全,是绝大多数中小型 Web 项目的默认选择。所以“Django 连 MySQL”几乎是每个后端开发者都要走一遍的路。
3. 环境准备与 MySQL 连接配置
3.1 安装依赖:驱动选哪个
Django 连 MySQL,需要一个数据库驱动。历史上主流的有两个:mysqlclient和PyMySQL。
mysqlclient是 C 扩展,性能好,是 Django 官方推荐的首选。但它安装时依赖系统的 MySQL 开发库,在 Windows 上经常报错,在 Linux 上要先装default-libmysqlclient-dev之类的包。
PyMySQL是纯 Python 实现,安装简单,pip install pymysql就完事,跨平台无痛。代价是性能略逊于mysqlclient,而且需要在项目里手动“伪装”成MySQLdb才能被 Django 识别。
我的建议是:本地开发、Windows 环境、图省事,用 PyMySQL;生产环境、追求性能,用 mysqlclient。下面两种我都给配置方法。
# 方案一:mysqlclient(推荐生产用) pip install mysqlclient # 方案二:PyMySQL(推荐本地/Windows用) pip install pymysql如果用 PyMySQL,还要在项目的__init__.py(和settings.py同级那个包目录下的)里加两行:
import pymysql pymysql.install_as_MySQLdb()这两行的作用是让 PyMySQL 冒充MySQLdb模块,因为 Django 内部默认找的是MySQLdb。不加这两行,启动时会报Error loading MySQLdb module。
注意:PyMySQL 的版本要和 Django 版本匹配。Django 较新版本对驱动有版本要求,如果报
mysqlclient 1.4.3 or newer is required之类的错,升级 PyMySQL 到最新版通常能解决。
3.2 配置 settings.py 里的 DATABASES
Django 默认的DATABASES长这样:
DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } }要换成 MySQL,改成:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "mydb", "USER": "root", "PASSWORD": "your_password", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", }, } }逐个字段解释一下,这些参数不是随便填的:
ENGINE:固定写django.db.backends.mysql,告诉 Django 用 MySQL 后端。NAME:数据库名。这个库必须提前在 MySQL 里建好,Django 不会帮你建库,只会建表。这是新手最常踩的坑。USER/PASSWORD:MySQL 账号密码。HOST:数据库地址。本机就是127.0.0.1或localhost。PORT:MySQL 默认 3306,改过端口就填实际端口。OPTIONS.charset:强烈建议设成utf8mb4,而不是utf8。原因下面细说。
3.3 字符集为什么必须用 utf8mb4
这是个特别值得展开的点。MySQL 里的utf8其实是个“假 utf8”,它最多只支持 3 字节的字符,存不了 emoji、存不了一些生僻汉字和特殊符号。而utf8mb4才是真正的 UTF-8,支持 4 字节,能存 emoji。
现在用户输入里带 emoji 太常见了,昵称、评论、备注都可能出现。如果数据库字符集是utf8,插入 emoji 时会直接报错或者变成乱码。所以建库和建表时统一用utf8mb4,这是血泪教训。
建库命令可以这样写:
CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;utf8mb4_unicode_ci是排序规则,ci表示 case insensitive(大小写不敏感),unicode表示按 Unicode 规则排序,对多语言支持更好。
提示:如果库已经建好了但字符集不对,可以用
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改。但已经建好的表不会自动改,需要单独处理。
4. 创建模型与生成数据表
4.1 定义一个模型类
假设我们要做一个简单的博客,先建一个文章模型。在某个 app 的models.py里写:
from django.db import models class Article(models.Model): title = models.CharField(max_length=200, verbose_name="标题") content = models.TextField(verbose_name="正文") author = models.CharField(max_length=50, verbose_name="作者") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") class Meta: db_table = "article" verbose_name = "文章" verbose_name_plural = "文章" def __str__(self): return self.title这里有几个细节值得说:
CharField必须指定max_length,这是硬性要求,因为 MySQL 的VARCHAR需要长度。TextField用于长文本,不需要指定长度,对应 MySQL 的TEXT类型。
auto_now_add=True表示对象第一次创建时自动填入当前时间,之后不再变。auto_now=True表示每次保存时都更新为当前时间。这两个参数省去了手动维护时间戳的麻烦。
class Meta里,db_table指定实际表名。不写的话 Django 会默认用app名_模型名小写,比如blog_article。我习惯显式指定,表名更干净。
__str__方法决定了在 Django 管理后台和 shell 里打印对象时显示什么,调试时非常有用。
4.2 字段类型与 MySQL 类型的对应关系
很多人好奇 Django 字段到底对应 MySQL 什么类型,我整理了一张常用对照表:
| Django 字段 | MySQL 类型 | 说明 |
|---|---|---|
| AutoField | int AUTO_INCREMENT | 自增主键,默认自动添加 |
| CharField | varchar(n) | 必须指定 max_length |
| TextField | longtext | 长文本,无长度限制 |
| IntegerField | int | 整数 |
| BigIntegerField | bigint | 大整数 |
| FloatField | double | 浮点数 |
| DecimalField | decimal | 精确小数,需指定 max_digits 和 decimal_places |
| BooleanField | tinyint(1) | 布尔值 |
| DateTimeField | datetime | 日期时间 |
| DateField | date | 日期 |
| EmailField | varchar | 带邮箱格式校验的 CharField |
| ForeignKey | int | 外键,指向另一张表的主键 |
理解这张表的意义在于:你知道每个字段最终会变成什么,就能预判存储空间和性能。比如金额字段一定要用DecimalField而不是FloatField,因为浮点数有精度问题,算钱会出错。
4.3 makemigrations 与 migrate 的分工
这是 Django 建表的核心两步,很多人搞不清区别,我打个比方:
makemigrations相当于写施工图纸。它对比你的模型和上一次的迁移记录,算出“这次改了什么”,生成一个迁移文件。这个文件是 Python 代码,记录了要建哪些表、加哪些字段。这一步不碰数据库。
migrate相当于按图纸施工。它读取迁移文件,翻译成 SQL,真正在数据库里执行。这一步才真正改数据库。
命令如下:
# 第一步:生成迁移文件 python manage.py makemigrations # 第二步:执行迁移,真正建表 python manage.py migrate执行makemigrations后,你会看到类似输出:
Migrations for 'blog': blog/migrations/0001_initial.py - Create model Article执行migrate后,会看到:
Operations to perform: Apply all migrations: admin, auth, blog, contenttypes, sessions Running migrations: Applying blog.0001_initial... OK看到OK就说明表建好了。可以去 MySQL 里SHOW TABLES;验证一下。
注意:
migrate会同时创建 Django 内置的 admin、auth、sessions 等表,这是正常的,别慌。这些表是 Django 自带功能需要的。
4.4 迁移文件该不该提交到版本库
这个问题争论很多,我的答案是:要提交。迁移文件是项目历史的一部分,记录了数据库结构的演进过程。团队协作时,别人拉下代码,跑一遍migrate就能得到和你一样的表结构。如果不提交,别人就不知道你改了什么。
但有个例外:如果迁移文件多到几百个、跑起来很慢,可以考虑定期“压缩”迁移(squash),把多个迁移合并成一个。Django 提供了squashmigrations命令。不过这是优化手段,初期不用管。
5. 实操验证与常见问题排查
5.1 用 shell 验证数据能存能取
表建好了,怎么确认真的能用?最直接的办法是进 Django shell:
python manage.py shell然后:
from blog.models import Article # 创建一条 a = Article.objects.create(title="第一篇", content="内容", author="张三") print(a.id) # 应该输出 1 # 查询 print(Article.objects.all()) print(Article.objects.filter(author="张三").count()) # 更新 a.title = "改过的标题" a.save() # 删除 a.delete()如果这几步都不报错,说明 ORM 和 MySQL 已经打通了。objects是 Django 自动给每个模型加的管理器(Manager),所有查询都从它出发。
5.2 常见报错与解决思路
我把连 MySQL 时最常遇到的几个错误整理成表,方便对照排查:
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
| Error loading MySQLdb module | 没装驱动或没伪装 | 装 mysqlclient 或 PyMySQL 并加 install_as_MySQLdb |
| Unknown database 'mydb' | 数据库没提前建 | 手动 CREATE DATABASE |
| Access denied for user | 账号密码或权限不对 | 检查 USER/PASSWORD,确认账号有权限 |
| Can't connect to MySQL server | 服务没启动或地址端口错 | 启动 MySQL,检查 HOST/PORT |
| Incorrect string value | 字符集不是 utf8mb4 | 改库和表的字符集 |
| django.db.utils.OperationalError: 1045 | 认证失败 | 核对密码,注意特殊字符转义 |
其中“数据库没提前建”和“字符集不对”这两个,我见过太多次了。前者是概念问题——Django 管表不管库;后者是配置问题——建库时忘了指定 utf8mb4。
5.3 几个容易忽略的实操心得
第一,改完模型一定要重新 makemigrations + migrate。我见过有人改了models.py就直接跑,然后纳闷为什么字段没生效。模型改动不会自动同步到数据库,必须走迁移流程。
第二,迁移前先看会生成什么。python manage.py makemigrations会打印改动内容,python manage.py sqlmigrate blog 0001能看到具体 SQL。生产环境执行前,务必先看 SQL,避免误删字段导致数据丢失。
第三,字段删除要谨慎。如果你从模型里删掉一个字段,makemigrations会生成删除该列的迁移,migrate一跑,数据就没了。如果只是想暂时不用,可以先注释掉,别急着删。
第四,善用verbose_name。给每个字段加中文说明,Django 管理后台会自动显示成中文,省得自己写后台界面。这是低成本高回报的习惯。
第五,null和blank不是一回事。null=True是数据库层面允许为空,blank=True是表单校验层面允许为空。字符串字段一般用blank=True而不是null=True,因为 Django 约定字符串的空值是空字符串""而不是NULL。这个区别新手特别容易混。
6. 从建表到建模的进阶思考
6.1 关系字段怎么设计
真实项目里,表之间几乎一定有联系。Django 提供了三种关系字段:ForeignKey(多对一)、ManyToManyField(多对多)、OneToOneField(一对一)。
拿博客举例,一篇文章属于一个分类,一个分类有多篇文章,这就是多对一:
class Category(models.Model): name = models.CharField(max_length=50) class Article(models.Model): category = models.ForeignKey( Category, on_delete=models.CASCADE, related_name="articles", )on_delete是必填参数,决定分类被删时文章怎么办。CASCADE表示级联删除,分类没了文章也删。还有SET_NULL(设为空)、PROTECT(禁止删除)等选项。这个参数一定要想清楚,选错了可能误删数据。
related_name让你能从分类反查文章,比如category.articles.all()。不指定的话默认是article_set,也能用,但不够直观。
6.2 索引与性能的初步考虑
数据量小的时候,怎么建表都无所谓。但数据一多,查询慢的问题就来了。这时候要考虑加索引。
Django 里加索引有两种方式。一是给字段加db_index=True:
author = models.CharField(max_length=50, db_index=True)二是在Meta里声明:
class Meta: indexes = [ models.Index(fields=["author", "created_at"]), ]索引能加速查询,但会拖慢写入,还会占空间。所以只给经常用来查询、排序、关联的字段加索引,别乱加。比如文章列表按创建时间倒序,那created_at就值得加索引。
6.3 数据库迁移的团队协作规范
多人开发时,迁移文件冲突是常事。两个人同时改了同一个模型,各自生成了迁移,合并代码后可能冲突。
我的经验是:每次拉代码后先跑 migrate,改模型前先确认自己的迁移是最新的。如果真冲突了,别硬合并迁移文件,可以删掉自己本地未提交的迁移,重新makemigrations生成一个。已经提交到远端的迁移,尽量别改,追加新迁移来修正。
还有个小技巧:python manage.py showmigrations能列出所有迁移及其状态,[X]表示已应用,[ ]表示未应用。排查“为什么表结构不对”时,先看这个。
6.4 从 SQLite 迁移到 MySQL 的注意事项
如果你项目一开始用 SQLite,后来想换 MySQL,除了改配置,还要注意:SQLite 的数据不会自动搬过去。需要手动导出再导入,或者用 Django 的dumpdata和loaddata。
步骤大致是:在 SQLite 环境下python manage.py dumpdata > data.json,改配置连上 MySQL,migrate建好表,再python manage.py loaddata data.json。但要注意,dumpdata导出的是模型数据,如果模型有变动,导入可能失败。所以最好在模型稳定后再迁移。
另外,SQLite 对字段类型很宽松,MySQL 严格得多。比如 SQLite 里字符串超长不报错,MySQL 会截断或报错。迁移后要重点测试边界数据。
7. 我踩过的坑和给你的建议
说几个我实际踩过的坑,都是文档里不太会强调的。
坑一:MySQL 8 的认证插件问题。MySQL 8 默认用caching_sha2_password认证插件,老版本的驱动可能连不上,报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法是升级驱动,或者把用户认证方式改成mysql_native_password。后者命令是ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';。
坑二:时区问题。Django 默认USE_TZ=True,存的是 UTC 时间。如果你直接看数据库里的datetime字段,会发现比北京时间少 8 小时。这不是 bug,是设计。显示时 Django 会按TIME_ZONE转换。如果不想用 UTC,可以设USE_TZ=False,但不推荐,多时区场景会乱套。
坑三:迁移文件里的依赖顺序。如果两个 app 的模型互相引用,迁移可能因为顺序问题失败。这时候要在迁移文件的dependencies里手动声明依赖。这种情况不常见,但遇到时很懵。
坑四:max_length设太小。一开始觉得昵称 20 个字符够了,结果用户起了个长名字,插入直接报错。字符串字段的长度,宁可设大一点,CharField的max_length在 MySQL 里就是VARCHAR(n),设 200 和设 50 在存储上差别不大(实际按内容长度存),但设小了会限制用户。
最后给个建议:新手别一上来就追求复杂的模型设计。先把一张表跑通,理解makemigrations和migrate的流程,理解objects怎么用,再逐步加关系、加索引。ORM 这东西,用多了自然就熟了,关键是先跑起来。
如果你卡在某一步,最有效的排查方式是:看完整报错信息(别只看最后一行)、确认数据库服务在跑、确认库和字符集建对了、确认驱动装了。这四步能解决八成问题。