作者:IT策士
10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。
如果你刚刚接触 Django,或者已经被"手写 SQL 语句"折磨过,那你一定会爱上 Django 的 ORM(对象关系映射)。它的核心思想很直接:用 Python 类来描述数据库表,用 Python 代码来操作数据,再也不需要手动拼接 SQL 字符串。
本文会从零开始,带你一步步掌握 Django 模型的设计方法。文中有大量可直接运行的例子和控制台打印输出,无论你是新手还是想进阶的开发者,都能有所收获。
1. ORM 到底帮我们做了什么事?
传统开发中,操作数据库需要:
-
用
CREATE TABLE建表,小心处理字段类型、主键、约束; -
用
INSERT INTO ... VALUES (...)插入数据,还得提防 SQL 注入; -
用
SELECT ... FROM ... WHERE ...查询,然后手动解析结果集。
Django 的 ORM 层帮你完成了这些翻译工作:你定义好 Python 类,框架负责生成对应的 SQL 并执行。你写的是:
bash
Book.objects.filter(title__startswith="Django")
ORM 会把它转成类似这样的 SQL:
bash
SELECT * FROM books_book WHERE title LIKE 'Django%';
这样一来,业务逻辑全部用 Python 表达,数据库操作变得安全、直观且易于维护。
2. 第一个模型:从一本书开始
假设我们要管理一批图书。在 Django 中,模型通常定义在 models.py 里。每个模型类都继承自 django.db.models.Model。
bash
# models.py
from django.db import models
class Book(models.Model):
# 字符字段:书名,最大长度 200,唯一
title = models.CharField(max_length=200, unique=True)
# 整数字段:页数,允许为空(数据库 NULL)
pages = models.IntegerField(null=True, blank=True)
# 小数:价格,最多 6 位,其中 2 位小数
price = models.DecimalField(max_digits=6, decimal_places=2)
# 日期:出版日期,可以设置默认值
pub_date = models.DateField(null=True, blank=True)
# 布尔值:是否在售,默认 True
is_on_sale = models.BooleanField(default=True)
def __str__(self):
return self.title
字段类型一览(常用)
-
CharField:必须指定max_length。 -
TextField:不限长度的文本,数据库里通常是text类型。 -
IntegerField、FloatField、DecimalField:数字类型,Decimal 需要max_digits和decimal_places。 -
DateField、DateTimeField:日期和时间,参数auto_now_add(创建时自动设)和auto_now(保存时自动更新)很实用。 -
BooleanField:布尔值。 -
EmailField、URLField:带有校验功能的 CharField 子类。 -
FileField、ImageField:文件上传,需要配置upload_to。
常用字段参数
-
null=True:数据库允许NULL(默认False)。 -
blank=True:表单验证时允许为空(默认False)。两者常配合使用。 -
default:默认值,可以是值或 callable。 -
unique=True:值在整张表唯一。 -
choices:给字段限定可选值,如status = models.CharField(max_length=1, choices=[('d', 'Draft'), ('p', 'Published')])。 -
verbose_name:人类可读的字段名,用于后台显示。
定义好模型后,你还没真正在数据库里创建表。接下来需要执行迁移。
3. 从 Python 类到数据库表:迁移命令
在 Django 项目目录下,运行:
bash
$ python manage.py makemigrations
Migrations for 'books':
books/migrations/0001_initial.py
- Create model Book
这条命令扫描模型变动,生成迁移文件(一个 Python 脚本,记录了如何修改数据库结构)。然后再执行:
bash
$ python manage.py migrate
Operations to perform:
Apply all migrations: books, admin, auth, contenttypes, sessions
Running migrations:
Applying books.0001_initial... OK
此时,数据库中已经存在一个 books_book 表(表名由 应用名_模型名小写 组成)。你完全不需要写一行 SQL。
如果你好奇 ORM 会生成什么 SQL,可以用 sqlmigrate 命令查看:
bash
$ python manage.py sqlmigrate books 0001
BEGIN;
--
-- Create model Book
--
CREATE TABLE "books_book" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(200) NOT NULL UNIQUE,
"pages" integer NULL,
"price" decimal NOT NULL,
"pub_date" date NULL,
"is_on_sale" bool NOT NULL DEFAULT 1
);
COMMIT;
(这里展示的是 SQLite 语法,MySQL/PostgreSQL 会略有不同。)
4. 在 Django Shell 里体验 CRUD
我们来通过交互式 Shell 感受一下"不用写 SQL"的快乐。
首先进入 Shell:
导入模型并开启 SQL 日志,方便看到 ORM 实际执行了什么:
bash
>>> from books.models import Book
>>> from django.db import connection
4.1 创建数据
bash
>>> book1 = Book.objects.create(
... title="Django 5 By Example",
... pages=520,
... price=59.99,
... pub_date="2025-01-15",
... )
>>> print(connection.queries[-1]['sql'])
INSERT INTO "books_book" ("title", "pages", "price", "pub_date", "is_on_sale")
VALUES ('Django 5 By Example', 520, 59.99, '2025-01-15', 1)
也可以实例化后调用 .save():
bash
>>> book2 = Book(title="Two Scoops of Django", pages=300, price=44.95)
>>> book2.save()
4.2 查询数据
获取全部对象:
bash
>>> Book.objects.all()
<QuerySet [<Book: Django 5 By Example>, <Book: Two Scoops of Django>]>
条件过滤:
bash
>>> Book.objects.filter(pages__gt=400) # pages 大于 400
<QuerySet [<Book: Django 5 By Example>]>
bash
>>> Book.objects.filter(title__icontains="django") # 不区分大小写的包含
<QuerySet [<Book: Django 5 By Example>, <Book: Two Scoops of Django>]>
获取单条记录:
bash
>>> book = Book.objects.get(id=1)
>>> book.title
'Django 5 By Example'
如果找不到对象,get() 会抛出 Book.DoesNotExist 异常,这是一层安全保障。
排除条件:
bash
>>> Book.objects.exclude(is_on_sale=False)
# 等同于 .filter(is_on_sale=True)
4.3 更新与删除
更新一条记录:
bash
>>> book = Book.objects.get(id=2)
>>> book.price = 39.99
>>> book.save()
批量更新(不会调用 save(),直接生成 UPDATE SQL):
bash
>>> Book.objects.filter(pages__lt=350).update(is_on_sale=False)
1 # 返回受影响的行数
删除:
bash
>>> book.delete()
(1, {'books.Book': 1})
4.4 排序与切片
bash
>>> Book.objects.order_by('-pub_date') # 按出版日期倒序
>>> Book.objects.order_by('price')[:3] # 取最便宜的前三本书
这里的切片直接翻译成 SQL 的 LIMIT,并不会把所有数据读到内存里,效率很高。
4.5 常用字段查找(Field Lookups)
双下划线 __ 是 Django ORM 的强大武器。常用查找类型:
你可以组合多个查找来实现复杂查询,如:
bash
>>> Book.objects.filter(
... price__lt=60,
... pages__gte=400,
... title__icontains="django"
... ).exclude(is_on_sale=False)
5. 模型之间的关系
现实中的实体很少是孤立的。一本书有作者、出版社、标签,这些都需要用关系来建模。
5.1 外键(多对一)
一个作者可以写多本书,所以 Book 到 Author 是多对一的关系。
bash
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
# ... 其他字段
author = models.ForeignKey(
Author,
on_delete=models.CASCADE, # 作者删除时,他的所有书也删除
related_name='books' # 从作者反向查书的名称
)
on_delete 选项:
-
CASCADE:级联删除。 -
PROTECT:防止删除(抛出异常)。 -
SET_NULL:设为 NULL(需要null=True)。 -
SET_DEFAULT:设为默认值。 -
DO_NOTHING:不做任何操作(可能引发数据库报错)。
操作示例:
bash
>>> author1 = Author.objects.create(name="William S. Vincent")
>>> book = Book.objects.create(
... title="Django for Professionals", pages=380, price=49.00,
... author=author1
... )
>>> # 正向查询:从书到作者
>>> book.author.name
'William S. Vincent'
>>> # 反向查询:从作者到他的所有书(因为设置了 related_name='books')
>>> author1.books.all()
<QuerySet [<Book: Django for Professionals>]>
跨关系过滤:
查询写过页数大于 400 的书的所有作者:
bash
>>> Author.objects.filter(books__pages__gt=400)
ORM 自动进行 JOIN 操作。你也可以直接在 Book 查询里用作者信息:
bash
>>> Book.objects.filter(author__name__startswith="William")
5.2 多对多
一本书可以有多个标签(Tag),一个标签也可以被多本书使用。
bash
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.name
class Book(models.Model):
# ...
tags = models.ManyToManyField(Tag, related_name='books')
Django 会自动创建一张中间表(如 books_book_tags)来维护关系,你完全不用手动管理。
使用多对多关系:
bash
>>> tag_django = Tag.objects.create(name="Django")
>>> tag_python = Tag.objects.create(name="Python")
>>> book = Book.objects.get(id=1)
>>> book.tags.add(tag_django, tag_python)
>>> book.tags.all()
<QuerySet [<Tag: Django>, <Tag: Python>]>
>>> # 反向查:哪些书有 "Python" 标签
>>> tag_python.books.all()
<QuerySet [<Book: Django for Professionals>]>
要移除标签:book.tags.remove(tag_python)
要清空:book.tags.clear()
要直接设置标签列表:book.tags.set([tag_django])
跨多对多关系查询:
找出同时拥有 "Django" 和 "Python" 标签的书:
bash
>>> Book.objects.filter(tags__name="Django").filter(tags__name="Python")
注意:这里连续 filter 会产生两个 INNER JOIN,查询"同时拥有"两个标签的记录。
5.3 一对一
如果想把用户资料(Profile)从用户模型(User)中分离出来,一对一就很合适。
bash
from django.contrib.auth.models import User
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
使用示例:
bash
>>> user = User.objects.get(id=1)
>>> # 创建关联 profile
>>> profile = Profile.objects.create(user=user, bio="Django developer")
>>> # 访问
>>> user.profile.bio
'Django developer'
>>> profile.user.username
'admin'
6. 模型方法、属性和 __str__
模型不只是数据容器,还可以包含业务逻辑。
bash
class Book(models.Model):
# ... 字段定义
def is_long(self):
"""判断是否超过 500 页"""
return self.pages and self.pages > 500
@property
def price_display(self):
"""格式化显示价格"""
return f"${self.price:,.2f}"
def __str__(self):
return self.title
使用:
bash
>>> book = Book.objects.get(id=1)
>>> book.is_long()
True
>>> book.price_display
'$59.99'
把 __str__ 写好,能让 print、admin 后台、Shell 输出都清晰易读。
7. Meta 类的魔法
内部类 Meta 用于定义模型的元数据,比如排序、约束、表名等。
bash
class Book(models.Model):
# ... 字段
class Meta:
# 默认排序
ordering = ['-pub_date', 'title']
# 单复数显示名称(admin 用)
verbose_name = 'Book'
verbose_name_plural = 'Books'
# 自定义数据库表名(默认是 应用名_模型名小写)
db_table = 'library_books'
# 联合唯一约束
unique_together = [['title', 'author']]
# 索引
indexes = [
models.Index(fields=['pub_date']),
models.Index(fields=['price', 'pages']),
]
# 自定义权限
permissions = [
("can_publish", "Can publish books"),
]
Django 会在迁移时自动生成对应的索引和约束。例如 unique_together 会在数据库中创建 UNIQUE 约束,防止相同作者写两本完全同名书。
8. 迁移管理进阶
查看迁移计划
bash
$ python manage.py showmigrations
books
[X] 0001_initial
[X] 0002_author_tag
[ ] 0003_add_indexes
回滚迁移
假设你想回到上一个状态:
bash
$ python manage.py migrate books 0002
此时 0003_add_indexes 会被撤销,对应的数据库索引也会删除。
将迁移合并为一条
随着项目变大,迁移文件越来越多,你可以用 squashmigrations 把它们压缩成一个文件:
bash
$ python manage.py squashmigrations books 0001 0005
这不会影响数据库,但能让迁移历史更干净。
9. 进阶设计模式(简要)
9.1 抽象基类
当多个模型有相同的字段,可以抽取一个抽象基类,它不会创建数据库表。
bash
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Book(TimeStampedModel):
title = models.CharField(max_length=200)
# ...
现在 Book 自动拥有 created_at 和 updated_at 字段。
9.2 代理模型
代理模型不改数据库结构,只改变 Python 行为(如默认排序、新增方法)。
bash
class PublishedBookManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_on_sale=True)
class PublishedBook(Book):
objects = PublishedBookManager()
class Meta:
proxy = True
ordering = ['-pub_date']
PublishedBook 和 Book 用同一张表,但 PublishedBook.objects.all() 只会返回在售的图书,而且默认按出版日期倒序排列。
9.3 自定义管理器与 QuerySet
你可以封装常用的查询链,让代码更具可读性。
bash
class BookQuerySet(models.QuerySet):
def heavy_reads(self):
return self.filter(pages__gte=500)
def affordable(self):
return self.filter(price__lte=30)
class Book(models.Model):
# ...
objects = BookQuerySet.as_manager()
# 使用
>>> Book.objects.heavy_reads().affordable()
9.4 数据库函数与聚合
ORM 也支持直接调用数据库函数(如 Lower, Coalesce)和聚合(Count, Avg, Max)。
bash
from django.db.models import Count, Avg
# 每个作者的书籍数量
>>> Author.objects.annotate(book_count=Count('books'))
# 每本书的平均价格
>>> Book.objects.aggregate(avg_price=Avg('price'))
{'avg_price': Decimal('51.33')}
10. 总结:为什么说"无需 SQL"?
我们已经完整地走了一圈:
-
定义模型 :用 Python 类,字段类型和参数代替
CREATE TABLE。 -
生成表 :
makemigrations+migrate命令自动处理。 -
增删改查 :
create()、save()、filter()、update()、delete()完全用 Python 对象和方法。 -
关联查询 :点操作、双下划线、
related_name让你像访问属性一样操作外键和多对多。 -
业务逻辑:模型方法、属性、管理器,让代码集中在模型层,而不是散落在 SQL 字符串里。
但"无需 SQL"并不意味着可以完全忽视数据库。当你需要优化性能时,可以用 connection.queries 查看生成的 SQL,或用 explain() 分析查询计划,再通过 select_related、prefetch_related 或索引来调优。Django 模型让你起步时不必写 SQL,深入时又能精细控制 SQL,这正是它强大又平易近人的原因。
希望这篇文章帮你建立了对 Django 模型的系统认识。接下来,打开你的 models.py,把数据世界用 Python 描述出来吧。想了解更多,还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !