Django 模型(Model)设计:无需 SQL,用 Python 类定义你的数据库

作者: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 类型。

  • IntegerFieldFloatFieldDecimalField:数字类型,Decimal 需要 max_digitsdecimal_places

  • DateFieldDateTimeField:日期和时间,参数 auto_now_add(创建时自动设)和 auto_now(保存时自动更新)很实用。

  • BooleanField:布尔值。

  • EmailFieldURLField:带有校验功能的 CharField 子类。

  • FileFieldImageField:文件上传,需要配置 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 外键(多对一)

一个作者可以写多本书,所以 BookAuthor 是多对一的关系。

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_atupdated_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']

PublishedBookBook 用同一张表,但 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_relatedprefetch_related 或索引来调优。Django 模型让你起步时不必写 SQL,深入时又能精细控制 SQL,这正是它强大又平易近人的原因。

希望这篇文章帮你建立了对 Django 模型的系统认识。接下来,打开你的 models.py,把数据世界用 Python 描述出来吧。想了解更多,还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !

相关推荐
传说之后1 小时前
Go 调用 OpenAI 兼容 API:对话、流式输出、上下文与图片识别
后端
传说之后1 小时前
Go Channel 解析:原理与实践
后端
XovH1 小时前
Django Admin:5 分钟搭建一个全功能的后台管理系统
后端
阿星做前端1 小时前
不想再给ai回复下一步了,于是我给agent装上了一个自动挡
前端·后端·程序员
SimonKing1 小时前
Firefox 太卡?换了这浏览器,内存占用直接降了 70%
java·后端·程序员
小羊在睡觉1 小时前
Harness工程
后端·ai·ai编程
咖啡八杯1 小时前
GoF设计模式——建造者模式
java·后端
l软件定制开发工作室2 小时前
Spring开发系列教程(41)——集成Open API
java·后端·spring
传说之后2 小时前
GO语言 理解 Goroutine:使用与原理
后端