CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Django 实战入门/课时/模型与 ORM
RawBlame
课时目录12 节

Django 实战入门

  1. 01Django 概览
  2. 02项目结构
  3. 03URL 路由
  4. 04视图层
  5. 05模型与 ORM
  6. 06查询与优化
  7. 07模板 DTL
  8. 08Admin 与认证
  9. 09表单与安全
  10. 10DRF 与异步任务
  11. 11测试与部署
  12. 12综合练习

模型与 ORM

字段、关系与 migrations。

30 分钟

模型与 ORM

这一课你将能做到

  1. 用 Django Model 定义带各种字段和约束的数据表结构
  2. 定义外键、多对多关系,并掌握 related_name 反向查询
  3. 熟练使用 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()

讲清楚要点

  1. Model = 表,字段 = 列,实例 = 行:一对一映射。Meta 类控制表名、排序、索引、约束。
  2. migrations 是版本控制:每次改 models.py 都要 makemigrations 生成迁移文件,提交到 git。migrate 在数据库执行。它和 git 一样,有历史、能回滚:migrate blog 0001 回到某个版本。
  3. related_name 是反向查询的钥匙:不设置时默认是 article_set,设置了 related_name="articles" 就能用 user.articles.all()。建议永远显式设置。
  4. N+1 问题在 ORM 里天然存在:for a in Article.objects.all(): print(a.author.name) 会执行 1 + N 次查询。用 select_related()(FK)和 prefetch_related()(M2M)解决,第 6 课详讲。
  5. auto_now_add vs auto_now:前者只在创建时写入,后者每次 save 都更新。两者都用 timezone.now(),不受手动赋值影响。
  6. 数据库索引要手动加: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.nameN+1 查询,100 篇文章 101 次 SQLselect_related("author")
金额用 FloatField浮点精度丢失DecimalField(max_digits=10, decimal_places=2)
手动执行 SQL 改表结构迁移历史与实际不符,冲突永远通过 migrations 变更结构

卡点急救

  1. makemigrations 提示 "No changes detected":app 没在 INSTALLED_APPS 里,或 models.py 有语法错误。检查 import。
  2. migrate 报 "InconsistentMigrationHistory":手动改了数据库或迁移文件顺序乱了。生产环境小心,开发环境可以删 db.sqlite3 和 migrations/pycache 重来(仅限开发!)。
  3. 多对多 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(一对多/多对多)。

自测三问

  1. null=True 和 blank=True 分别作用于哪一层?为什么 CharField 不建议用 null=True?
  2. 画出 User → Article → Tag 的 ER 关系,并说明每种关系的 on_delete 应该怎么选。
  3. 什么时候需要自定义 Manager?给一个具体场景。

小结一句话

Django ORM 用 Python 类定义表结构,用 migrations 做版本控制,用 related_name 打通反向查询;理解字段类型、关系映射和 N+1 问题,就掌握了 80% 的日常开发。

延伸

  • Django 官方文档 - 模型
  • Django 官方文档 - 迁移
  • 下一课:查询与优化——QuerySet 高级用法、聚合、性能调优。
加载本节练习…
← 上一课视图层下一课 →查询与优化