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 原则——Dont Repeat Yourself你只在一个地方定义数据结构其他地方都从它派生。我个人的体会是模型设计得好不好直接决定后面写代码顺不顺。字段类型选错、关系没理清后面改起来要迁移、要处理历史数据非常痛苦。所以建模阶段多花十分钟想清楚比后面返工划算得多。2.2 ORM 帮你屏蔽了哪些数据库差异不同数据库的 SQL 方言是有差别的。比如自增主键MySQL 用AUTO_INCREMENTPostgreSQL 用SERIALSQLite 又是另一套。字符串类型、日期函数、分页语法各家都不太一样。Django ORM 在中间做了一层翻译。你写User.objects.filter(name张三)它会根据你配置的数据库类型翻译成对应的 SQL。这意味着同一套模型代码理论上可以跑在 SQLite、MySQL、PostgreSQL 上切换数据库时业务代码基本不用动。但这里有个坑要提前说“理论上”不等于“完全无痛”。不同数据库对字段长度、索引、事务隔离级别的支持不一样切换时还是要注意。比如 MySQL 的VARCHAR必须指定长度而 PostgreSQL 的VARCHAR可以不指定。Django 会帮你处理大部分但涉及数据库特有功能时还是得看文档。2.3 为什么真实项目更倾向 MySQLSQLite 是个文件数据库零配置、单文件、方便携带做小工具、做原型、做本地开发非常爽。但它有几个硬伤并发写入能力弱写操作会锁整个库、没有用户权限体系、不适合多机部署。真实项目一旦上线往往要面对多用户并发、要备份、要做主从、要远程连接这时候 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 里的 DATABASESDjango 默认的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/PASSWORDMySQL 账号密码。HOST数据库地址。本机就是127.0.0.1或localhost。PORTMySQL 默认 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_length200, verbose_name标题) content models.TextField(verbose_name正文) author models.CharField(max_length50, verbose_name作者) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, 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_addTrue表示对象第一次创建时自动填入当前时间之后不再变。auto_nowTrue表示每次保存时都更新为当前时间。这两个参数省去了手动维护时间戳的麻烦。class Meta里db_table指定实际表名。不写的话 Django 会默认用app名_模型名小写比如blog_article。我习惯显式指定表名更干净。__str__方法决定了在 Django 管理后台和 shell 里打印对象时显示什么调试时非常有用。4.2 字段类型与 MySQL 类型的对应关系很多人好奇 Django 字段到底对应 MySQL 什么类型我整理了一张常用对照表Django 字段MySQL 类型说明AutoFieldint AUTO_INCREMENT自增主键默认自动添加CharFieldvarchar(n)必须指定 max_lengthTextFieldlongtext长文本无长度限制IntegerFieldint整数BigIntegerFieldbigint大整数FloatFielddouble浮点数DecimalFielddecimal精确小数需指定 max_digits 和 decimal_placesBooleanFieldtinyint(1)布尔值DateTimeFielddatetime日期时间DateFielddate日期EmailFieldvarchar带邮箱格式校验的 CharFieldForeignKeyint外键指向另一张表的主键理解这张表的意义在于你知道每个字段最终会变成什么就能预判存储空间和性能。比如金额字段一定要用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 shellpython 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_MySQLdbUnknown database mydb数据库没提前建手动 CREATE DATABASEAccess denied for user账号密码或权限不对检查 USER/PASSWORD确认账号有权限Cant connect to MySQL server服务没启动或地址端口错启动 MySQL检查 HOST/PORTIncorrect 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不是一回事。nullTrue是数据库层面允许为空blankTrue是表单校验层面允许为空。字符串字段一般用blankTrue而不是nullTrue因为 Django 约定字符串的空值是空字符串而不是NULL。这个区别新手特别容易混。6. 从建表到建模的进阶思考6.1 关系字段怎么设计真实项目里表之间几乎一定有联系。Django 提供了三种关系字段ForeignKey多对一、ManyToManyField多对多、OneToOneField一对一。拿博客举例一篇文章属于一个分类一个分类有多篇文章这就是多对一class Category(models.Model): name models.CharField(max_length50) class Article(models.Model): category models.ForeignKey( Category, on_deletemodels.CASCADE, related_namearticles, )on_delete是必填参数决定分类被删时文章怎么办。CASCADE表示级联删除分类没了文章也删。还有SET_NULL设为空、PROTECT禁止删除等选项。这个参数一定要想清楚选错了可能误删数据。related_name让你能从分类反查文章比如category.articles.all()。不指定的话默认是article_set也能用但不够直观。6.2 索引与性能的初步考虑数据量小的时候怎么建表都无所谓。但数据一多查询慢的问题就来了。这时候要考虑加索引。Django 里加索引有两种方式。一是给字段加db_indexTrueauthor models.CharField(max_length50, db_indexTrue)二是在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改配置连上 MySQLmigrate建好表再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 rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;。坑二时区问题。Django 默认USE_TZTrue存的是 UTC 时间。如果你直接看数据库里的datetime字段会发现比北京时间少 8 小时。这不是 bug是设计。显示时 Django 会按TIME_ZONE转换。如果不想用 UTC可以设USE_TZFalse但不推荐多时区场景会乱套。坑三迁移文件里的依赖顺序。如果两个 app 的模型互相引用迁移可能因为顺序问题失败。这时候要在迁移文件的dependencies里手动声明依赖。这种情况不常见但遇到时很懵。坑四max_length设太小。一开始觉得昵称 20 个字符够了结果用户起了个长名字插入直接报错。字符串字段的长度宁可设大一点CharField的max_length在 MySQL 里就是VARCHAR(n)设 200 和设 50 在存储上差别不大实际按内容长度存但设小了会限制用户。最后给个建议新手别一上来就追求复杂的模型设计。先把一张表跑通理解makemigrations和migrate的流程理解objects怎么用再逐步加关系、加索引。ORM 这东西用多了自然就熟了关键是先跑起来。如果你卡在某一步最有效的排查方式是看完整报错信息别只看最后一行、确认数据库服务在跑、确认库和字符集建对了、确认驱动装了。这四步能解决八成问题。