Django博客从零搭建:纯后端CRUD实战教程

1. 项目概述:从零开始搭一个真正能用的 Django 博客

我带过十几期 Python 后端训练营,每次开课前都会问学员一个问题:“你写过一个能自己访问、自己增删改查、自己看到数据变化的完整 Web 应用吗?”——超过七成的人会摇头。不是学不会,而是教程太碎:有的讲完路由就停了,有的配好数据库就收尾,还有的直接甩出一整套现成代码,连 manage.py 是干啥的都没解释清楚。这篇内容,就是为那些卡在“写了半天,浏览器里只有一行 ‘It worked!’” 的人写的。

我们不碰 React、不聊 Vue、不堆 CSS 框架,就用 Django 自带的模板引擎、SQLite 默认数据库、admin 后台和原生 Python 逻辑,从空文件夹开始,一步步搭出一个真实可运行、可管理、可扩展的博客系统。它支持创建文章、列表展示、点击查看详情、编辑更新、一键删除——也就是完整的 CRUD 流程。重点在于:每一步你都能在终端里敲出来,每一行代码你都能在浏览器里看到效果,每一个报错你都能准确定位到是哪一行、为什么错。比如python manage.py runserver启动后打不开 localhost:8000?不是网络问题,大概率是你没激活虚拟环境,或者终端当前路径不在项目根目录下。这种细节,我会在实操环节掰开揉碎讲透。

这个项目不需要你精通 JavaScript 或前端工程化,但要求你熟悉 Python 基础语法:知道def怎么定义函数、class怎么声明类、for循环怎么遍历列表、字典怎么用键取值。如果你能写一个带if-else判断的计算器脚本,那你就能跟上这篇内容。它适合两类人:一是刚学完 Python 基础、想立刻做出点“看得见摸得着”东西的新手;二是已经会写 Flask 小应用、想系统理解 Django “约定优于配置”设计哲学的进阶者。它不教你如何部署到云服务器,但会把本地开发环境的每个螺丝钉都拧紧——因为线上问题,八成源于本地环境没理顺。

2. 整体架构与设计思路拆解:为什么 Django 要这样组织项目?

2.1 Django 的“三层洋葱模型”:从外到内看懂项目结构

很多新手第一次django-admin startproject mysite后,面对生成的七八个文件和嵌套目录直接懵掉。其实 Django 的项目结构不是随意堆砌,而是一个精心设计的“三层洋葱模型”,每一层解决一类问题,且严格隔离:

  • 最外层(Project 层):对应django_blog/目录,它是一个容器,负责全局配置。settings.py是它的大脑,管数据库连接、静态文件路径、安全密钥、已安装应用列表;urls.py是它的门卫,决定所有 HTTP 请求该分发给谁;manage.py是它的总控台,所有命令行操作都从这里发起。它不处理具体业务逻辑,只做统筹调度。

  • 中间层(App 层):对应blog/目录,它是功能单元。Django 强调“一个 App 做好一件事”,比如blog只管文章,users只管用户,comments只管评论。每个 App 内部有清晰分工:models.py定义数据结构(就像 Excel 表头),views.py处理用户请求(就像客服接电话),urls.py规划 App 内部路线(就像商场楼层导览图),admin.py配置后台管理(就像物业办公室)。这种拆分让项目像乐高一样可插拔——今天加个搜索 App,明天换掉评论 App,都不影响博客主体。

  • 最内层(Template & Static 层):对应templates/static/目录,它是用户界面。templates/blog/下的 HTML 文件不是纯静态页面,而是 Django 模板,里面嵌入{% for post in blog_list %}这样的逻辑标签,能动态渲染数据;static/下放 CSS、JS、图片等资源,由 Django 在运行时自动拼接 URL。关键点在于:模板里绝对不写 Python 代码,只写极简的控制流和变量输出——这是 Django 的铁律,保证前后端关注点分离。

