外观
Django框架-构建在线学习平台
约 8900 字大约 30 分钟
2026-03-24
本节将创建一个新的 Django 项目,其中包含一个自定义内容管理系统(CMS)的在线学习平台。在线学习平台是需要高级内容处理工具的典型应用场景。
本节你将学会:
为内容管理系统创建数据模型
用 fixtures(数据夹具)为模型填充初始数据
通过模型继承实现多态内容的数据模型
创建自定义模型字段
实现课程模块和内容的排序功能
功能概述
本节重点讲解模型继承和自定义模型字段的实现。由于这部分内容结构比较特殊,之前常用的图表不太方便展示,所以我们会用一些专门的图表来帮你理解这些概念。
- 整体项目目录结构:
educa/ # 外部 educa/ 根目录只是一个项目的容器
├── manage.py # 命令行实用程序,实现与此Django项目进行各种交互
├── educa # 内部 educa/ 目录是当前项目的实际Python包
│ ├── __init__.py # 空文件,代表Python包的标志文件
│ ├── settings.py # 此Django项目的设置/配置 文件
│ ├── urls.py # 该Django项目的URL路由声明
│ ├── asgi.py # ASGI兼容的Web服务器为您的项目提供服务的入口点
│ └── wsgi.py # WSGI兼容的Web服务器为您的项目提供服务的入口点
├── courses # courses/目录为账户应用程序目录名称(Python包)
│ ├── __init__.py # 空文件,代表Python包的标志文件
│ ├── admin.py # 用于在Django管理站点中注册模型的,使用此站点是可选
│ ├── apps.py # 包括blog应用程序的主要配置
│ ├── migrations # 此目录将包含应用程序的数据库迁移文件(Python包)
│ ├── models.py # 应用程序的数据模型,每个应用程序的必须文件
│ ├── tests.py # 在这里为你的应用程序添加测试
│ ├── urls.py # 自定义当前应用URL路由配置
│ ├── views.py # 应用程序的逻辑写在这里,HTTP请求、处理并返回响应。
│
├── media/ # 存放上传的图片目录- 关于上面完整的静态资源与模板文件的下载:
一、 构建项目前的准备
注:准备Python3.12版本的虚拟环境,并安装Django5.2版本框架。若已完成请跳过。
1. 使用Python3.12的版本:
Django 5.2 支持 Python 3.10、3.11、3.12 和 3.13。接下来我们将使用 Python 3.12。
如果你的 Python 版本低于 3.12,或者电脑上还没有安装 Python,请从https://www.python.org/downloads/下载 Python 3.12 并按照说明进行安装。
python3 --version
python -V
# Python 3.12.92. 创建Python虚拟环境:
- 自 Python 3.3 版本起,Python 自带了一个
venv库,用于创建轻量级的虚拟环境。
# 1. 创建虚拟环境
python -m venv my_env
# 2. 激活虚拟环境
# Linux下的激活虚拟环境
#source my_env/bin/activate
# Windows下的激活虚拟环境
.\my_env\Scripts\activate
#3. Linux下的验证激活
# (my_env) $ which python
# Windows下的验证激活
(my_env) PS> Get-Command python
# 4. 停用虚拟环境
(my_env) $ deactivate3. 安装Django
- 作为Python Web框架,Django需要Python,在安装Python同时需要安装pip。
# 在线安装Django,指定版本安装,目前5.2的最新版为5.2.9
python -m pip install django==5.2
# 默认会安装:Django==5.2.9、sqlparse==0.5.4、asgiref==3.11.0 和 tzdata==2025.3
# 可以使用pip list查看
# 检测当前是否安装Django及版本
python -m django --version
5.2.9
# 我们也可以先下载安装包:pip download django=4.2.18 -d ./
# 此项目中需要管理图片上传,因此还需要使用Pillow以下命令进行安装:
python -m pip install Pillow==11.2.1二、创建项目与构建应用
1. 创建项目 educa
在当前Python虚拟环境中运行以下命令创建 educa 项目:
$ django-admin startproject educa查看startproject自动创建项目结构如下:
educa/ # 外部 educa/ 根目录只是一个项目的容器
├── manage.py # 命令行实用程序,实现与此Django项目进行各种交互
├── educa # 内部 educa/ 目录是当前项目的实际Python包
│ ├── __init__.py # 空文件,代表Python包的标志文件
│ ├── settings.py # 此Django项目的设置/配置 文件
│ ├── urls.py # 该Django项目的URL路由声明
│ ├── asgi.py # ASGI兼容的Web服务器为您的项目提供服务的入口点
│ └── wsgi.py # WSGI兼容的Web服务器为您的项目提供服务的入口点2. 创建应用程序 courses
Django 自带一个工具,可以自动生成应用程序的基本目录结构,这样你就可以专注于写代码,而不用手动创建目录。
要创建应用程序,请确保当前目录和 manage.py 在同一层级,然后运行以下命令:
$ cd educa
$ python manage.py startapp courses- 这会创建一个 courses 目录,结构如下:
educa/
├── manage.py
├─── educa
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
└── courses # courses/目录为账户应用程序目录名称(Python包)
├── __init__.py # 空文件,代表Python包的标志文件
├── admin.py # 用于在Django管理站点中注册模型的,使用此站点是可选
├── apps.py # 包括blog应用程序的主要配置
├── migrations # 此目录将包含应用程序的数据库迁移文件(Python包)
│ └── __init__.py
├── models.py # 应用程序的数据模型,每个应用程序的必须文件(可留空)
├── tests.py # 在这里为你的应用程序添加测试
└── views.py # 应用程序的逻辑写在这里,HTTP请求、处理并返回响应。3. 启用网站Admin管理
(1). 项目配置文件的设置
打开 educa/settings.py 文件,进行如下配置:
编辑
educa/settings.py,在INSTALLED_APPS列表中添加以下加粗显示的行:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'courses.apps.coursesConfig', # 添加自己创建的courses应用
]- 默认情况下,继续保持使用SQLite数据库,无需额外设置 DATABASES :
# SQLite数据库的连接配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}- 设置时区和语言,让admin后台显示中文版界面:
...
LANGUAGE_CODE = 'zh-hans' # 语言设置
TIME_ZONE = 'Asia/Shanghai' # 时区设置
...(2) 安装 Pillow 并提供媒体文件
需要安装 Pillow 库来处理图片。Pillow 是 Python 中处理图片的标准库,支持多种图片格式,提供强大的图像处理功能。Django 的
ImageField字段需要依赖 Pillow 来处理图片。在终端中运行以下命令安装 Pillow:
python -m pip install Pillow==11.0.0- 编辑
educa/settings.py,在文件末尾添加以下配置(后面会展示完整配置):
# 媒体文件
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'MEDIA_URL<baseURL>是用户上传的媒体文件的访问 URL 前缀。MEDIA_ROOT<localPath>是媒体文件在服务器上的存储路径。为了保证可移植性,文件路径和 URL 都会动态拼接项目路径或媒体 URL 前缀。为了让 Django 开发服务器能够正常提供媒体文件,编辑
educa目录下的主urls.py文件,添加以下加粗显示的代码:
"""
项目主URL配置
将不同URL路径映射到对应的视图函数
"""
from django.conf import settings # 导入设置
from django.conf.urls.static import static # 导入静态文件处理模块
from django.contrib import admin
from django.urls import path
urlpatterns = [
# 管理后台URL
path('admin/', admin.site.urls),
]
# 静态文件处理
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
)- 注意,这种方式只适用于开发阶段。在生产环境中,绝对不能用 Django 来提供静态文件,因为 Django 开发服务器的性能不足以胜任这个工作。
(3). 数据迁移
- 运行以下命令,把 Admin 管理所需的数据表结构同步到数据库:
$ python manage.py migrate- 也可以用下面的命令查看迁移的状态:
$ python manage.py showmigrations- 具体执行效果如下:
# 执行上面命令后的输出结果
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying sessions.0001_initial... OK
# 执行下面命令查看迁移状态,具体如下:
$ python manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
[X] 0010_alter_group_name_max_length
[X] 0011_update_proxy_permissions
[X] 0012_alter_user_first_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial(4). 创建管理员用户
首先,创建一个可以登录管理后台的超级用户。运行以下命令:
$ python manage.py createsuperuser
# 输入用户名,然后按回车键。
Username: admin
# 接着输入邮箱地址:
Email address: admin@example.com
# 最后输入密码(不少于8位)。需要输入两次,第二次用于确认。
Password: **********
Password (again): *********
Superuser created successfully.(5). 启动开发服务器
默认情况下,Django 管理后台是自动启用的。下面我们启动开发服务器来看看效果。
启动开发服务器命令如下:
$ python manage.py runserver
或
$ python manage.py runserver 0.0.0.0:8000现在,打开一个Web浏览器,访问地址: http://127.0.0.1:8000/admin/
使用刚刚创建的账号密码进行登录测试:

