模型与 ORM
字段、关系与 migrations。
模型与 ORM
这一课你将能做到
- 用 Django Model 定义带各种字段和约束的数据表结构
- 定义外键、多对多关系,并掌握
related_name 反向查询
- 熟练使用 migrations 工作流:makemigrations、migrate、回滚、解决冲突
为什么要学
ORM 是 Django 的核心武器。它把「Python 对象」和「数据库表」一一映射,让你不用写 SQL 就能操作数据。本站用 Spring Boot + JPA(Hibernate),原理一模一样:Java 类对应表,属性对应列。但 Django 的 ORM 在语法糖上更激进,很多查询一行就能搞定。
人话:ORM 就是给数据库套了个 Python 外壳,你操作的是对象,它帮你翻译成 SQL。不会 SQL 也能干活,但懂 SQL 才能写出不卡的代码。
先建立图像
Python 类 数据库表
┌─────────────────────┐ ┌─────────────────┐
│ class Article: │ │ article │
│ title = CharField │ ──> │ id title │
│ author = FK(User) │ │ author_id │
└─────────────────────┘ └─────────────────┘
│ ▲
│ article.save() │
└───────── INSERT INTO ──────┘
article.title = "新标题"
article.save()
└───────── UPDATE ──────────┘
跟着做
步骤 1:定义第一个模型
blog/models.py:
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name="分类名")
slug = models.SlugField(max_length=100, unique=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = "分类"
verbose_name_plural = "分类"
ordering = ["name"]
def __str__(self):
return self.name
class Article(models.Model):
STATUS_CHOICES = [
("draft", "草稿"),
("published", "已发布"),
("archived", "已归档"),
]
title = models.CharField(max_length=200, verbose_name="标题")
slug = models.SlugField(max_length=200, unique=True)
content = models.TextField(verbose_name="正文")
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="articles",
verbose_name="作者"
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="articles"
)
tags = models.ManyToManyField("Tag", blank=True, related_name="articles")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="draft")
views = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = "文章"
verbose_name_plural = "文章"
ordering = ["-created_at"]
indexes = [
models.Index(fields=["status", "-created_at"]),
]
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
| 字段 | 用途 | 关键参数 |
|---|
CharField | 短文本 | max_length(必填)、unique、default |
TextField | 长文本 | 无长度限制 |
IntegerField / PositiveIntegerField | 整数 | default、validators |
DateTimeField | 日期时间 | auto_now_add(创建时)、auto_now(每次保存) |
BooleanField | 布尔 | default |
EmailField / URLField / SlugField | 带格式校验的字符串 | 同 CharField |
DecimalField | 精确小数 | max_digits、decimal_places(金额必用) |
ForeignKey | 多对一 | on_delete 必填 |
ManyToManyField | 多对多 | 自动建中间表 |
OneToOneField | 一对一 | 类似 FK 但加唯一约束 |
步骤 2:理解 on_delete 的四种行为
# 1. CASCADE:作者删除 → 所有文章删除(危险!)
author = models.ForeignKey(User, on_delete=models.CASCADE)
# 2. PROTECT:有文章时禁止删除作者
author = models.ForeignKey(User, on_delete=models.PROTECT)
# 3. SET_NULL:作者删除 → 文章保留但 author 设为 NULL(需要 null=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
# 4. SET_DEFAULT:作者删除 → author 设为默认值
author = models.ForeignKey(User, on_delete=models.SET_DEFAULT, default=1)
对比 Spring Data JPA:on_delete=models.CASCADE 类似 @OneToMany(cascade = CascadeType.REMOVE),但 Django 默认在数据库层面也加外键约束,JPA 默认只在 ORM 层做级联。
步骤 3:执行迁移
# 生成迁移文件(检查 models 变化,生成 SQL 的 Python 描述)
python manage.py makemigrations blog
# 查看将要执行的 SQL(不真正执行)
python manage.py sqlmigrate blog 0001
# 真正执行迁移
python manage.py migrate blog
# 查看所有迁移状态
python manage.py showmigrations blog
迁移文件长这样(blog/migrations/0001_initial.py):
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="Article",
fields=[
("id", models.BigAutoField(...)),
("title", models.CharField(max_length=200)),
("author", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, ...)),
...
],
),
]
步骤 4:基本 CRUD 操作
# 进入 Django shell 操作
python manage.py shell
# Create
from blog.models import Article, Category, Tag
from django.contrib.auth.models import User
user = User.objects.create_user(username="alice", password="pass123")
cat = Category.objects.create(name="Python", slug="python")
tag1 = Tag.objects.create(name="Django")
tag2 = Tag.objects.create(name="ORM")
article = Article.objects.create(
title="Django ORM 入门",
slug="django-orm-intro",
content="...",
author=user,
category=cat,
status="published"
)
article.tags.add(tag1, tag2) # 多对多必须 save 之后才能 add
# Read
article = Article.objects.get(id=1)
articles = Article.objects.filter(status="published")
first = Article.objects.filter(author=user).first()
# Update
article.title = "新标题"
article.views += 1
article.save()
# 批量更新(不触发 save 信号)
Article.objects.filter(status="draft").update(status="archived")
# Delete
article.delete()
# 反向查询(通过 related_name)
user.articles.all() # 该作者的所有文章
cat.articles.count() # 该分类的文章数
tag1.articles.filter(status="published") # 该标签下的已发布文章
步骤 5:自定义 Manager 和查询方法
class ArticleManager(models.Manager):
def published(self):
return self.filter(status="published")
def by_author(self, user):
return self.filter(author=user, status="published")
class Article(models.Model):
# ... 字段定义 ...
objects = ArticleManager() # 替换默认 manager
# 实例方法
def is_hot(self):
return self.views > 1000
# 使用
Article.objects.published()
Article.objects.by_author(user)
article.is_hot()
讲清楚要点
- Model = 表,字段 = 列,实例 = 行:一对一映射。
Meta 类控制表名、排序、索引、约束。
- migrations 是版本控制:每次改 models.py 都要
makemigrations 生成迁移文件,提交到 git。migrate 在数据库执行。它和 git 一样,有历史、能回滚:migrate blog 0001 回到某个版本。
related_name 是反向查询的钥匙:不设置时默认是 article_set,设置了 related_name="articles" 就能用 user.articles.all()。建议永远显式设置。
- N+1 问题在 ORM 里天然存在:
for a in Article.objects.all(): print(a.author.name) 会执行 1 + N 次查询。用 select_related()(FK)和 prefetch_related()(M2M)解决,第 6 课详讲。
auto_now_add vs auto_now:前者只在创建时写入,后者每次 save 都更新。两者都用 timezone.now(),不受手动赋值影响。
- 数据库索引要手动加:Django 不会自动给所有字段加索引。
db_index=True 或 Meta.indexes,给经常出现在 WHERE / ORDER BY 的字段加。
你可能会懵的地方
-
null=True 和 blank=True 的区别?
null=True 是数据库层:允许存 NULL。blank=True 是表单验证层:允许表单提交空值。CharField 一般只用 blank=True 不用 null=True(避免同时存在空字符串和 NULL 两种「空」状态)。ForeignKey 两个都要加才能允许为空。
-
ForeignKey 和 OneToOneField 的区别?
OneToOneField 就是加了 UNIQUE 约束的 ForeignKey。常用于「扩展 User 模型」:Profile.user = OneToOneField(User)。反向查询时 OneToOne 直接 user.profile(单数),FK 是 user.articles(复数)。
-
ManyToManyField 中间表能自己控制吗?
能。用 through 参数指定中间模型,可以在中间表加额外字段(如 added_at、role)。例如「用户-文章-收藏时间」三元关系。
-
save() 和 objects.create() 的区别?
create() 是「构造 + save」的快捷方式,一步完成。手动 obj = Model(...); obj.save() 分两步,中间可以改属性。save() 可以用 update_fields=["title"] 只更新部分字段。
-
Django 支持复合主键吗?
5.2 之前不原生支持,需要 workaround(如 AutoField + UniqueConstraint)。5.2+ 引入了 CompositePrimaryKey。但实践中很少用,单一主键 + 唯一约束足够。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 改了 models.py 直接重启服务 | 表结构没变,运行报错 | 先 makemigrations 再 migrate |
CharField 不写 max_length | 迁移时报错 | CharField 必须指定 max_length |
用 on_delete=models.CASCADE 不加思考 | 删用户把文章全删了 | 根据业务选 SET_NULL / PROTECT |
循环里 article.author.name | N+1 查询,100 篇文章 101 次 SQL | select_related("author") |
金额用 FloatField | 浮点精度丢失 | DecimalField(max_digits=10, decimal_places=2) |
| 手动执行 SQL 改表结构 | 迁移历史与实际不符,冲突 | 永远通过 migrations 变更结构 |
卡点急救
makemigrations 提示 "No changes detected":app 没在 INSTALLED_APPS 里,或 models.py 有语法错误。检查 import。
migrate 报 "InconsistentMigrationHistory":手动改了数据库或迁移文件顺序乱了。生产环境小心,开发环境可以删 db.sqlite3 和 migrations/pycache 重来(仅限开发!)。
- 多对多
add() 报 "needs to have a value for field id":实例还没 save 就 add 标签。先 article.save() 再 article.tags.add(...)。
面试常问
-
Django ORM 的 N+1 问题是什么?怎么解决?
查询文章列表后再访问每篇文章的作者,会触发 1 次查询文章 + N 次查询作者。解决:FK 用 select_related("author")(SQL JOIN),M2M 和反向 FK 用 prefetch_related("tags")(两次查询 + Python 拼接)。
-
migrations 的工作原理?
makemigrations 对比当前 models.py 和上次迁移文件的状态,生成新的迁移文件(纯 Python 描述)。migrate 把迁移文件翻译成 SQL 执行,并在 django_migrations 表记录已执行的迁移。本质是数据库 schema 的版本控制系统。
-
select_related 和 prefetch_related 的区别?
select_related 用 SQL JOIN 一次查回,适合 ForeignKey 和 OneToOne(一对一/多对一)。prefetch_related 分两次查询,第二次用 IN (ids) 批量查,然后在 Python 层组装,适合 ManyToMany 和反向 FK(一对多/多对多)。
自测三问
null=True 和 blank=True 分别作用于哪一层?为什么 CharField 不建议用 null=True?
- 画出 User → Article → Tag 的 ER 关系,并说明每种关系的
on_delete 应该怎么选。
- 什么时候需要自定义 Manager?给一个具体场景。
小结一句话
Django ORM 用 Python 类定义表结构,用 migrations 做版本控制,用 related_name 打通反向查询;理解字段类型、关系映射和 N+1 问题,就掌握了 80% 的日常开发。
延伸