这个结构不是为了炫技,而是为了解决真实痛点。比如你发现博客首页加载慢,排查方向就很明确:先看views.py里的查询是否用了.select_related()避免 N+1 查询,再看templates/blog_list.html里有没有循环中嵌套数据库查询(这是新手常踩的坑),最后才去优化 CSS 文件大小。结构即约束,约束即效率。

2.2 为什么选 SQLite 而非 MySQL/PostgreSQL?

教程里直接用sqlite3,常被质疑“太假,生产环境谁用 SQLite?”。这话对了一半。SQLite 确实不适合高并发网站,但它在开发阶段有不可替代的优势:

  • 零配置启动:MySQL 要装服务、建库、设用户、配权限;PostgreSQL 要初始化集群、改配置文件。而 SQLite 就是一个.db文件,Django 创建项目时自动生成,migrate命令一跑,表结构全建好。你省下的这半小时,足够写三个视图函数。

  • 完全隔离的测试沙盒:每个 Django 项目配一个独立的db.sqlite3文件。你删库重来?rm db.sqlite3 && python manage.py migrate两行命令搞定。而 MySQL 里删库可能误伤其他项目,还得手动备份。我在带团队时,强制要求新人用 SQLite 开发,就是为了让他们敢试错——改坏模型?删库重来;写错迁移?python manage.py migrate blog zero回滚到初始状态。

  • ORM 抽象层的真实检验场:Django ORM 的强大,在于它能屏蔽底层数据库差异。你在 SQLite 上写的Post.objects.filter(title__icontains='Django'),换到 PostgreSQL 里几乎不用改。但如果一开始就用 MySQL,遇到JSONField兼容性问题、fulltext search语法差异,反而会干扰你对 Django 核心机制的理解。就像学开车,先在空旷操场练熟离合油门,再去高速路实战。

当然,当项目要上线,切换数据库只需三步:改settings.py里的DATABASES配置,装对应驱动(pip install psycopg2-binary),再跑一次migrate。这个过程本身,就是理解 Django 架构的好机会。

2.3 为什么坚持“纯后端”路线?不碰前端框架的深层考量

教程明确说“不使用 JavaScript、CSS 等前端技术”,这不是偷懒,而是刻意为之的设计选择:

  • 聚焦核心抽象能力:新手最大的认知负担,不是语法,而是“数据如何从数据库流到浏览器”。用原生 HTML 模板,你能清晰看到:views.pycontext = {'blog_list': posts}把数据打包 →render()函数把包塞进blog_list.html→ 模板里{{ post.title }}解包显示。如果引入 Vue,这个链条就变成:Python 返回 JSON → JS 接收 → Vue 渲染 DOM,中间多了一层黑箱,出问题时你不知道是后端没传对数据,还是前端解析错了格式。

  • 避免“前端焦虑”干扰学习主线:我见过太多人卡在“怎么让 CSS 样式居中”上,花三天研究 Flexbox,却忘了models.pyCharField(max_length=200)max_length是干嘛的。Django 的价值在于它帮你把重复劳动(用户认证、表单验证、URL 路由)封装成一行代码,而不是让你成为全栈超人。先把Post.objects.create(title="Hello", content="World")这行 Python 玩明白,比折腾 Tailwind CSS 配置重要十倍。

  • 为后续扩展留足接口:这个纯后端博客,天然就是一个 REST API 基座。你只需要在views.py里把render()换成JsonResponse(),把blog_list.html换成序列化逻辑,它立刻变成供 React/Vue 调用的数据源。我在实际项目中,常先用 Django 搭好数据核心和 admin 后台,再让前端团队基于/api/posts/接口开发,效率极高。

所以,这不是一个“简陋版”博客,而是一个经过深思熟虑的“最小可行教学模型”。它剔除所有干扰项,只保留 Django 最硬核的骨架:模型定义数据、视图处理请求、模板渲染结果、URL 连接一切。

