Django-基本入门
这篇讲什么
- 这篇把第一条链路完整跑通:装 Django、建项目、配数据库、建应用、定义模型、迁移出表、写视图和模板、用路由把页面打开。
- 所有命令和输出都来自本机 Django 5.2.19 LTS + Python 3.14.10 + MySQL 9.7.4,命名一律按 PEP8(应用叫
myapp,字段叫boy_num)。
Django简介
-
是一个开放源代码的 Web 应用框架,由 Python 写成。它自带 ORM、后台管理、模板引擎、表单、认证、缓存、迁移这些「电池全含」的组件,做后台系统时基本不用再挑第三方库。
-
历史:2005 年 7 月以 BSD 许可首次公开发布,2008 年 9 月发布第一个正式版本 1.0。本套笔记的当前基线是 Django 5.2.19 LTS(延长支持到 2028-04),版本选择的实测依据见《Django 5.2 LTS 与 6.x 的区别对比》。
-
一句话理解它在干什么:收到一个 URL → 路由表找到视图 → 视图查模型、渲染模板 → 把响应发回去。这篇就按这条线把每个环节跑通一遍。
-
命名一律按 PEP8 写:模块名、包名(应用名)、函数、变量、模型字段都用小写下划线,类名用大驼峰。所以本笔记里应用叫
myapp(不是老笔记的myApp),字段叫boy_num、is_delete(不是boyNum、isDelete)。理由很实际:字段名一旦建表就很难改,而且python -m pylint、ruff这类检查器默认都会报驼峰字段。 -
MVC
- 概述
一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC 被独特地发展起来,用于把传统的输入、处理和输出功能映射到一个图形化用户界面的逻辑结构中。 - 核心思想
解耦:界面改了不用动业务逻辑,换数据库也不用动视图代码。 - 图解

- 编程模式
- Model(模型):
应用程序中处理数据逻辑的部分,通常负责在数据库中存取数据。 - View(视图):
应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。 - Controller(控制器):
应用程序中处理用户交互的部分,负责从视图读取数据、控制用户输入,并向模型发送数据。
- Model(模型):
- 优点
降低各功能模块之间的耦合性,方便变更,更容易重构代码,最大程度地实现代码重用。
- 概述
-
MTV
-
概述
本质上与 MVC 没有差别,也是各组件之间为了保持松耦合关系,只是定义上有些许不同。 -
核心思想
还是解耦,但 Django 把「控制器」这个角色拆给了框架自己(路由分发 + 请求对象),所以你的代码里只有 M、T、V 三件要写。 -
图解