三、添加课程学科与课程内容模型
1. 添加课程学科与课程内容模型
在线学习平台将提供涵盖多种学科的课程。每门课程分为若干模块,每个模块包含若干内容。内容类型多样,包括文本、文件、图片和视频。下面是课程的数据结构示例:
Subject 1 课程学科
Course 1 课程
Module 1 模块
Content 1 (image) 内容
Content 2 (text) 内容
Module 2
Content 3 (text)
Content 4 (file)
Content 5 (video)
...CMS 数据层级架构树
Subject ➔ Course ➔ Module ➔ Content Polymorphism
🧬
Subject信息技术 (IT)
+
📚
CourseDjango Web开发高级
+
📂
Module第七章:CMS系统
+
📝
Content (Text)本章知识点概述
🖼️
Content (Image)模型架构图.png
▶️
Content (Video)继承与多态操作演示.mp4
📦
Content (File)课后源码.zip
一个典型的在线学习平台内容管理系统(CMS)拥有深层的嵌套层级。最高层是**学科 (Subject)**,其下挂载具体的**课程 (Course)**。一门课程又被切分为多个学习**模块 (Module)**。到了最底层的叶子节点,我们需要挂载各种形态各异的学习材料:纯文本、图片、视频或是附件。这种结构要求最低层具备极强的包容性(多态性 Polymorphism),就像神经树突末端能连接不同类型的信号接收器一样。
A typical Content Management System (CMS) for an e-learning platform operates on deeply nested hierarchies. The root is the **Subject**, which branches into specific **Courses**. A Course is further segmented into learning **Modules**. At the absolute terminal leaf nodes, we must attach heterogenous learning materials: Text, Images, Videos, or Files. This data structural requirement demands extreme flexibility (Polymorphism) at the foundational layer, functioning much like nerve dendrites attaching to various types of signal receivers.
下面来构建课程模型。编辑 courses 应用的 models.py 文件,添加以下代码:
from django.contrib.auth.models import User # 导入用户模型
from django.db import models # 导入模型模块
# 课程学科模型
class Subject(models.Model):
title = models.CharField('学科名称', max_length=200)
slug = models.SlugField('学科别名', max_length=200, unique=True)
class Meta:
ordering = ['title']
verbose_name = '课程学科'
verbose_name_plural = '课程学科管理'
def **str**(self):
return self.title
# 课程模型
class Course(models.Model):
owner = models.ForeignKey(
User,
related_name='courses_created',
on_delete=models.CASCADE,
verbose_name='课程创建者'
)
subject = models.ForeignKey(
Subject,
related_name='courses',
on_delete=models.CASCADE,
verbose_name='课程学科'
)
title = models.CharField('课程标题', max_length=200)
slug = models.SlugField('课程别名', max_length=200, unique=True)
overview = models.TextField('课程概述')
created = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
ordering = ['-created']
verbose_name = '课程'
verbose_name_plural = '课程管理'
def **str**(self):
return self.title
# 模块模型(特定课程)
class Module(models.Model):
course = models.ForeignKey(
Course, related_name='modules', on_delete=models.CASCADE,
verbose_name='所属课程'
)
title = models.CharField('模块标题', max_length=200)
description = models.TextField('模块描述', blank=True)
class Meta:
verbose_name = '模块'
verbose_name_plural = '模块管理'
def **str**(self):
return self.title以上是 Subject、Course 和 Module 三个初始模型。Course 模型的字段说明如下:
owner:创建这门课程的讲师。subject:该课程所属的学科,是一个指向Subject模型的ForeignKey外键字段。title:课程名称。slug:课程别名,后续会用在 URL 中。overview:TextField类型,用于存储课程简介。created:课程创建的日期和时间,设置了auto_now_add=True,Django 会在创建新对象时自动填入当前时间。
每门课程可以包含多个模块,所以 Module 模型中有一个 ForeignKey 外键指向 Course 模型。
打开终端,运行以下命令为该应用创建初始迁移:
python manage.py makemigrations你会看到以下输出:
Migrations for 'courses':
courses/migrations/0001_initial.py:
- Create model Course
- Create model Module
- Create model Subject
- Add field subject to course然后,运行以下命令把迁移应用到数据库:
python manage.py migrate输出中会包含所有已应用的迁移信息,其中应该有这一行:
Applying courses.0001_initial... OKcourses 应用的模型已经和数据库同步了。接下来,我们把模型注册到管理后台,方便管理课程数据。
2. 在管理后台注册模型
我们需要把课程模型注册到 Django 管理后台,方便进行数据管理。编辑 courses 目录下的 admin.py 文件,添加以下代码:
from django.contrib import admin
# 导入课程类别、课程和模块模型
from .models import Course, Module, Subject
# 注册课程学科到admin管理中
@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
list_display = ['title', 'slug'] # 列表显示
prepopulated_fields = {'slug': ('title',)} # 预填充字段
# 注册课程和模块
class ModuleInline(admin.StackedInline):
model = Module
# 注册课程到admin管理中
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
list_display = ['title', 'subject', 'created'] # 列表显示
list_filter = ['created', 'subject'] # 列表过滤
search_fields = ['title', 'overview'] # 搜索字段
prepopulated_fields = {'slug': ('title',)} # 预填充字段
inlines = [ModuleInline] # 添加模块courses 应用的模型现在已经注册到管理后台了。这里用的是 @admin.register() 装饰器来完成注册。
- 编辑 courses/apps.py 文件。
from django.apps import AppConfig # 导入应用配置类
# 课程应用配置类
class CoursesConfig(AppConfig):
# 默认主键字段类型
default_auto_field = 'django.db.models.BigAutoField'
name = 'courses' # 应用名称
verbose_name = '学习平台管理' # 在Admin中显示的应用名称3. 用 fixtures(数据夹具)为模型填充初始数据
有时候我们需要预先往数据库中写入一些固定的数据,比如项目初始化时需要的默认数据。Django 提供了一个方便的机制叫做 fixtures(数据夹具),可以把数据库中的数据导出到文件,也可以从文件中导入数据到数据库。Django 支持 JSON、XML 和 YAML 格式的 fixture 文件。fixture 的数据结构和模型的 API 格式很接近,所以可以方便地在数据库和外部应用之间转换数据。接下来我们创建一个 fixture,往 Subject 模型中填入几条初始学科数据。
首先,用以下命令创建超级用户:
python manage.py createsuperuser然后启动开发服务器:
python manage.py runserver在浏览器中打开 http://127.0.0.1:8000/admin/courses/subject/,通过管理后台添加几条学科数据。添加完成后,学科列表页面应该类似下图:
图:管理后台的学科列表页面
在终端中运行以下命令:
python manage.py dumpdata courses --indent=2你会看到类似这样的输出:
[
{
"model": "courses.subject",
"pk": 1,
"fields": {
"title": "数学",
"slug": "mathematics"
}
},
{
"model": "courses.subject",
"pk": 2,
"fields": {
"title": "音乐",
"slug": "music"
}
},
{
"model": "courses.subject",
"pk": 3,
"fields": {
"title": "物理学",
"slug": "physics"
}
},
{
"model": "courses.subject",
"pk": 4,
"fields": {
"title": "编程",
"slug": "programming"
}
}
]dumpdata 命令会把数据库中的数据序列化成 JSON 格式输出。默认输出到标准输出(终端),数据结构中包含模型名称和字段信息,Django 可以根据这些信息把数据重新导入数据库。
你可以通过指定应用名称来限制导出范围,也可以用 app.Model 格式指定只导出某个模型的数据。
此外,你还可以用 --format 参数指定输出格式(默认是 JSON),用 --output 参数指定输出文件路径,用 --indent 参数指定缩进。更多参数说明可以运行 python manage.py dumpdata --help 查看。
用以下命令把导出的数据保存到 courses 应用的 fixtures/ 目录下:
mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json启动开发服务器,在管理后台中把你刚才创建的学科全部删除,如下图所示:
图:在管理后台删除所有学科数据
删除完成后,用以下命令把 fixture 数据重新导入数据库:
python manage.py loaddata subjects.jsonfixture 中的所有 Subject 对象会被重新加载到数据库中:
图:fixture 中的学科数据已重新加载到数据库
默认情况下,Django 会在每个应用的 fixtures/ 目录下查找 fixture 文件。你也可以在 loaddata 命令中指定 fixture 文件的完整路径,或者在 FIXTURE_DIRS 配置项中添加额外的 fixture 查找目录。
fixture 不仅可以用来设置初始数据,还可以为应用提供示例数据,或者为自动化测试准备测试数据,甚至可以用来填充生产环境中的必要数据。
你可以访问 https://docs.djangoproject.com/en/5.2/topics/testing/tools/#fixture-loading 了解更多关于在测试中使用 fixtures 的内容。
如果你想在数据迁移中加载 fixture,可以参考 Django 关于数据迁移的文档:https://docs.djangoproject.com/en/5.2/topics/migrations/#data-migrations。
到目前为止,我们已经创建了管理课程学科、课程和课程模块的模型。接下来要创建模型来管理不同类型的模块内容。
四、 创建多态内容模型
接下来我们要往课程模块中添加不同类型的内容,比如文本、图片、文件和视频。这里涉及到一个概念叫多态性——简单来说,就是用统一的接口来处理不同类型的数据。我们需要建立一个灵活的数据模型,能够存储各种类型的内容,并且可以通过统一的方式来访问。在之前的章节中,我们学过用通用关系(Generic Relation)创建可以指向任意模型的外键。现在我们要创建一个 Content 模型来表示模块内容,并通过通用关系把任意类型的内容对象关联起来。
编辑 courses 应用的 models.py 文件,添加以下导入语句:
# 引入通用模型
from django.contrib.contenttypes.fields import GenericForeignKey
# 引入内容类型模型
from django.contrib.contenttypes.models import ContentType然后,将以下代码添加到文件末尾:
# 内容模型,使用通用关系关联
class Content(models.Model):
module = models.ForeignKey(
Module,
related_name='contents',
on_delete=models.CASCADE,
verbose_name='所属模块'
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
limit_choices_to={
'model__in': ('text', 'video', 'image', 'file')
},
verbose_name='内容类型'
)
object_id = models.PositiveIntegerField('内容ID')
item = GenericForeignKey('content_type', 'object_id')泛型外键双拨盘虫洞
GenericForeignKey: content_type ✖ object_id = item
Content 泛型实例
1. 表拨盘 (content_type):
Text 模型
2. ID拨盘 (object_id):
ID = 1
合并属性
.item 目标锁定... 📝 Text 表
ID:1文本 "Hello"
ID:2文本 "World"
▶️ Video 表
ID:1URL "h.."
ID:2URL "v.."
传统 ForeignKey 是一条死死焊在单一数据表上的直达铁轨。而针对模块化 CMS 多态特性的需求,Django 祭出终极武器:**泛型外键(GenericForeignKey)**。它更像是一个双重坐标设定的传送台:首先旋动 content_type 拨盘锁定异构表的维度(图中文本表或视频表),接着调整 object_id 滑块框定该表内的具体记录位置。Django 极其灵性地将这两个参数缝合成了名为 item 的黑胶传送门,一瞬间打通异构关系,精确空投挂载任何不同类型的动态学习模块!
A traditional `ForeignKey` is a rigid railway tracked exclusively to a single designated table. To satisfy the polymorphic nature of structured CMS modules, Django deploys its ultimate weapon: **GenericForeignKey**. It functions like a dual-coordinate teleporter pad: First, spin the `content_type` dial to lock onto an entirely heterogeneous dimensional plane (e.g., Text Table vs Video Table). Second, slide the `object_id` toggle to pinpoint the exact internal record. Django elegantly melds these two disjointed scalars into a pseudo-attribute named `item`, instantaneously ripping open a dynamic wormhole that securely binds your `Module` to ANY radically different data structure required for learning assignments!
这就是 Content 模型。一个模块可以包含多个内容,所以这里有一个 ForeignKey 指向 Module 模型。同时还设置了一个通用关系,可以关联不同类型的内容模型。要建立通用关系,需要以下三个字段:
content_type:一个指向ContentType模型的ForeignKey外键字段object_id:PositiveIntegerField类型,存储关联对象的主键item:GenericForeignKey字段,它把前面两个字段组合起来,指向实际的关联对象
其中只有 content_type 和 object_id 两个字段会在数据库表中生成对应的列。item 字段是基于这两个字段工作的,你可以通过它直接获取或设置关联的对象。
我们会为每种类型的内容(文本、图片、视频、文件)分别创建一个模型。这些内容模型有一些共同的字段,但各自存储的具体数据不同。比如文本内容需要存储文本正文,视频内容需要存储视频 URL。为了实现代码复用,我们需要用到模型继承。在构建具体的内容模型之前,先来了解一下 Django 提供的几种模型继承方式。
1. 使用模型继承
Django 支持模型继承,它的工作方式和 Python 的标准类继承类似。如果你还不熟悉类继承,简单来说,就是定义一个新类,让它从已有的类中继承方法和属性,从而实现代码复用,简化相关类的创建。更多内容可以参考 https://docs.python.org/3/tutorial/classes.html#inheritance。
Django 提供了三种模型继承方式:
抽象模型:适合把多个模型的公共字段抽取到一个基类中
多表模型继承:父模型和子模型各自对应一张数据库表
代理模型:不创建新的数据库表,只是修改模型的行为,比如添加新方法、修改默认排序等
下面分别来看看这三种方式。
抽象模型
抽象模型是一个基类,你可以在里面定义一些公共字段,这些字段会被所有子模型继承。Django 不会为抽象模型创建数据库表,只会为每个子模型创建表,表中包含从抽象模型继承来的字段以及子模型自己定义的字段。
要把一个模型标记为抽象模型,需要在它的 Meta 类中设置 abstract=True。Django 会识别出这是抽象模型,不会为它建表。要创建子模型,直接继承这个抽象模型就行了。
下面是一个抽象模型 BaseContent 和子模型 Text 的例子:
from django.db import models
class BaseContent(models.Model):
title = models.CharField('标题', max_length=100)
created = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
abstract = True
class Text(BaseContent):
body = models.TextField()在这个例子中,Django 只会为 Text 模型创建一张表,包含 title、created 和 body 三个字段。 下图展示了这个代码示例中的模型和对应的数据库表:
图:抽象模型继承的模型和数据库表示例
抽象基类全息剥离器
Model Inheritance: Abstract Base Model (abstract=True)
BaseContent (abstract=True)
※ 不创建数据库表
title: CharFieldcreated: DateTimeField数据库物理表: courses_text
🧬
body: TextField原生字段🔮
title: CharField继承字段🔮
created: DateTimeField继承字段在**抽象基类(Abstract Model)**模式下,父类 BaseContent 在物理数据库中并不存在。它仅仅是一张悬浮在虚拟空间的“全息蓝图”。只有当具体的子模型(如 Text)需要建表时,这张蓝图才会被激活,将其所有的公用字段配置无阻力地“光束隔空投送”给子模型,最终在数据库层面凝结合并为唯一的一张坚固数据表 courses_text。
Under the **Abstract Model** inheritance pattern, the parent `BaseContent` class does not exist in the physical database at all. It acts purely as a suspended holographic blueprint in virtual space. Only when a concrete child model (like `Text`) prepares for Database migration is this blueprint activated. It seamlessly beams its shared field configurations over the air into the child model, solidifying them together into a singular, unified physical database table `courses_text`.
接下来看另一种模型继承方式——多表继承,它会为每个模型创建单独的数据库表。
多表模型继承
在多表继承中,每个模型都对应一张数据库表。Django 会自动创建一个 OneToOneField 字段来建立子模型和父模型之间的关联。使用多表继承时,直接继承一个已有的模型即可,Django 会为父模型和子模型各创建一张表。下面是一个例子:
from django.db import models
class BaseContent(models.Model):
title = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
class Text(BaseContent):
body = models.TextField()Django 会在 Text 模型中自动生成一个指向 BaseContent 模型的 OneToOneField 字段,字段名为 basecontent_ptr(其中 ptr 代表"指针")。父模型和子模型各自拥有一张数据库表。 下图展示了多表继承中的模型和对应的数据库表:
图:多表模型继承的模型和数据库表示例
多表继承物理连体婴
Model Inheritance: Multi-table Inheritance (Implied OneToOneField)
物理表 1
courses_basecontent 表
id: 8
title: "Advanced Django"
created: 2026-03-26
basecontent_ptr (OneToOneField)
💥
物理表 2
courses_text 表
basecontent_ptr_id: 8
body: "In this chapter..."
相比于极其节约空间的抽象模型,**多表继承(Multi-table Inheritance)**采用了纯物理的方法。当子模型 Text 持有一条数据时,父类信息(标题/日期)被强行存储在上面一个叫 BaseContent 的实体沉重铁箱里,而自己的特有信息(文本内容)存储在底下的 Text 铁箱里。在保存的瞬间,Django 宛如动用电焊,在两个箱子间强行焊接了一条名为 basecontent_ptr_id 的钢管(也就是一对一外键关联 OneToOneField)。虽然冗余严重,但结构极其稳定物理锁死。
In stark contrast to the space-saving Abstract Model, **Multi-table Inheritance** employs a brute-force physical approach. When a `Text` data entry is saved, its parental attributes (title/date) are securely stored in an upper heavy iron vault named `BaseContent`, while its distinct attributes (the body text) are kept in a separate lower vault named `Text`. At the moment of database committing, Django acts like a welder, forcibly searing a thick steel tube called `basecontent_ptr_id` (an implicit `OneToOneField`) between the two vaults. Though highly redundant in database tables, it forms an unbreakable, physically interlocked schema.
接下来看第三种方式——代理模型,它不创建新的数据库表,而是复用父模型的表。
代理模型
代理模型不会创建新的数据库表,它和原始模型共用同一张表。代理模型的作用是在不改变数据库结构的前提下,自定义模型的行为,比如修改默认排序、添加新方法等。要创建代理模型,在 Meta 类中设置 proxy=True 即可。下面是一个例子:
from django.db import models
from django.utils import timezone
class BaseContent(models.Model):
title = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
class OrderedContent(BaseContent):
class Meta:
proxy = True
ordering = ['created']
def created_delta(self):
return timezone.now() - self.created这里定义了一个 OrderedContent 代理模型,它继承自 BaseContent。这个代理模型设置了默认按 created 字段排序,并且新增了一个 created_delta() 方法。BaseContent 和 OrderedContent 都操作同一张数据库表,你可以通过任意一个模型来查询和访问数据。 下图展示了代理模型和对应的数据库表:
图:代理模型继承的模型和数据库表示例
代理模型 AR 变脸仪
Model Inheritance: Proxy Model (proxy=True)
物理层库表 (原始模型: BaseContent)
Item B
10:00
Item A
09:00
Item C
11:00
**代理模型(Proxy Model)**是最轻量级的继承挂载方式,通常被戏谑地称为“披着羊皮的狼”。如图所示,底层那个坚固厚重的物理数据库表(包含 A、B、C 数据)自始至终没有发生任何分裂或新增。当这堆数据戴上了 OrderedContent 这副 AR 代理面具后,它对外的表现行为立刻被重写覆盖:不仅原先杂乱的数据在视觉(QuerySet)上被强制按创建时间重排,甚至还如同外骨骼一般凭空多出了 .created_delta() 这种全新自定义函数技能!
The **Proxy Model** is the most lightweight schema-less inheritance method, often humorously likened to "a wolf in sheep's clothing". As visualized, the foundational, heavy physical database table (housing records A, B, C) neither replicates nor splits. However, equipping it with the `OrderedContent` AR proxy mask instantly overwrites its outward behavior: not only is the previously unordered data strictly resorted chronologically in visual `QuerySets`, but it also functionally gains brand-new custom methods out of thin air, like the exoskeleton-attached `.created_delta()` skill!
现在你已经了解了三种模型继承方式。更多详细信息可以参考 Django 官方文档:https://docs.djangoproject.com/en/5.2/topics/db/models/#model-inheritance。接下来我们就在项目中实际运用抽象模型继承,来创建不同类型的内容模型。
2. 创建内容模型
下面用模型继承来实现多态内容模型。具体思路是:先创建一个抽象基类,定义所有内容类型共有的字段,然后分别创建文本、图片、视频、文件四个子模型,每个子模型存储各自特有的数据。这种方式既能复用公共字段,又能灵活扩展不同类型的内容。
courses 应用中的 Content 模型使用了通用关系来关联不同类型的内容。我们需要为每种内容类型创建一个单独的模型,这些模型有一些公共字段,也有各自特有的字段。接下来创建一个抽象模型,提取出公共字段。
编辑 courses 应用的 models.py 文件,添加以下代码:
class ItemBase(models.Model):
owner = models.ForeignKey(User,
related_name='%(class)s_related',
on_delete=models.CASCADE
)
title = models.CharField(max_length=250)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
def __str__(self):
return self.title
class Text(ItemBase):
content = models.TextField()
class File(ItemBase):
file = models.FileField(upload_to='files')
class Image(ItemBase):
file = models.FileField(upload_to='images')
class Video(ItemBase):
url = models.URLField()这段代码定义了一个抽象模型 ItemBase,在它的 Meta 类中设置了 abstract=True。 这个模型包含 owner、title、created 和 updated 四个通用字段,所有内容类型都会用到。
owner 字段用来记录是哪个用户创建了该内容。由于这个字段定义在抽象类中,每个子模型需要不同的 related_name。Django 允许在 related_name 属性中使用 %(class)s 占位符,它会被替换为子模型的类名(小写)。所以四个子模型的反向关系名称分别是 text_related、file_related、image_related 和 video_related。
从 ItemBase 继承出来的四个内容模型分别是:
Text:存储文本内容File:存储文件,比如 PDF 文档Image:存储图片文件Video:存储视频 URL,用于嵌入视频播放
每个子模型除了继承 ItemBase 的公共字段外,还有各自特有的字段。Django 会分别为 Text、File、Image、Video 四个模型创建数据库表。ItemBase 是抽象模型,不会有对应的数据库表。
下图展示了 Content 模型及相关的数据库表:
图:内容模型及相关数据库表
接下来修改之前创建的 Content 模型的 content_type 字段:
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
limit_choices_to={
'model__in':('text', 'video', 'image', 'file')
}
)这里给 content_type 字段添加了 limit_choices_to 参数,限制通用关系只能关联 model 属性为 'text'、'video'、'image' 或 'file' 的 ContentType 对象。 接下来创建迁移。在终端运行以下命令:
python manage.py makemigrations你会看到以下输出:
Migrations for 'courses':
courses/migrations/0002_video_text_image_file_content.py
- Create model Video
- Create model Text
- Create model Image
- Create model File
- Create model Content然后运行以下命令应用迁移:
python manage.py migrate输出结果应该以这一行结尾:
Applying courses.0002_video_text_image_file_content... OK现在模型已经可以往课程模块中添加各种类型的内容了。不过还差一个功能:课程模块和内容需要支持排序,方便按特定顺序展示。我们需要添加一个排序字段。
3. 创建自定义模型字段
Django 提供了丰富的内置模型字段,但你也可以创建自己的模型字段,用来存储自定义数据类型、实现自定义验证、封装复杂的字段逻辑,或者定义特殊的表单渲染方式。
我们需要一个字段来定义对象的排序。最简单的做法是在模型中加一个 PositiveIntegerField 类型的 order 字段,用整数来表示排序。更进一步,我们可以创建一个自定义的 OrderField,继承自 PositiveIntegerField,并添加一些额外的功能。
这个排序字段需要实现两个核心功能:
自动分配排序值:保存新对象时,如果没有手动指定排序值,字段会自动取当前最大排序值加 1。比如已有两个对象的排序值分别是
1和2,新对象会自动分配排序值3。按关联字段分组排序:课程模块按所属课程分组排序,模块内容按所属模块分组排序。
在 courses 应用目录下创建一个新文件 fields.py,添加以下代码:
# 定义顺序字段
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
# 模型
class OrderField(models.PositiveIntegerField):
# 初始化方法
def __init__(self, for_fields=None, *args, **kwargs):
self.for_fields = for_fields # 依据字段
super().__init__(*args, **kwargs)
# 预保存方法
def pre_save(self, model_instance, add):
if getattr(model_instance, self.attname) is None:
# 当前值为空,需要设置
try:
qs = self.model.objects.all() # 获取所有对象
if self.for_fields:
# 根据具有相同字段值的对象进行过滤
# 针对"for_fields"中的字段
query = {
field: getattr(model_instance, field)
for field in self.for_fields
}
qs = qs.filter(**query)
# 获取最后一个项目的顺序值
last_item = qs.latest(self.attname)
value = getattr(last_item, self.attname) + 1
except ObjectDoesNotExist:
value = 0
# 设定当前实例的顺序值
setattr(model_instance, self.attname, value)
return value # 返回值
else:
# 没有指定for_fields,使用默认行为
return super().pre_save(model_instance, add)这就是自定义的 OrderField 字段,它继承自 Django 内置的 PositiveIntegerField。它接受一个可选的 for_fields 参数,用来指定按哪些字段分组计算排序值。
这个字段重写了 PositiveIntegerField 的 pre_save() 方法,该方法会在字段值保存到数据库之前执行。具体逻辑如下:
首先检查当前模型实例中该字段是否已有值(通过
self.attname获取字段在模型中的属性名)。如果值为None,说明需要自动计算排序值:构建一个 QuerySet,获取该字段所属模型的所有对象(通过
self.model访问模型类)。如果
for_fields中指定了分组字段,就按这些字段的当前值进行过滤,这样排序值就只在同一分组内计算。用
qs.latest(self.attname)获取当前最大排序值的对象。如果没有找到任何对象,说明这是该分组的第一个对象,排序值设为0。如果找到了对象,就在最大排序值基础上加
1。用
setattr()把计算出的排序值赋给模型实例,并返回。
如果字段已经有值了,就直接使用该值,不做计算。
创建自定义模型字段时,要保持字段的通用性,不要把依赖特定模型或字段的逻辑硬编码进去,确保字段可以用在任何模型上。
更多关于自定义模型字段的内容可以参考:https://docs.djangoproject.com/en/5.2/howto/custom-model-fields/。
接下来,把我们创建的自定义字段用起来。
3. 为模块和内容对象添加排序功能
把新字段添加到模型中。编辑 courses 应用的 models.py 文件,导入 OrderField 并添加到 Module 模型中:
from .fields import OrderField
class Module(models.Model):
# ...
order = OrderField(blank=True, for_fields=['course'])新字段命名为 order,通过 for_fields=['course'] 指定按课程分组计算排序值。也就是说,新模块的排序值会在同一课程的已有模块中取最大值加 1。 接下来修改 Module 模型的 __str__() 方法,把排序值显示出来:
class Module(models.Model):
# ...
def __str__(self):
return f'{self.order}. {self.title}'模块内容也需要排序,给 Content 模型也添加 OrderField 字段:
class Content(models.Model):
# ...
order = OrderField(blank=True, for_fields=['module'])这里指定排序值按 module 字段分组计算。 最后,给两个模型都添加默认排序。在 Module 和 Content 模型中添加 Meta 类:
class Module(models.Model):
# ...
class Meta:
ordering = ['order']
class Content(models.Model):
# ...
class Meta:
ordering = ['order']修改完成后,Module 和 Content 模型的完整代码如下:
class Module(models.Model):
course = models.ForeignKey(
Course, related_name='modules', on_delete=models.CASCADE
)
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
order = OrderField(blank=True, for_fields=['course'])
class Meta:
ordering = ['order']
def __str__(self):
return f'{self.order}. {self.title}'
class Content(models.Model):
module = models.ForeignKey(
Module,
related_name='contents',
on_delete=models.CASCADE
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
limit_choices_to={
'model__in':('text', 'video', 'image', 'file')
}
)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
order = OrderField(blank=True, for_fields=['module'])
class Meta:
ordering = ['order']- 完整 courses/models.py 文件代码:
#
from django.contrib.auth.models import User # 导入用户模型
from django.contrib.contenttypes.fields import GenericForeignKey # 引入通用模型
from django.contrib.contenttypes.models import ContentType # 引入内容类型模型
from django.db import models # 导入模型模块
from .fields import OrderField # 引入自定义字段
# 课程学科模型
class Subject(models.Model):
title = models.CharField('学科名称', max_length=200)
slug = models.SlugField('学科别名', max_length=200, unique=True)
class Meta:
ordering = ['title']
verbose_name = '课程学科'
verbose_name_plural = '课程学科管理'
def __str__(self):
return self.title
# 课程模型
class Course(models.Model):
owner = models.ForeignKey(
User,
related_name='courses_created',
on_delete=models.CASCADE,
verbose_name='课程创建者'
)
subject = models.ForeignKey(
Subject,
related_name='courses',
on_delete=models.CASCADE,
verbose_name='课程学科'
)
title = models.CharField('课程标题', max_length=200)
slug = models.SlugField('课程别名', max_length=200, unique=True)
overview = models.TextField('课程概述')
created = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
ordering = ['-created']
verbose_name = '课程'
verbose_name_plural = '课程管理'
def __str__(self):
return self.title
# 模块模型(特定课程)
class Module(models.Model):
course = models.ForeignKey(
Course, related_name='modules', on_delete=models.CASCADE, verbose_name='所属课程'
)
title = models.CharField('模块标题', max_length=200)
description = models.TextField('模块描述', blank=True)
order = OrderField(blank=True, for_fields=['course']) # 顺序字段
class Meta:
ordering = ['order']
verbose_name = '模块'
verbose_name_plural = '模块管理'
def __str__(self):
return f'{self.order}. {self.title}'
# 内容模型,使用通用关系关联
class Content(models.Model):
module = models.ForeignKey(
Module,
related_name='contents',
on_delete=models.CASCADE,
verbose_name='所属模块'
)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
limit_choices_to={
'model__in': ('text', 'video', 'image', 'file')
},
verbose_name='内容类型'
)
object_id = models.PositiveIntegerField('内容ID')
item = GenericForeignKey('content_type', 'object_id')
order = OrderField(blank=True, for_fields=['module'])
class Meta:
ordering = ['order']
verbose_name = '内容'
verbose_name_plural = '内容管理'
class ItemBase(models.Model):
owner = models.ForeignKey(
User,
related_name='%(class)s_related',
on_delete=models.CASCADE,
verbose_name='作者'
)
title = models.CharField('标题', max_length=250)
created = models.DateTimeField('创建时间', auto_now_add=True)
updated = models.DateTimeField('更新时间', auto_now=True)
class Meta:
abstract = True # 抽象基类(不创建数据库表)
def __str__(self):
return self.title
# 文本内容
class Text(ItemBase):
content = models.TextField('文本内容')
# 文件内容
class File(ItemBase):
file = models.FileField('文件内容', upload_to='files')
# 图片内容
class Image(ItemBase):
file = models.FileField('图片内容', upload_to='images')
# 视频内容
class Video(ItemBase):
url = models.URLField('视频地址')接下来创建迁移,把新的 order 字段同步到数据库。打开终端运行以下命令:
python manage.py makemigrations courses你会看到以下输出:
It is impossible to add a non-nullable field 'order' to content without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option:Django 提示你需要为已有数据行的新 order 字段提供一个默认值。如果字段设置了 null=True,Django 会允许空值,自动创建迁移而不要求默认值。你可以在这里指定默认值,也可以取消迁移、先在 models.py 中给 order 字段加上 default 属性再重新创建迁移。
输入 1 并按回车,为已有数据提供默认值。你会看到以下提示:
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>>输入 0 作为默认值并按回车。Django 还会要求你为 Module 模型做同样的操作,同样选择第一个选项并输入 0。最后你会看到类似这样的输出:
Migrations for 'courses':
courses/migrations/0003_alter_content_options_alter_module_options_and_more.py
- Change Meta options on content
- Change Meta options on module
- Add field order to content
- Add field order to module然后运行以下命令应用迁移:
python manage.py migrate输出会显示迁移已成功应用:
Applying courses.0003_alter_content_options_alter_module_options_and_more... OK来测试一下新字段。用以下命令打开 Django shell:
python manage.py shell先创建一门课程:
>>> from django.contrib.auth.models import User
>>> from courses.models import Subject, Course, Module
>>> user = User.objects.last()
>>> subject = Subject.objects.last()
>>> c1 = Course.objects.create(subject=subject, owner=user, title='Course 1', slug='course1')课程创建好了。现在给课程添加模块,看看排序值是怎么自动计算的。先创建第一个模块:
>>> m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0OrderField 把值设为了 0,因为这是该课程的第一个模块。再创建第二个模块:
>>> m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1OrderField 自动计算出下一个排序值为 1。接下来创建第三个模块,这次手动指定排序值:
>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5如果创建对象时手动指定了排序值,OrderField 会直接使用你指定的值,不再自动计算。 再添加第四个模块:
>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6这个模块的排序值被自动设为了 6。OrderField 不保证排序值是连续的,但它会根据当前最大排序值来分配下一个值。
接下来创建第二门课程并添加一个模块:
>>> c2 = Course.objects.create(subject=subject, title='Course 2', slug='course2', owner=user)
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0计算新模块的排序值时,OrderField 只考虑同一课程下的模块。由于这是第二门课程的第一个模块,排序值从 0 开始。这就是因为我们在 Module 模型的 order 字段中设置了 for_fields=['course']。
自增排序域扫描游标尺
Custom OrderField: Pre-save Aggregation (.latest() within for_fields)
Course=201 轨道区域Module 1
order: 0
Module 2
order: 1
Course=202 轨道区域Module X
order: 0
当这套名为 OrderField 的智能游标扫描方案在 pre_save 拦截阶段生效时,场面极度舒适:系统并非全局盲目自增。第一步,先依靠定义的 for_fields=['course'] 从浩如烟海的数据库轨道中锁定仅仅属于当前课程的封闭区域。第二步,放出数据库游标探针执行 qs.latest(self.attname),精准揪出刚才轨道上的老末(它的顺位值是 1)。最后,机器臂进行简单的 +1 加法运算,在落盘前一棒子死死将新元素印上 “2” 的编号,自动并排入队口中。完美兼顾了分组隔离机制与防跨界打架的自动补齐!
When this intelligent cursor scanning strategy named `OrderField` activates during the pre_save interception phase, the process runs immaculately: the system avoids blindly incrementing globally. Step 1: It isolates the exact closed-off database track associated entirely with the current course, relying on the `for_fields=['course']` definition to prevent contamination. Step 2: It deploys a database cursor probe executing `qs.latest(self.attname)` to precisely snipe the trailing end-element of that track (finding its position value is 1). Lastly, the automated machinery computes simple addition (+1) and stamps the shiny "2" designation directly onto the new element before disk commit, cleanly racking it into sequence. Complete modular automation balancing group isolation and frictionless gap-filling!
到这里,你已经成功创建了第一个自定义模型字段。接下来将为内容管理系统(CMS)创建身份验证系统。