3. 核心细节解析与实操要点:每个命令背后的原理与陷阱

3.1 虚拟环境:为什么venv不是可选项,而是必选项?

python -m venv env这条命令,新手常忽略其重量。它创建的不是一个文件夹,而是一个Python 运行时宇宙。让我用一个真实案例说明:去年有个学员,本地装了 Python 3.9,用pip install django==4.2装了新版 Django。两周后他想跑一个老项目,需要 Django 3.2,于是pip install django==3.2——结果整个系统崩了,因为pip直接覆盖了全局安装。他不得不重装 Python。

虚拟环境就是为解决这个问题。venv的本质,是复制一份 Python 解释器及其标准库,再新建一个独立的site-packages目录存放第三方包。执行source env/bin/activate(Mac/Linux)或env\Scripts\activate.bat(Windows)后,终端提示符前会出现(env),这意味着:

  • which python指向env/bin/python,而非系统/usr/bin/python
  • pip list显示的只有env里装的包
  • import django加载的是env/lib/python3.9/site-packages/django/

提示:永远不要在未激活虚拟环境时运行pip install。我养成的习惯是:打开终端第一件事,cd到项目目录,然后source env/bin/activate。如果提示Command not found,说明你没装 Python 或路径不对,这时别硬试,先python --version确认基础环境。

还有一个隐藏陷阱:Windows 用户用 Git Bash 运行source env/Scripts/activate会失败,因为 Bash 无法执行 Windows 的.bat文件。正确做法是:要么用 Windows Terminal 运行env\Scripts\activate.bat,要么在 Git Bash 中用source env/Scripts/activate(注意是正斜杠)。我在训练营里,专门录了 3 分钟视频演示不同终端的激活方式,因为 90% 的环境问题都出在这里。

3.2manage.pydjango-admin:两个命令行工具的本质区别

新手常混淆django-admin startprojectpython manage.py startapp。它们看似相似,实则定位完全不同:

  • django-admin是 Django 的全局命令行工具,随 Django 安装到系统 PATH。它只做两件事:startproject(创建新项目)和check(检查项目配置)。它不依赖任何项目,就像工厂的总装线,只负责产出初始框架。

  • manage.py是 Django 为每个项目量身定制的管理脚本,由django-admin startproject自动生成。它内部做了三件关键事:1) 设置DJANGO_SETTINGS_MODULE环境变量,指向你的settings.py;2) 将项目根目录加入sys.path,让 Python 能 import 你的 App;3) 加载 Django 设置,使所有命令(如runserver,migrate)能读取数据库配置。没有manage.pypython manage.py migrate就是一句无效的 Python 代码。

所以,startproject必须用django-admin,因为此时项目还不存在;而startapprunservermigrate等所有后续操作,必须用manage.py,因为它承载了项目的上下文。我教学生一个记忆法:“admin是建楼的设计师,manage是管楼的物业经理”。

3.3makemigrationsmigrate:数据库演化的双阶段模型

python manage.py makemigrationspython manage.py migrate是 Django 数据库操作的灵魂,但很多人只知其然,不知其所以然。它们构成一个严谨的“双阶段演化模型”:

  • 第一阶段:makemigrations—— 生成迁移计划书
    当你修改models.py(比如给Post加个pub_date = models.DateTimeField()字段),Django 并不直接改数据库。它先扫描所有模型,对比当前代码与上次迁移记录(migrations/0001_initial.py),生成一个可执行的 Python 脚本,存入migrations/0002_add_pub_date.py。这个脚本里写着:“在blog_post表加一列pub_date,类型是 datetime,允许为空”。它之所以是 Python 而非 SQL,是为了跨数据库兼容——同一份迁移脚本,在 SQLite、PostgreSQL、MySQL 上都能运行。

  • 第二阶段:migrate—— 执行迁移计划书
    migrate命令读取migrations/目录下的所有脚本,按数字顺序(0001, 0002...)执行。它先检查数据库里django_migrations表,确认哪些脚本已执行,然后只运行未执行的脚本。执行时,Django 根据当前数据库后端,将 Python 脚本翻译成对应 SQL(如 SQLite 的ALTER TABLE blog_post ADD COLUMN pub_date datetime;),再发送给数据库。