-
编程模式与实测对应关系
MTV 在 Django 里是哪个文件 负责什么 本笔记实测里的例子 Model myapp/models.py业务对象与数据库的映射(ORM) Grade、Student两个类Template templates/myapp/*.html把页面展示给用户 students.html、grades.htmlView myapp/views.py业务逻辑,适当时候调用 Model 和 Template index、students、grades、grade_students(分发器) urls.py把 URL 请求分派给不同视图 path("students/<int:gid>/", ...) -
注意
- Django 还有一个 URL 分发器,它的作用是把一个个 URL 的页面请求分发给不同的 view 处理,view 再调用相应的 Model 和 Template。
- Django 的 View 比 MVC 的 View 更「厚」:它同时承担了 MVC 里 Controller 的活(读请求、调模型、选模板),所以新手最容易犯的错是把业务逻辑全写进模板里。
-
Django安装
-
Django 与 Python 版本对应关系
-
官方 FAQ 的支持矩阵(2026-11-04 抓的),加上本机
importlib.metadata读到的Requires-Python实测值:Django 版本 官方支持的 Python 本机实测 Requires-Python4.2 LTS 3.8、3.9、3.10、3.11、3.12(4.2.8 起) — 5.0 3.10、3.11、3.12 — 5.1 3.10、3.11、3.12、3.13(5.1.3 起) — 5.2 LTS(本笔记基线) 3.10、3.11、3.12、3.13、3.14(5.2.8 起) >=3.106.0 3.12、3.13、3.14 — 6.1 3.12、3.13、3.14、3.15(6.1.3 起) >=3.12 -
老笔记里那张 1.8 / 1.9 / 2.0 / 2.1 的表已经全部停止支持(连安全修复都没有了),照着装会踩到 Python 版本对不上的问题,所以这里换成现行矩阵。
-
-
安装过程
-
建议给每个项目单独建环境,不要都塞进 base:
1
2
3conda create -n django python=3.14 -y
conda activate django
pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple Django==5.2.19 -
验证是否安装成功(进 Python 环境,实测输出):
1
2
3
4
5>>> import django
>>> django.get_version()
'5.2.19'
>>> django.VERSION
(5, 2, 19, 'final', 0)get_version()返回给人看的字符串,VERSION是给代码写判断用的五元组,两者都可用。 -
命令行验证也可以:
1
django-admin --version # 5.2.19
-
Django项目创建
-
建工程
-
打开终端,进入工程存放目录,然后:
1
django-admin startproject project
-
也可以指定建在哪(
startproject的第二个参数是目标目录,目录要先存在):1
django-admin startproject project E:\Desktop\Test\django-basic-probe
-
-
目录层级(实测
startproject+startapp myapp后的完整文件清单)1
2
3
4
5
6
7
8
9
10
11
12
13manage.py
project/__init__.py
project/asgi.py
project/settings.py
project/urls.py
project/wsgi.py
myapp/__init__.py
myapp/admin.py
myapp/apps.py
myapp/models.py
myapp/tests.py
myapp/views.py
myapp/migrations/__init__.pymanage.py:一个命令行工具,使我们能用多种方式与 Django 项目交互。project/__init__.py:空文件,告诉 Python 这个目录是一个包。project/settings.py:项目的配置文件。project/urls.py:项目的 URL 声明(根路由表)。project/wsgi.py:项目与 WSGI 兼容的 Web 服务器入口(同步部署用,如 Gunicorn)。project/asgi.py:ASGI 入口(异步部署用,如 uvicorn;老笔记里没有这个文件,是 Django 3.0 之后才加的)。myapp/apps.py:应用配置类,需要「应用启动时做点事」时在这里写ready()。myapp/migrations/__init__.py:迁移文件目录,没有它makemigrations会报错。
-
settings.py里几个关键行(实测生成内容)settings.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# project/settings.py 实测生成内容(节选)
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
ALLOWED_HOSTS = []
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
...
},
]
USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"BASE_DIR是pathlib.Path对象,所以官方注释里推荐BASE_DIR / "templates"这种写法;老笔记里的os.path.join(BASE_DIR, "templates")也能用,但要先把os导进来。DEFAULT_AUTO_FIELD这一行新项目里默认就是BigAutoField(实测),它决定了所有没写主键的模型自增列的类型,见下面「定义模型」。- 一个坑:生成文件里的引号风格由本机装没装
black决定(startproject末尾会调run_formatters()),装了就是双引号、没装就是模板里的单引号。写脚本批量改 settings 时,替换完一定要断言期望内容真的进了文件。 - 这一节只过了「生成文件里能看到的那几行」。想知道每个键的默认值、改了为什么不生效、
diffsettings怎么读,看《Django-settings详解》——146 个键的默认值是本机global_settings现读的。
数据库配置
-
默认用 SQLite
-
Django 默认使用 SQLite,
settings.py里DATABASES的默认值就是:settings.py 1
2
3
4
5
6DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
} -
学习阶段用 SQLite 最省事:不用装数据库、不用起服务,工程目录下会生成一个
db.sqlite3文件。
-
-
换成 MySQL(官方推荐 mysqlclient,本机实测)
-
官方推荐的驱动是
mysqlclient(C 扩展,导入名是MySQLdb)。PyPI 上有对应 Python 版本的预编译 wheel,本机不需要编译环境:1
2pip install --only-binary :all: mysqlclient
Successfully installed mysqlclient-2.3.0-
--only-binary :all:的作用是「没有 wheel 就立刻失败」,避免它悄悄退回源码编译然后卡一晚上。 -
装完直接
import MySQLdb就能用,不需要pymysql.install_as_MySQLdb()那种伪装:1
2MySQLdb.version_info: (2, 3, 0, 'final', 0)
MySQLdb.get_client_info(): 3.4.93.4.9是 Windows wheel 里自带的 MariaDB Connector/C 客户端库版本,不是 MySQL 版本;用它连本机的 MySQL 9.7.4 服务端实测正常。
-
-
配置
DATABASES(password走环境变量,不要把明文写进代码):settings.py 1
2
3
4
5
6
7
8
9
10
11
12
13import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "django_note_mcli", # 数据库名,要先建好
"USER": "root",
"PASSWORD": os.environ["DJANGO_NOTE_MCLI"],
"HOST": "127.0.0.1",
"PORT": "3306",
"OPTIONS": {"charset": "utf8mb4"},
}
}OPTIONS里的charset决定连接字符集,实测建出来的表列排序规则是utf8mb4_0900_ai_ci。 -
本机实测(WSL 里的 MySQL 9.7.4 测试库,Windows 侧填
127.0.0.1就能连上):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15check rc=0 System check identified no issues (0 silenced).
makemigrations rc=0
+ Create model Grade
+ Create model Student
migrate rc=0
Applying myapp.0001_initial... OK
vendor: mysql
mysql_version: (9, 7, 4)
mysql_is_mariadb: False
客户端库: 3.4.9
@@sql_mode: ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
读回: mcli班级 1 1
超长写入: DataError (1406, "Data too long for column 'name' at row 1")
回滚后存在吗: False
参数化查询: (1,)-
sqlmigrate能看到 Django 给 MySQL 生成的 DDL:主键是bigint AUTO_INCREMENT,布尔列写成bool,外键单独一条ALTER TABLE ... ADD CONSTRAINT:1
2
3CREATE TABLE `myapp_grade` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(20) NOT NULL, `boy_num` integer NOT NULL, `girl_num` integer NOT NULL, `is_delete` bool NOT NULL);
CREATE TABLE `myapp_student` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(20) NOT NULL, `sex` bool NOT NULL, `age` integer NOT NULL, `is_delete` bool NOT NULL, `grade_id` bigint NOT NULL);
ALTER TABLE `myapp_student` ADD CONSTRAINT `myapp_student_grade_id_c33d0adc_fk_myapp_grade_id` FOREIGN KEY (`grade_id`) REFERENCES `myapp_grade` (`id`); -
判断严格模式有个坑:
connection.sql_mode返回的是集合({'STRICT_TRANS_TABLES', ...}),写"STRICT" in connection.sql_mode永远是False;要么用完整名字判断,要么直接SELECT @@sql_mode。
-
-
老笔记那条纯 Python 路线(
PyMySQL+ 伪装)也实测能跑,属于兼容层,不是官方推荐:myapp/__init__.py 1
2
3import pymysql
pymysql.install_as_MySQLdb()- 本机的实测数字:
pip元数据PyMySQL 1.2.3、pymysql.__version__ 2.2.8、 伪装后MySQLdb.version_info (2, 2, 8, 'final', 1),正好过了 Django 对mysqlclient版本下限的检查,所以check不抱怨。 - 新项目直接用
mysqlclient,别把伪装写进__init__.py;只有在装不了 C 扩展的环境(某些纯托管镜像、共享主机)才退回 PyMySQL。
- 本机的实测数字:
-
-
换成 PostgreSQL(WSL2 Ubuntu 里装,本机实测)
-
安装(Ubuntu 26.04 源里的当前大版本就是 18):
1
2
3sudo apt-get install -y postgresql postgresql-contrib
systemctl is-enabled postgresql
systemctl is-active postgresql@18-main- 实测装到
postgresql-18 (18.6-0ubuntu0.26.04.1),服务enabled且postgresql@18-main是active。 - PostgreSQL 没有单独的 LTS 线:每个大版本发布后支持 5 年,所以「要最新的长期支持版」在 Ubuntu 里就等于「源里最新的大版本」。
- 实测装到
-
建角色和库(在 WSL 里以
postgres用户执行):1
2CREATE ROLE django LOGIN PASSWORD 'password';
CREATE DATABASE django_note OWNER django;- 实测
SELECT rolname, rolcanlogin FROM pg_roles WHERE rolname = 'django'得到django | t,\l里django_note的编码是UTF8。 - 这里的
password是占位符,真项目换成随机密码并放环境变量,不要提交进仓库。
- 实测
-
让 Windows 侧也能连(WSL2 的坑,实测):
/etc/postgresql/18/main/postgresql.conf 1
2listen_addresses = '*'
port = 5600 # 默认是 5432,本机必须挪走,原因见下一条1
2# /etc/postgresql/18/main/pg_hba.conf 末尾追加
host all all 0.0.0.0/0 scram-sha-256- 默认只监听
localhost,所以第一步就是listen_addresses = '*'并重启服务。 - 端口必须从 5432 挪走:
127.0.0.1:5432在 Windows 侧压根没人听(netstat -ano | findstr :5432空),因为 5432 落在netsh int ipv4 show excludedportrange protocol=tcp列出的保留区间5384-5483里,负责转发的wslrelay.exe绑不上这个端口。实测wsl --shutdown重启整个 WSL 也没用,换到区间外的5600立刻就能被代收。 - 不想改端口的话只有两条退路:主机填 WSL 自己的 IP(
wsl hostname -I拿到192.168.104.119,实测能连通,但这个 IP 每次重启可能变),或者管理员加一条netsh interface portproxy(普通权限直接报「请求的操作需要提升」)。 - 细节与 Navicat 的完整填法见《用 Navicat 连 WSL2 里的数据库》的 PostgreSQL 一节。
- 默认只监听
-
装驱动:Django 4.2 起 PostgreSQL 后端只认 psycopg 3(
psycopg2不再支持),本机装带二进制包的 extra:1
2pip install --only-binary :all: "psycopg[binary]"
Successfully installed psycopg-3.3.8 psycopg-binary-3.3.8 -
配置(同样
password走环境变量):settings.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "django_note",
"USER": "django",
"PASSWORD": os.environ["DJANGO_NOTE_PG_PW"],
"HOST": "127.0.0.1",
"PORT": "5600", # 本机把 PostgreSQL 从 5432 挪到了 5600;没挪就是 5432
"OPTIONS": {"connect_timeout": 8},
"CONN_MAX_AGE": 60,
}
}OPTIONS里的connect_timeout强烈建议加:连不上时 8 秒就报错。本机踩过「HOST 填了127.0.0.1、端口还是 5432,manage.py直接挂住」的坑,就是因为没有超时(实测等满 3 秒超时才抛ConnectionTimeout)。CONN_MAX_AGE = 60是连接复用(持久连接),60 秒内不重连。
-
本机实测:
1
2
3
4
5
6
7
8
9
10
11
12check rc=0 System check identified no issues (0 silenced).
migrate rc=0
Applying blog.0001_initial... OK
vendor: postgresql
psycopg: 3.3.8
has psycopg2: False
SQL version: PostgreSQL 18.6 (Ubuntu 18.6-0ubuntu0.26.04.1) | num: 180006
读回: pg文章 {'k': [1, 2]} ['django', 'pg'] 1
JSONField 查询: 1
ArrayField 查询: 1
超长写入: DataError value too long for type character varying(50)
回滚后存在吗: False-
sqlmigrate生成的 DDL 和 MySQL 明显不同:主键是bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,JSONField落成jsonb,ArrayField落成varchar(20)[],unique=True的CharField还会额外建一个varchar_pattern_ops索引:1
2CREATE TABLE "blog_article" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "title" varchar(50) NOT NULL UNIQUE, "meta" jsonb NOT NULL, "keywords" varchar(20)[] NOT NULL, "views" integer NOT NULL);
CREATE INDEX "blog_article_title_3c514952_like" ON "blog_article" ("title" varchar_pattern_ops); -
django.contrib.postgres里的特性只在 PostgreSQL 上有效。最直观的一条:admin 的search_fields用@前缀时走全文检索,实测生成的 SQL 是:1
WHERE (UPPER("blog_article"."title"::text) = UPPER('pg文章') OR to_tsvector(COALESCE("blog_article"."title", '')) @@ (plainto_tsquery('pg文章')))
同样的配置放到 MySQL 上,实测直接
django.core.exceptions.FieldError: Unsupported lookup 'search' for CharField or join on the field not permitted.
-
-
后面的文章(模型、视图、站点管理等)默认都走 MySQL 主路径;只有需要
ArrayField、全文检索这类 PostgreSQL 特性时才换过来。
-
-
一句提醒
上面
127.0.0.1:3306、192.168.104.119:5432都是本机的测试库,只给「写文章时逐条跑一遍」用。线上生产库的连接信息不要写进笔记,也不要因为本机连不上就去改它。
创建应用与激活应用
-
创建应用
-
一个工程可以有一个或多个应用,每个应用对应处理一类业务逻辑。终端进入工程目录后:
1
python manage.py startapp myapp
-
应用名就是 Python 包名,按 PEP8 全小写(
myapp、student_manage这种)。老笔记用的myApp也能跑,但包名带大写会在大小写敏感的系统(Linux 服务器、Docker 卷)上埋雷,而且ruff/pylint会直接报invalid-name。
-
-
激活应用
-
配置
settings.py中的INSTALLED_APPS(注意拼写,是 installed):settings.py 1
2
3
4
5
6
7
8
9INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"myapp",
] -
老笔记这里写的是
INSTALL_APPS。实测两种错法的表现完全不同:错法 实测结果 说明 键名写成 INSTALL_APPSpython manage.py check失败:LookupError: No installed app with label 'admin'.;apps.get_app_configs()返回[](myapp 在里面吗: False)框架读的是 INSTALLED_APPS,你写的那个键没人看,等于用了默认的空列表键名对、但应用名写错( "myappTYPO")ModuleNotFoundError: No module named 'myappTYPO'这条会立刻炸,反而好查 - 结论:settings 里的键名是按名字被读取的,写错名字不会有任何提示,所以改完 settings 就顺手跑一次
python manage.py check。
- 结论:settings 里的键名是按名字被读取的,写错名字不会有任何提示,所以改完 settings 就顺手跑一次
-
定义模型与生成数据表
-
有一个数据表,就对应有一个模型类
-
先按老笔记的表结构图理解要存什么(班级表 + 学生表,一对多):

-
在应用目录下的
models.py中定义模型(字段名全部按 PEP8 下划线写):myapp/models.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25from django.db import models
class Grade(models.Model):
"""班级表"""
name = models.CharField(max_length=20)
boy_num = models.IntegerField()
girl_num = models.IntegerField()
is_delete = models.BooleanField(default=False)
def __str__(self):
return self.name
class Student(models.Model):
"""学生表,grade 外键指向 Grade"""
name = models.CharField(max_length=20)
sex = models.BooleanField()
content = models.CharField(max_length=20)
age = models.IntegerField()
grade = models.ForeignKey(Grade, on_delete=models.CASCADE, related_name="students")
is_delete = models.BooleanField(default=False)
def __str__(self):
return self.name -
老笔记这里写的是
boyNum、girlNum、isDelete;上面换成boy_num、girl_num、is_delete。字段名一旦建表就很难改(改名要么写db_column过渡,要么做「加新列-迁数据-删旧列」三步迁移),所以一开始就按规范命名最省事。 -
on_delete=models.CASCADE是必填的:不写会直接TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete'(实测)。 -
related_name="students"让反向查询写成grade.students.all();不写的话 Django 按「小写模型名 +_set」生成,即grade.student_set.all()(前一轮实测过默认名就是student_set)。
-
-
不需要定义主键
-
老笔记说「不需要定义主键,生成时自动添加并且值为自动增长」,这句对,但要说清是哪一种自增主键。实测:
1
python manage.py shell -c "from myapp.models import Student; f = Student._meta.pk; print(f.name, type(f).__name__, f.get_internal_type())"
1
2主键: id BigAutoField BigAutoField
字段: ['id', 'name', 'sex', 'content', 'age', 'grade', 'is_delete'] -
因为新项目
settings.py里默认写了DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField",所以自动主键是 64 位的BigAutoField。 -
两个实测细节:Django 5.2 的全局默认(settings 里完全不管这个设置时)是
AutoField,32 位;而把生成的那一行删掉后check并不报错、主键也仍是BigAutoField,因为已生成的0001_initial.py里把字段类型写死了。也就是说DEFAULT_AUTO_FIELD只管以后新建的模型,改它不会改动已有表。
-
-
生成数据表
-
生成迁移文件:
1
python manage.py makemigrations
1
2
3
4Migrations for 'myapp':
myapp\migrations\0001_initial.py
- Create model Grade
- Create model Student- 迁移文件生成在应用的
migrations目录下,它描述的是「怎么改表」,不是表本身。
- 迁移文件生成在应用的
-
执行迁移(在数据库中建表):
1
python manage.py migrate
1
2
3
4
5Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...
Applying myapp.0001_initial... OK
Applying sessions.0001_initial... OK -
任何时候想确认工程没问题:
1
python manage.py check
1
System check identified no issues (0 silenced).
-
数据测试:shell 里的增删改查
-
进入 shell 并引入模型
1
python manage.py shell
1
from myapp.models import Student, Grade
manage.py shell与直接敲python的区别是:它已经把settings装好、数据库连接也配好了,所以模型可以直接用。 -
增
1
2
3
4
5
6
7
8
9
10
11
12
13
14grade = Grade()
grade.name = "zzpython1801"
grade.boy_num = 40
grade.girl_num = 10
grade.save()
sut = Student()
sut.name = "sunck"
sut.sex = True
sut.content = "sunck is a good man"
sut.age = 18
sut.grade = grade
sut.save()
sut.pk, sut.grade_id
(1, 1)-
实测这一串跑完的输出:
pk= 1 grade_id= 1 反向= 1(最后一项是grade.students.count())。 -
更省事的写法是一次性
create:1
2
3g = Grade.objects.create(name="zzpython1802", boy_num=41, girl_num=11)
g.pk, str(g)
(2, 'zzpython1802')str(g)之所以是班级名而不是Grade object (2),是因为模型里定义了__str__;不定义就会显示Grade object (2)(前一轮实测),模板里{{ stu.grade }}也会跟着变难看。 -
老笔记这里把字段名写成了
sut.contend,这是个典型的静默丢数据。实测两种写法的结果完全不同:写法 实测结果 属性赋值 sut.contend = "..."然后save()save成功,但库里content存的是空字符串''(contend 是字段吗: False)—— 数据静默丢了构造参数 Student(contend="...")当场 TypeError: Student() got unexpected keyword arguments: 'contend'所以:给模型实例赋一个不存在的属性,Python 不会拦你,Django 也不会拦,只有查库才发现。写的时候用表单/序列化器保存,或者调
full_clean()自检,比肉眼可靠。
-
-
查
1
2
3Student.objects.all() # 全部,返回查询集
Student.objects.get(pk=1) # 一条,返回模型对象
Student.objects.filter(grade_id=1) # 多条,返回查询集-
实测三种等价取法的结果一致:
filter(grade_id=g.id).count()=filter(grade=g).count()=g.students.count(),都返回2。 -
get()查不到时抛的是模型自己的异常,不是返回None(实测):1
2DoesNotExist : Student matching query does not exist.
first() 拿不到时: None所以要么
filter(...).first(),要么try / except Student.DoesNotExist。
-
-
删
1
2stu = Student.objects.get(pk=1)
stu.delete()-
老笔记说「仅仅删除数据库中对应数据,程序中的对象依然可以使用」,这句在现在的 Django 上只对了一半,实测:
1
删前 pk= 1 删后对象 pk= None 库里条数= 1
对象确实还能访问属性,但
pk被置成了None(Django 3.0 起的行为),所以别指望继续拿它当「刚删掉那条」用。 -
老笔记还说「
stu.save()可以恢复数据」,实测恢复不出原来那条:1
再 save 之后: 原 pk 还在吗 False 新 pk= 3 库里条数= 2
因为
pk已经是None,save()走的是 INSERT,新行拿到一个新主键。要「撤销删除」得靠事务(transaction.atomic里回滚),不是靠save()。
-
-
改
1
2
3stu = Student.objects.get(pk=1)
stu.age = 19
stu.save()只改部分字段可以用
save(update_fields=["age"]),能少发几列的 UPDATE。 -
max_length到底拦不拦(实测两种数据库)1
2s = Student(name="n" * 40, sex=True, content="c" * 40, age=1, grade=g)
s.full_clean()1
2ValidationError {'name': ['Ensure this value has at most 20 characters (it has 40).'],
'content': ['Ensure this value has at most 20 characters (it has 40).']}-
但直接
save()时:数据库 实测结果 SQLite 存下来了, len(...) == 40(不截断、不报错)MySQL 9.7.4( sql_mode含STRICT_TRANS_TABLES)DataError (1406, "Data too long for column 'name' at row 1") -
结论:
max_length主要由表单和full_clean()负责校验,数据库层是否拦取决于 DBMS。开发用 SQLite 一路绿灯、上线换 MySQL 就报错,就是这个原因。
-
视图、路由与模板:把页面跑起来
-
视图的基本使用
视图是对 Web 请求进行的响应;它接收
request对象作为第一个参数(由 Django 构建,包含请求的信息);本质就是一个函数,定义在views.py中。函数名同样按 PEP8 用下划线(老笔记的gradeStudents改成grade_students)。myapp/views.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from django.http import HttpResponse
from django.shortcuts import render
from myapp.models import Student, Grade
def index(request):
return HttpResponse("<H1>Hello Django</H1>")
def students(request):
stus = Student.objects.all()
return render(request, "myapp/students.html", {"stus": stus})
def grades(request):
gs = Grade.objects.all()
return render(request, "myapp/grades.html", {"grades": gs})
def grade_students(request, gid):
stus = Student.objects.filter(grade_id=gid)
return render(request, "myapp/students.html", {"stus": stus}) -
路由配置
-
主路由(
project/urls.py)关联子路由。老笔记这段少了urlpatterns的收尾方括号,照抄会SyntaxError,这里给完整版:urls.py 1
2
3
4
5
6
7from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("myapp.urls")),
] -
子路由:在应用目录下创建
urls.py。path()够用就用它,需要正则再用re_path()(django.conf.urls.url()早在 Django 4.0 就被删了):myapp/urls.py 1
2
3
4
5
6
7
8
9
10
11
12
13from django.urls import path, re_path
from myapp import views
app_name = "myapp" # 命名空间写在这里
urlpatterns = [
path("", views.index, name="index"),
path("students/", views.students, name="students"),
path("grades/", views.grades, name="grades"),
path("students/<int:gid>/", views.grade_students, name="grade_students"),
re_path(r"^old_students/(?P<gid>\d+)/$", views.grade_students, name="old_grade_students"),
] -
三种写法的实测差别:
写法 匹配范围 视图收到的 gidpath("students/<int:gid>/", ...)只有数字, /students/abc/直接 404(实测)已经是 intre_path(r"^students/(\d+)/$", ...)同样只匹配数字 是字符串,要自己转 re_path(r"^old_students/(?P<gid>\d+)/$", ...)命名组,可读性更好 同上,字符串 -
命名路由 +
reverse()实测(写死 URL 的坏处就是改路径时到处都要改):1
2
3
4
5
6
7from django.urls import reverse
reverse("myapp:index")
'/'
reverse("myapp:grade_students", kwargs={"gid": 2})
'/students/2/'
reverse("myapp:old_grade_students", kwargs={"gid": 3})
'/old_students/3/'
-
-
模板的基本使用
-
模板是 HTML 页面,可以根据视图中传递的数据进行填充(渲染)。
-
建目录:工程目录下建
templates,其下再按应用建myapp(这样不同应用的同名模板不会互相覆盖)。 -
配置模板路径(
settings.py里TEMPLATES的DIRS):settings.py 1
2
3
4
5
6
7
8TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
...
},
]DIRS必须是列表。实测把它写成字符串"DIRS": "templates"时check一声不吭,真正渲染才炸出TemplateDoesNotExist: myapp/students.html;APP_DIRS=True时还会额外去每个应用的templates/子目录里找。 -
模板文件(
templates/myapp/students.html),注意{% empty %}是空数据兜底:1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="zh">
<head><meta charset="UTF-8"><title>学生信息</title></head>
<body>
<h1>学生信息</h1>
<ul>
{% for stu in stus %}
<li>{{ stu.name }}--{{ stu.age }}--{{ stu.grade }}</li>
{% empty %}
<li>还没有学生</li>
{% endfor %}
</ul>
</body>
</html> -
templates/myapp/grades.html(点班级看对应班级的学生)。这里用{% url %}而不是写死路径:1
2
3
4
5
6
7
8
9
10
11
<html lang="zh">
<head><meta charset="UTF-8"><title>班级信息</title></head>
<body>
<ul>
{% for grade in grades %}
<li><a href="{% url 'myapp:grade_students' grade.id %}">{{ grade.name }}</a></li>
{% endfor %}
</ul>
</body>
</html>老笔记写的是
<a href="/students/{{ grade.id }}/">,改路由时模板得跟着改;{% url %}会跟着路由表走。 -
视图里用模板:
views.py 1
2
3
4
5
6
7def index(request):
return render(request, "myapp/index.html")
def students(request):
stus = Student.objects.all()
return render(request, "myapp/students.html", {"stus": stus})render()的第三个参数是上下文字典,模板里用{{ stu.name }}取值;{{ stu.grade }}打印的是班级对象,实际调用模型的__str__。
-
-
启动服务与实测结果
1
python manage.py runserver 127.0.0.1:8435 --noreload
请求 实测状态码 页面长度 校验点 /200 21 字节 HttpResponse直接返回的那串 HTML/students/200 314 字节 渲染 students.html,列表里是sunck--18--zzpython1801/grades/200 251 字节 {% url %}生成的链接是/students/1/、/students/2//students/1/200 314 字节 走 grade_students,同样渲染students.html/students/abc/404 — <int:gid>转换器不匹配非数字/probe/200 31 字节 内容是三条 reverse()结果用竖线拼起来的:/、/students/2/、/old_students/3/- 格式:
python manage.py runserver ip:port。ip可以不写,直接python manage.py runserver 8080;默认端口是 8000。- 写成
0.0.0.0:8000会监听所有网卡,手机/局域网都能访问,但DEBUG=True时别这么干到公网机器上。 --noreload关掉自动重载,调试脚本、压测时用;默认开着,改代码会自动重启,但删文件、加应用这类改动仍需手动重启。
- 这是纯 Python 写的轻量级开发服务器,只在开发阶段用,稳定性不足以对外服务;生产用 Gunicorn / uvicorn + Nginx(见部署篇)。
DEBUG = False时ALLOWED_HOSTS必须填上域名或 IP,否则每个请求都是 400 并提示DisallowedHost。
- 格式:
修改模型之后:迁移排障
-
正常流程
改
models.py→makemigrations→migrate,绝大多数情况这样就够了。 -
先判断「到底有没有待生成的迁移」
1
python manage.py makemigrations --check --dry-run
实测两种结果的退出码不一样,这条命令可以直接放进 CI:
情况 rc 输出 模型和迁移文件一致 0 No changes detected模型改了但没生成迁移 1 打印出生成的迁移内容,例如 myapp\migrations\0002_teacher.py - Create model Teacher -
看迁移计划与已应用状态
1
python manage.py migrate --plan
1
2Planned operations:
No planned migration operations.1
python manage.py showmigrations myapp
1
2myapp
[X] 0001_initial[X]表示这条迁移已经写进django_migrations表(记录),[ ]表示没跑过。 -
真出问题时的两种方案
- 先备份。任何迁移排障前,
mysqldump或复制db.sqlite3。 - 迁移文件没问题、只是记录对不上:
python manage.py migrate myapp 0001_initial --fake或--fake-initial,把记录补上而不动表。 - 想让某应用的迁移记录全部回到「没跑过」:实测输出:
1
python manage.py migrate myapp zero --fake
再1
2
3
4
5Operations to perform:
Unapply all migrations: myapp
Running migrations:
Rendering model states... DONE
Unapplying myapp.0001_initial... FAKEDshowmigrations myapp就变成[ ] 0001_initial(实测)。表和数据还在库里,只是记录被抹掉了 —— 这正是--fake的危险之处:接下来migrate会试图重新建表,如果表还在就会报「表已存在」。所以第 3 步之后要么手工把表删掉再migrate,要么把迁移文件也一起删干净重来。 - 开发库(SQLite / 本地测试 MySQL):删掉
db.sqlite3或DROP DATABASE,删掉migrations/00*.py(留__init__.py),重新makemigrations+migrate。 - 生产库:不允许走到第 3、4 步,只能补迁移文件,必要时用
RunPython写数据修补。
- 先备份。任何迁移排障前,
-
改字段名/删字段的顺序
先
makemigrations生成迁移,再打开迁移文件检查它是不是「删旧列 + 加新列」(这会丢数据)。想把boyNum改成boy_num这种改名,正确做法是在新字段上写db_column="boyNum"先占住旧列名,或者拆成两步迁移:先加新列、跑数据、再删旧列。
这篇里的新手卡点清单
都是实测过的,按踩中概率排序:
| 卡点 | 症状 | 正确做法 |
|---|---|---|
字段名拼错(contend) |
属性赋值不报错,库里存空值 | 用表单/序列化器保存,或 full_clean() 自检 |
字段用驼峰(boyNum) |
能跑,但迁移改名很痛、检查器报警 | 建表前就按 PEP8 写 boy_num |
INSTALLED_APPS 拼成 INSTALL_APPS |
check 报 LookupError: No installed app with label 'admin'. |
改完 settings 就跑一次 python manage.py check |
外键不写 on_delete |
TypeError: ForeignKey.__init__() missing 1 required positional argument: 'on_delete' |
每个外键都写 on_delete=models.CASCADE 之类 |
模板目录没配进 DIRS |
TemplateDoesNotExist: myapp/students.html |
DIRS: [BASE_DIR / "templates"],注意是列表 |
delete() 后想 save() 恢复 |
恢复出一条新记录,pk 变了 |
删除前留好主键,或用事务回滚 |
以为 max_length 会拦超长 |
SQLite 存进去了,MySQL 报 DataError (1406, ...) |
校验交给表单/序列化器,别指望数据库 |
照抄旧笔记的 url() |
ImportError: cannot import name 'url' from 'django.conf.urls' |
用 path(),需要正则时 re_path() |
urlpatterns 少收尾 ] |
SyntaxError 或导入期报错 |
老笔记里这段是截断的,别整段照抄 |
参考
- Django 官方文档:Writing your first Django app, part 1:官方入门工程写法。
- PEP 8 — Naming Conventions:模块/函数/变量小写下划线、类名大驼峰的出处。
- Django FAQ:What Python version can I use with Django:版本支持矩阵。
- Django 1.0 release notes:框架历史(2005-07 首发、2008-09 出 1.0)。
- Django migrations 参考:
makemigrations --check、migrate --plan、--fake等选项的官方说明。 - Django 官方文档:数据库后端:各数据库的官方支持口径与推荐驱动(MySQL 用
mysqlclient,PostgreSQL 用 psycopg 3)。 - mysqlclient:官方推荐的 MySQL 驱动,本机装的是 2.3.0 的 cp314 wheel。
- psycopg 3 文档:Django 4.2 起 PostgreSQL 后端唯一支持的驱动,
psycopg[binary]的出处。 - PostgreSQL 版本政策:每个大版本支持 5 年,没有单独的 LTS 线。
- PyMySQL:纯 Python 的 MySQL 驱动,
install_as_MySQLdb()兼容路线的出处。