注意:makemigrations生成的脚本必须提交到 Git!它是数据库版本的唯一真相源。如果同事拉代码后没运行migrate,他的数据库就和代码不一致,必然报错。我在团队里强制规定:git commit前,必须git status确认migrations/下有新增文件。

一个经典错误场景:你改了模型,makemigrations生成了0002.py,但还没migrate就直接runserver。这时 Django 会报django.core.exceptions.ImproperlyConfigured: Requested setting DATABASES, but settings are not configured.——因为 ORM 尝试加载模型时,发现数据库表结构和代码不匹配。解决方案永远是:先migrate,再runserver

3.4admin.site.register(Post):后台管理的魔法开关原理

admin.py里短短一行admin.site.register(Post),背后是 Django 最精妙的设计之一。它不是简单地把模型“暴露”给后台,而是触发了一整套自动化流程:

  • 元数据提取:Django 通过 Python 的inspect模块,读取Post类的属性(title,content,created_at),自动推断字段类型(CharField→ 文本框,TextField→ 多行文本框,DateTimeField→ 日期时间选择器)。

  • CRUD 视图注入register调用后,Django 动态生成四个视图函数:post_add(创建)、post_change(编辑)、post_changelist(列表)、post_delete(删除)。这些视图已内置权限校验、表单验证、日志记录。

  • URL 自动注册admin.site.urls在项目启动时,会扫描所有已注册模型,为每个模型生成 URL 模式,如/admin/blog/post/(列表页)、/admin/blog/post/add/(创建页)。你无需在urls.py里手动添加。

但这也带来一个常见误区:有人以为register后就能直接用,结果访问/admin/看不到Post。原因通常是:1)INSTALLED_APPS里没加'blog',Django 根本没加载blog.admin模块;2)Post模型定义在models.py里,但admin.pyfrom .models import Post导入失败(比如models.py有语法错误)。我的排查口诀是:“先看INSTALLED_APPS,再看admin.py导入,最后看models.py语法”。

4. 实操过程与核心环节实现:手把手完成博客全流程

4.1 环境搭建与项目初始化:从零到第一个 "It worked!"

我们从最干净的状态开始。假设你已安装 Python 3.8+(python --version确认),现在打开终端(Git Bash / Terminal / CMD):

# 1. 创建项目目录并进入 mkdir ~/Desktop/project-blog cd ~/Desktop/project-blog # 2. 创建虚拟环境(注意:env 是环境名,可自定义) python -m venv env # 3. 激活虚拟环境 # Mac/Linux: source env/bin/activate # Windows (CMD/PowerShell): env\Scripts\activate.bat # Windows (Git Bash): source env/Scripts/activate # 4. 升级 pip(避免旧版 pip 安装包失败) pip install --upgrade pip # 5. 安装 Django(指定稳定版本,避免新特性导致教程不兼容) pip install "Django>=4.2,<5.0"

提示:pip install "Django>=4.2,<5.0"用引号包裹,防止 shell 把<当作重定向符号。这是 Windows Git Bash 用户的高频坑点。

激活后,终端提示符应显示(env)。现在创建 Django 项目:

# 6. 创建项目(django_blog 是项目名,可改) django-admin startproject django_blog . # 注意最后的点(.),它让文件生成在当前目录,而非子目录 # 此时目录结构:project-blog/ -> manage.py, django_blog/

验证项目结构:

ls -la # 应看到:manage.py django_blog/ env/ ls django_blog/ # 应看到:__init__.py asgi.py settings.py urls.py wsgi.py

启动开发服务器:

# 7. 运行服务器(确保在 project-blog/ 目录下) python manage.py runserver

打开浏览器,访问http://127.0.0.1:8000http://localhost:8000。看到 “The install worked successfully! Congratulations!” 页面,说明环境搭建成功。此时不要关闭终端Ctrl+C可停止服务器。

注意:如果报错ModuleNotFoundError: No module named 'django',一定是虚拟环境没激活;如果报错Error: That port is already in use,说明另一个程序占用了 8000 端口,加端口号python manage.py runserver 8001换个端口。

4.2 创建 Blog App 与模型定义:让数据落地生根

项目骨架有了,现在创建业务模块:

# 确保在 project-blog/ 目录,且虚拟环境已激活 python manage.py startapp blog

此时目录新增blog/文件夹。现在要告诉 Django:“这个项目要用 blog 这个 App”。编辑django_blog/settings.py,找到INSTALLED_APPS列表,在末尾添加'blog'

# django_blog/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', # ← 新增这一行,注意逗号 ]

提示:INSTALLED_APPS里的字符串必须是 App 名(blog),不是文件夹路径(./blog)或 Python 模块名(blog.apps.BlogConfig)。新手常在此处拼写错误,比如写成'Blog'(首字母大写)或'myblog'(名字不一致)。

接下来定义数据模型。编辑blog/models.py

# blog/models.py from django.db import models class Post(models.Model): title = models.CharField(max_length=200) # 标题,最长200字符 content = models.TextField() # 正文,无长度限制 created_at = models.DateTimeField(auto_now_add=True) # 创建时间,自动填入 def __str__(self): return self.title # 在 admin 后台显示标题,而非 "Post object (1)"

关键点解析:

  • CharField(max_length=200)max_length是必填参数,SQLite 要求显式声明长度。
  • TextField():适合长文本,无需max_length
  • auto_now_add=True:对象首次保存时自动设置为当前时间,之后不再更新。
  • __str__方法:Python 类的“字符串表示”,让 admin 后台和调试时显示有意义的名字。

4.3 数据库迁移与 Admin 后台:让数据可管理

模型写好了,但数据库里还没有blog_post表。执行迁移:

# 生成迁移文件 python manage.py makemigrations # 输出:Migrations for 'blog': blog/migrations/0001_initial.py # 应用迁移(创建表) python manage.py migrate

查看数据库文件(SQLite):

ls -la # 应看到 db.sqlite3 文件(约 20KB)

现在注册模型到 admin 后台。编辑blog/admin.py

# blog/admin.py from django.contrib import admin from .models import Post # ← 导入模型 @admin.register(Post) # ← 使用装饰器注册(推荐写法) class PostAdmin(admin.ModelAdmin): list_display = ('title', 'created_at') # 后台列表页显示哪些字段 list_filter = ('created_at',) # 右侧筛选栏按时间过滤 search_fields = ('title', 'content') # 顶部搜索框支持标题和正文搜索

提示:@admin.register(Post)比老式admin.site.register(Post)更灵活,可直接配置显示字段。list_display里不能写方法名(如get_title),除非该方法有short_description属性。

创建超级用户(admin 登录账号):

python manage.py createsuperuser # 按提示输入用户名、邮箱(可选)、密码(不显示)

启动服务器,访问http://127.0.0.1:8000/admin/,用刚创建的账号登录。左侧应看到 “Blog” 分组和 “Posts” 链接。点击 “ADD POST”,填写标题和内容,点击 “SAVE”。刷新列表页,新文章出现——数据已成功写入数据库。

4.4 视图、URL 与模板:让数据在浏览器里活起来

现在数据有了,但用户无法访问。我们需要“视图”处理请求,“URL” 定义路径,“模板” 渲染页面。

第一步:编写视图函数
编辑blog/views.py

# blog/views.py from django.shortcuts import render from .models import Post def blog_list(request): """博客列表页视图""" posts = Post.objects.all().order_by('-created_at') # 按时间倒序 context = {'blog_list': posts} # 键名 blog_list 将在模板中使用 return render(request, 'blog/blog_list.html', context) def blog_detail(request, id): """博客详情页视图""" post = Post.objects.get(id=id) # 获取指定 ID 的文章 context = {'post': post} return render(request, 'blog/blog_detail.html', context) def blog_delete(request, id): """删除博客视图""" post = Post.objects.get(id=id) post.delete() return redirect('/posts/') # 删除后跳转回列表页

注意:redirect需要导入,所以在文件开头加:

from django.shortcuts import render, redirect from django.http import HttpResponse from .models import Post

第二步:配置 App 内部 URL
blog/目录下创建urls.py

# blog/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.blog_list, name='blog_list'), # /posts/ path('<int:id>/', views.blog_detail, name='blog_detail'), # /posts/1/ path('<int:id>/delete/', views.blog_delete, name='blog_delete'), # /posts/1/delete/ ]

关键点:<int:id>表示 URL 中的id必须是整数,Django 会自动转换为int类型传给视图函数。

第三步:配置项目主 URL
编辑django_blog/urls.py,导入include并添加blog路由:

# django_blog/urls.py from django.contrib import admin from django.urls import path, include # ← 添加 include urlpatterns = [ path('admin/', admin.site.urls), path('posts/', include('blog.urls')), # ← 新增这一行 ]

第四步:创建模板文件
在项目根目录(project-blog/)创建templates/文件夹,再建templates/blog/

mkdir -p templates/blog

创建templates/blog/blog_list.html

<!-- templates/blog/blog_list.html --> <!DOCTYPE html> <html> <head> <title>我的博客</title> </head> <body> <h1>博客列表</h1> <ul> {% for post in blog_list %} <li> <a href="/posts/{{ post.id }}/">{{ post.title }}</a> <small>{{ post.created_at|date:"Y-m-d H:i" }}</small> </li> {% endfor %} </ul> <p><a href="/admin/blog/post/add/">添加新文章</a></p> </body> </html>

创建templates/blog/blog_detail.html

<!-- templates/blog/blog_detail.html --> <!DOCTYPE html> <html> <head> <title>{{ post.title }}</title> </head> <body> <h1>{{ post.title }}</h1> <p>{{ post.content|linebreaks }}</p> <p><small>发布于 {{ post.created_at|date:"Y年m月d日 H:i" }}</small></p> <p><a href="/posts/">返回列表</a> | <a href="/admin/blog/post/{{ post.id }}/change/">编辑</a> | <a href="/posts/{{ post.id }}/delete/">删除</a></p> </body> </html>

关键点:

  • {{ post.title }}输出变量值
  • {% for %}是模板循环语法
  • |date:"Y-m-d H:i"是 Django 过滤器,格式化时间
  • |linebreaks将换行符转为<br>,让正文换行生效

第五步:配置模板路径
编辑django_blog/settings.py,找到TEMPLATES配置,修改'DIRS'

# django_blog/settings.py TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], # ← 添加这一行,指向 templates/ 文件夹 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]

启动服务器,访问http://127.0.0.1:8000/posts/,应看到博客列表;点击标题,进入详情页。至此,CRUD 中的 C(Create)、R(Read)、D(Delete)已实现。U(Update)已在 admin 后台支持(/admin/blog/post/1/change/)。

5. 常见问题与排查技巧实录:那些年踩过的坑与速查方案

5.1 终端报错速查表:从现象定位根本原因

现象可能原因排查步骤解决方案
Command 'django-admin' not foundDjango 未安装或 PATH 未配置pip list | grep Djangopip install Django,重启终端
ModuleNotFoundError: No module named 'blog'INSTALLED_APPS未添加'blog'或拼写错误cat django_blog/settings.py | grep blog检查INSTALLED_APPS是否含'blog',注意引号和逗号
ImportError: cannot import name 'Post' from 'blog.models'models.py有语法错误(如少括号、冒号)python -m py_compile blog/models.py修复models.py语法,python manage.py check验证
no such table: blog_postmigrate未执行或执行失败ls db.sqlite3python manage.py showmigrationspython manage.py migrate,确认blog显示[X]
Reverse for 'blog_detail' not foundURL 名称不匹配(name='blog_detail'未定义或拼错)grep -r "blog_detail" blog/urls.py检查blog/urls.pypath(..., name='blog_detail')是否存在
Page not found (404)访问/posts/django_blog/urls.pyinclude('blog.urls')cat django_blog/urls.py确认path('posts/', include('blog.urls'))存在
TemplateDoesNotExist at /posts/templates/路径未在settings.py中配置cat django_blog/settings.py | grep DIRS确认TEMPLATES[0]['DIRS']包含BASE_DIR / 'templates'
Invalid HTTP_HOST headerALLOWED_HOSTS未配置(Django 3.1+ 安全要求)cat django_blog/settings.py | grep ALLOWED_HOSTSALLOWED_HOSTS = ['127.0.0.1', 'localhost']

5.2 实操中的独家避坑技巧

技巧一:python manage.py check是你的第一道防线
在每次修改settings.pymodels.pyurls.py后,运行python manage.py check。它会扫描整个项目,报告配置错误、模型关系问题、URL 冲突等。比如你忘了在INSTALLED_APPS'blog',它会直接提示?: (models.E001) 'blog.Post' has no field named 'title'——虽然提示有点绕,但比等到runserver报错再排查快十倍。

技巧二:用shell实时验证模型操作
别总靠admin后台测试数据。启动 Django Shell:

python manage.py shell

然后直接写 Python 代码:

>>> from blog.models import Post >>> Post.objects.all() # 查所有文章 >>> Post.objects.create(title="Test", content="Hello World") # 创建新文章 >>> Post.objects.filter(title__startswith="Test") # 模糊查询

Shell 是 Django 的“交互式实验室”,所有 ORM 操作在这里秒级反馈,比写视图、启服务、刷网页高效得多。

技巧三:迁移冲突的黄金处理法
多人协作时,A 和 B 同时改了模型,makemigrations生成了0002.py0002_another.pymigrate会报错。正确做法:

  1. python manage.py migrate blog zero回滚到初始状态
  2. rm blog/migrations/0002_*.py删除冲突的迁移文件
  3. python manage.py makemigrations重新生成一个合并后的0002.py
  4. python manage.py migrate执行

注意:此操作仅限开发环境。生产环境必须用--fake-initial等高级选项,切勿删迁移文件。

技巧四:DEBUG=False下的静态文件救星
教程中没配静态文件,但当你加 CSS 时,DEBUG=False会导致样式丢失。临时解决方案(仅开发):在django_blog/urls.py末尾加:

from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

这行代码让 Django 开发服务器能直接提供static/文件,避免配置 Nginx 的复杂度。

5.3 从“能跑”到“好用”的三个关键升级

完成基础 CRUD 后,这三个小升级能让博客立刻专业起来:

升级一:添加分页(避免列表页加载过慢)
编辑blog/views.py

from django.core.paginator import Paginator def blog_list(request): posts = Post.objects.all().order_by('-created_at') paginator = Paginator(posts, 5) # 每页5篇 page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) return render(request, 'blog/blog_list.html', {'page_obj': page_obj})

修改templates/blog/blog_list.html

{% for post in page_obj %} <li><a href="/posts/{{ post.id }}/">{{ post.title }}</a></li> {% endfor %} <div class="pagination"> <span class="page-links"> {% if page_obj.has_previous %} <a href="?page=1">&laquo; first</a> <a href="?page={{ page_obj.previous_page_number }}">previous</a> {% endif %} <span class="current">Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.</span> {% if page_obj.has_next %} <a href="?page={{ page_obj.next_page_number