ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DjangoBlog全解析:MVT架构、ORM查询优化与博客系统实战

DjangoBlog全解析:MVT架构、ORM查询优化与博客系统实战 1. DjangoBlog是什么功能拆解与设计思路1.1 博客系统的核心功能清单先说结论DjangoBlog是一个典型的博客系统但它的价值不在于“博客”本身而在于把Django的核心机制全部串联了起来。我见过不少新手项目要么是TodoList要么是投票应用做完一遍对Django还是懵的——因为那些小项目根本用不上MVT架构的完整链路。DjangoBlog不一样它逼着你把模型关系、视图处理、模板渲染、路由分发、后台管理从头到尾串一遍。一个标准的DjangoBlog至少包含以下几块功能文章管理发布、编辑、草稿、删除以及文章的状态流转草稿→已发布→下架。分类与标签文章可以挂在多个分类下也可以打多个标签这涉及一对一、一对多、多对多三种关系。评论系统用户对文章发表评论涉及表单处理、数据校验、关联查询。用户认证登录、注册、权限控制至少得区分普通访客和管理员。侧栏组件最新文章、热门文章、分类列表、标签云。后台管理Django自带的Admin站点直接支撑内容维护。如果你只是想要一个能写文章的博客WordPress早就解决了。DjangoBlog这类项目真正的意义是让你在一个足够复杂、又足够熟悉的场景里把MVT架构的每一个环节都亲手摸一遍。博客是最常见的内容系统每个人都能理解它的业务逻辑这让你可以把全部精力放在技术实现上而不是花时间去理解业务需求。1.2 为什么选Django来做博客系统选Django不是因为它最轻量恰恰相反对于博客这种规模的项目Django有些“重”。但正是这些重家伙才是学习价值所在。第一Django自带Admin后台。你不需要自己写管理界面创建完模型后只需在admin.py里注册一下就拥有了一个完整的后台管理系统。对于博客这种内容型应用“内容管理”本身就是核心需求而Django把这块直接送给你了。第二ORM足够成熟。Django的ORM支持复杂的关联查询、聚合、分页而且生成的SQL是经过高度优化的。你在写博客时肯定会遇到“查某篇文章的所有评论”这类需求手写SQL很容易但要在不同模型之间维护一致性就麻烦了。ORM帮你把这条链路的复杂度封装掉了。第三项目结构规整适合新手建立正确心智。Django的MVT架构不是一个抽象概念它通过目录结构强制你分解职责models.py管数据、views.py管逻辑、templates管展示。我见过很多自学后端的朋友代码写得不错但问他“你这块的Model是哪个”“这个页面由哪个View渲染”答不上来。用DjangoBlog练一遍这类问题自然就解决了。1.3 MVT架构在DjangoBlog中的具体映射MVT发音上和MVC很像但Django这边把Controller的角色弱化了一些用URLconf路由加View层一起承担。要理解MVT最好的办法是把它映射到DjangoBlog的实际代码里架构层DjangoBlog中的具体角色典型代码位置Model文章、分类、标签、评论的数据结构models.pyView获取数据、处理逻辑、传给模板views.pyTemplate前端HTML渲染、展示数据templates/URLconf路由分发把URL映射到Viewurls.py拿“查看文章列表”这一个最简单的功能拆解用户在浏览器输入localhost:8000/post/请求先到urls.py找到对应的ViewView调用Model层查询所有已发布的文章把查询结果打包到context里最后渲染模板把HTML返回给浏览器。每一步都有明确的归属这就是MVT架构的设计哲学——各司其职互不越界。这一点在排查问题的时候尤其好用。页面显示不对先判断是数据错了还是展示错了数据错了去查Model和View展示错了去查Template范围直接缩小一大半。2. MVT架构逐层拆解模型、视图、模板各管什么2.1 Model层用ORM定义数据结构和关系DjangoBlog里最核心的Model当然是文章。一个简单的Post模型看起来是这样的# blog/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name models.CharField(分类名称, max_length50) created_time models.DateTimeField(创建时间, auto_now_addTrue) def __str__(self): return self.name class Tag(models.Model): name models.CharField(标签名称, max_length50) def __str__(self): return self.name class Post(models.Model): title models.CharField(标题, max_length100) body models.TextField(正文) category models.ForeignKey(Category, on_deletemodels.CASCADE) tags models.ManyToManyField(Tag, blankTrue) author models.ForeignKey(User, on_deletemodels.CASCADE) created_time models.DateTimeField(创建时间, auto_now_addTrue) modified_time models.DateTimeField(修改时间, auto_nowTrue) is_published models.BooleanField(是否发布, defaultFalse) views models.PositiveIntegerField(浏览量, default0) class Meta: ordering [-created_time] def __str__(self): return self.title这段代码看起来简单但里面有几个关键决策值得细想。首先是on_deletemodels.CASCADE这个参数决定了“父记录被删除时子记录怎么办”。拿分类来说如果某一分类被删了它下面所有的文章也一并删除——这就是级联删除。如果你不希望文章被连带删除可以用PROTECT或者SET_NULL这完全取决于业务需求。从模型建立起就把这些关系想清楚比写到一半再改要省事得多。其次是ManyToManyField文章和标签是多对多的关系——一篇文章可以打多个标签一个标签可以挂在多篇文章下面。Django会自动创建一张中间表专门维护这种关系不需要你手动设计。我第一次写多对多关系时老想着自己建一张关联表然后手动查后来才发现ORM帮你把这一切都封装好了直接post.tags.add(tag_obj)就完事。定义好Model之后需要生成数据库迁移文件并执行迁移python manage.py makemigrations python manage.py migrate这个过程会把Model里的字段翻译成数据库表的列把关系翻译成外键和中间表。很多新手在这里会漏掉makemigrations直接跑migrate然后发现数据库里啥也没变。记住这两条命令是先后关系前者生成迁移文件后者执行迁移缺一不可。2.2 View层业务逻辑的真正执行者View层是MVT架构里“最聪明”的那一层它负责接收用户的请求从Model层拿数据处理各种业务逻辑最后把结果交给Template层。Django的View有两种写法函数视图FBV和类视图CBV。DjangoBlog这种项目推荐优先用类视图尤其是内置的ListView和DetailView能省掉大量样板代码。比如文章列表# blog/views.py from django.views.generic import ListView, DetailView from .models import Post class PostListView(ListView): model Post template_name blog/post_list.html context_object_name post_list paginate_by 5 def get_queryset(self): # 只显示已发布的文章并按创建时间倒序 return Post.objects.filter(is_publishedTrue)这里读懂两件事就够了。第一get_queryset是核心的查询逻辑入口你在这里写“取哪些数据”。上面的代码过滤掉了草稿文章只保留已发布的。如果要做分类归档只需要在这个方法里加过滤条件。第二ListView帮我们处理了分页这种通用逻辑paginate_by 5表示每页5篇文章模板里可以拿到page_obj变量来渲染分页组件。为什么用类视图而不是手写函数视图省代码只是表面原因更关键的是内置的类视图把“取数据、分页、上下文打包”这些通用步骤固化下来了逻辑边界清晰不容易出错。等你熟悉了ListView的工作方式为博客加一个“分类归档页”基本就是换一个get_queryset的事。2.3 Template层页面渲染与模板继承Template层在MVT里负责展示它不应该包含任何业务逻辑。很多新手犯的错是在模板里写复杂判断甚至去做数据计算。这不是Django的风格。模板层面只干三件事展示变量、做简单的循环和判断、复用页面结构。DjangoBlog里最典型的模板操作是基模板继承。每个页面都有公共的导航栏、底部信息没必要在每个HTML里重复粘贴。把公共结构放在base.html!-- templates/base.html -- !DOCTYPE html html head title{% block title %}我的博客{% endblock %}/title /head body nav.../nav {% block content %} {% endblock %} footer.../footer /body /html子模板只需要重新定义block!-- templates/blog/post_list.html -- {% extends base.html %} {% block title %}文章列表 | 我的博客{% endblock %} {% block content %} {% for post in post_list %} article h2a href{% url blog:detail post.pk %}{{ post.title }}/a/h2 p{{ post.body|truncatechars:100 }}/p /article {% endfor %} {% endblock %}模板里{% url blog:detail post.pk %}这种语法是Django模板的特色它不是直接写死链接地址而是通过URL路由反向解析生成。这样即使你改动了路由的URL规则模板里的链接会自动跟着变不用挨个去改HTML。这算是我用得最多也最推荐的做法。需要提醒的是模板里能用过滤器比如truncatechars:100把正文截断到100个字符但不要在模板里做太复杂的逻辑。如果你发现自己在模板里写了一长串{% if %}嵌套优先考虑把逻辑移到View层在View里把数据处理好模板只负责最后一步展示。2.4 URL路由把请求分发给正确的ViewURLconf是很多初学者会忽略的一层但它其实是MVT里把“用户请求”和“业务逻辑”衔接起来的关键环节。DjangoBlog的路由设计分为两层项目级路由和应用级路由。项目级的urls.py把根域名下的请求分发到不同的应用# config/urls.py from django.contrib import admin from django.urls import path, include urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ]应用级的blog/urls.py则定义具体的URL规则# blog/urls.py from django.urls import path from . import views app_name blog urlpatterns [ path(, views.PostListView.as_view(), namehome), path(post/int:pk/, views.PostDetailView.as_view(), namedetail), ]注意int:pk这种写法它表示URL里带一个整数类型的参数Django会自动把它解析出来传给View。比如访问/post/3/pk3会被提取出来在PostDetailView里通过self.kwargs[pk]拿到。Django的路径转换器还支持字符串、slug、UUID等类型博客的详情页我个人更喜欢用文章slug而不是数字主键URL更友好。app_name blog结合namedetail构成了命名空间。这样在模板里写{% url blog:detail post.pk %}的时候即使有多个应用都有名叫detail的URL也不会冲突。3. DjangoBlog核心功能的实操实现从建项目到核心功能3.1 初始化项目与创建app的标准流程老规矩先把项目骨架搭起来。这里我假设你已经装好了Python和pipDjango版本用最新的LTS版本就好我这篇是基于Django 4.x来写的。# 创建虚拟环境并激活 python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate # 安装Django pip install django # 创建项目和app django-admin startproject config . python manage.py startapp blog创建好后你的目录结构大概是这样的config/ settings.py urls.py manage.py blog/ models.py views.py urls.py templates/manage.py startapp blog创建了一个app但Django此时还不知道它的存在。你需要去config/settings.py里的INSTALLED_APPS列表把blog加进去。很多人做完这一步就忘了然后发现模型建了、迁移也跑了但页面就是404多半是app没注册。接着是注册路由在config/urls.py里include(blog.urls)。再把blog/urls.py建出来写上app_name blog和两个初始路由。这时候跑python manage.py runserver访问localhost:8000应该能看到一个空的文章列表页面。到这里一个最小可运行的DjangoBlog骨架就通了。3.2 文章列表与详情的查询逻辑文章列表页的View在2.2节给过示例这里补充DetailView的写法。class PostDetailView(DetailView): model Post template_name blog/post_detail.html context_object_name post def get_object(self, querysetNone): # 通过get_object_or_404自动处理404 post super().get_object(queryset) # 每访问一次浏览量加1 post.views 1 post.save(update_fields[views]) return post这里有个细节值得说浏览量自增时用了update_fields[views]。如果不指定这个参数Django会默认把这条记录的所有字段都更新一遍虽然结果没差别但会多产生一次不必要的数据库写入。对于浏览量这种高频更新的字段精确指定字段是个好习惯。详情页的模板和列表页类似区别在于数据是单个对象而非对象列表直接用{{ post.title }}、{{ post.body|linebreaks }}这样的变量即可。linebreaks过滤器会把正文里的换行符转换成p标签这是博客正文展示常用的处理方式。查询逻辑这块还有一个高频场景就是“某分类下的所有文章”。在View里加一个过滤就行def get_queryset(self): queryset super().get_queryset() category self.kwargs.get(category) if category: queryset queryset.filter(category__namecategory) return queryset.filter(is_publishedTrue)category__name的双下划线是Django ORM的跨表查询语法意思是“从关联的Category模型里按name字段过滤”。别看它只是个查询写法在新手阶段能不能熟练写出外键字段__关联表字段这种查询往往就决定了你能不能顺畅地用ORM完成复杂查询。3.3 分类、标签与评论的数据关联DjangoBlog的功能丰富度很大程度上取决于模型之间的关联设计。分类是一对多一个分类下多篇文章标签是多对多文章和标签互相交叉评论也是多对一一篇文章下有多个评论。评论的模型设计如下class Comment(models.Model): post models.ForeignKey(Post, on_deletemodels.CASCADE, related_namecomments) name models.CharField(称呼, max_length30) text models.TextField(评论内容) created_time models.DateTimeField(评论时间, auto_now_addTrue) def __str__(self): return self.text[:20] class Meta: ordering [-created_time]这里related_namecomments很关键。有了它在文章详情页查评论就变成了一行代码post.comments.all()。Django通过外键的反向关系自动生成了这个查询不用你手动写filter(postpost)。不指定related_name也行默认会生成post.comment_set.all()但显式声明可读性好很多。评论的提交逻辑在View层处理# 伪代码评论提交逻辑 def post_comment(request, post_id): post get_object_or_404(Post, pkpost_id) if request.method POST: form CommentForm(request.POST) if form.is_valid(): comment form.save(commitFalse) comment.post post comment.save() return redirect(post.get_absolute_url())提交评论时必须把评论和文章绑定否则一条评论不知道挂在哪篇文章下面。表单组件负责校验form.save(commitFalse)先把表单数据生成一个Comment实例但不写入数据库然后手动设置post字段再save()。这套流程我建议每个Django新手都亲手走一遍它是“表单-模型-外键”三者联动的标准动作。3.4 删除对象的正确姿势物理删除、级联删除与软删除热词里专门提到了“django执行查询-删除对象”这里的坑其实挺多值得单独展开。Django删除单条对象最简单的写法是post Post.objects.get(pk3) post.delete()这会立即从数据库里删除该记录。注意delete()方法是有返回值的它返回一个(总删除数, 各类型删除数明细)的元组因为外键关联的对象也会被一并删除。如果Post下挂着多条评论删Post的同时评论也会被级联删除——这就是我们在Model里设置的on_deletemodels.CASCADE在起作用。但实际开发中我强烈建议用软删除替代物理删除。博客文章删错了怎么办用户误删了评论怎么办物理删除后数据就真的没了根本找不回来。软删除的思路很简单给模型加一个is_deleted或is_published字段删除操作只是把状态置为不可见而不是真正从数据库移除。# 软删除的查询逻辑 class PostListView(ListView): model Post def get_queryset(self): # 默认过滤掉已删除/OFF状态的文章 return Post.objects.filter(is_deletedFalse)对于博客这种内容型系统管理后台里的删除操作最好也设计成软删除保证内容安全。物理删除留给运维和数据库层面处理。判断什么时候该硬删什么时候该软删标准很简单数据删了之后能否承受损失不能承受就软删。4. 新手常见问题与排查技巧实录4.1 模型改了但数据库没变这是出现频率最高的问题几乎每周都能看到有人问。症状是改了models.py里的字段但在Admin后台看不到新列或者查询时报no such column。原因九成是忘了跑迁移命令。Django的ORM不会自动检测模型变化你必须主动生成迁移文件并应用python manage.py makemigrations python manage.py migrate这两条命令要按顺序执行。makemigrations生成迁移脚本migrate把迁移脚本应用到数据库。改完模型后先跑第一步Django会提示你生成了新的迁移文件再执行第二步。如果改了字段后忘了第一步直接跑第二步Django会提示没有可应用的迁移这时候跑一下第一步就好。另外排查的一个技巧是看blog/migrations/目录里面应该有一个个按时间戳命名的迁移文件。如果你发现最近改动没有对应的迁移文件基本可以确定就是漏了makemigrations。4.2 模板变量不显示或报错模板渲染不显示数据排查链路其实很短。常见情况有三类。第一变量的名字写错了。模板里的{{ post.title }}必须在View的context里有post这个对象。用ListView时如果你没有设置context_object_nameDjango默认会用小写的模型名也就是post_list或post按这个来写模板准没错。第二没有把数据传到模板。函数视图必须显式返回render(request, 模板.html, {post: post})保证键名和模板变量名一致。第三模板路径写错。Django按照TEMPLATES配置里的DIRS和每个app的templates目录去查找模板。如果模板放在blog/templates/blog/post_list.html在View里就要写template_name blog/post_list.html因为Django在app的templates目录下还有一层blog目录作为命名空间。排查这类问题时我第一次推荐的做法是先在模板里打印整个context{{ debug }}或者临时输出变量的类型一般能很快定位到是变量名错了还是数据压根没传进来。4.3 静态文件和上传图片404博客需要在页面里展示CSS、JS和用户上传的图片404是经典问题。原因是Django在开发模式下处理静态文件需要额外配置。在settings.py里加上STATIC_URL /static/ STATICFILES_DIRS [ BASE_DIR / static, ] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media然后项目级的urls.py里开发环境下加上静态文件路由from django.conf import settings from django.conf.urls.static import static urlpatterns [ path(admin/, admin.site.urls), path(, include(blog.urls)), ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT) urlpatterns static(settings.STATIC_URL, document_rootsettings.STATICFILES_DIRS[0])模板里引用静态文件时要先在顶部加载静态文件标签{% load static %} img src{% static img/logo.png %} altlogo这套配置在Django项目里几乎是标配但新手容易漏掉后半段尤其是不在本地设置MEDIA路由导致上传的图片访问不了。在DEBUG模式下Django默认只处理static/前缀的静态文件上传的媒体文件路径映射需要手动加。4.4 查询性能篇N1问题的初步处理DjangoBlog的文章列表页每篇文章要显示作者、分类、标签。如果模板里对每篇文章都去查询关联数据就会产生N1次查询——先查N篇文章再对每篇查作者、查分类、查标签。文章少还好文章一多数据库压力立刻上来。解决方案是Django ORM的select_related和prefetch_relatedclass PostListView(ListView): model Post def get_queryset(self): # select_related处理外键prefetch_related处理多对多 return Post.objects.select_related(author, category).prefetch_related(tags).filter(is_publishedTrue)select_related适合处理外键关系它通过SQL的JOIN把关联数据一次性查出来。prefetch_related适合处理多对多关系它通过额外的查询把关联数据批量取回在Python层面组装。这两者的区别建议记牢外部键用select_related多对多用prefetch_related。用错了也不会报错但查询次数不会减少。写DjangoBlog这一步优化是必须的项目小的时候性能差异不明显但不写这个习惯一旦数据量上来再改每次排查数据库慢查询都会非常痛苦。4.5 关于MVT和GIS上MVT格式的澄清搜索这个词的时候发现了不少混淆有人把Django的MVT架构和GIS领域里的MVT格式混为一谈。这两者完全是两回事只是缩写碰巧一样必须说清楚。Django的MVT指Model-View-Template架构是本文讨论的核心。而GIS领域里的MVT是Mapbox Vector Tile的缩写是一种矢量瓦片格式常用于地图渲染配合Cesium、Mapbox这类前端地图引擎加载。“cesium加载MVT格式”指的是地图数据可视化场景跟Django的MVT架构没有任何关系。这个澄清不是凑字数。我见过有人在Django技术交流群里问“MwT架构怎么配合cesium加载”结果把两类完全无关的讨论搅在一起。做Django开发时看到MVT默认为架构做GIS开发时看到MVT默认为矢量瓦片语境不同含义完全不同。5. 进阶玩法用AI辅助Django开发与后续扩展方向5.1 用AI Agent辅助开发Django项目的实测体验“用ai agent开发django”这段时间很热我也试过几次说点真实感受。AI在Django开发中最擅长的是生成样板代码和解释报错信息。比如让它生成一个标准的CRUD视图、写一个带分页的ListView配置完成度很高几乎可以直接拿来用。但如果要做复杂的业务逻辑AI的表现就参差不齐了。它可能生成一个表面上合理的代码但忽略了外键关系、权限控制、事务处理这些关键细节。我见过AI生成的删除逻辑直接用了物理删除而当时的需求要求软删除它就完全没有考虑到。所以AI可以作为效率工具但最终代码能不能用必须你自己从头到尾理解一遍。我推荐的用法是把AI当作“高级搜索引擎”。遇到不懂的Django知识点让它给个带注释的示例代码遇到报错信息把完整traceback贴给它让它分析可能的原因。这两件事AI的准确率都很高。但如果让AI直接帮你从一个零散想法写出完整的DjangoBlog风险就很大——它会把很多基础假设藏在代码里出错时不容易定位。本质上用AI辅助开发的前提是你自己已经理解了Django的核心概念。MVT架构、ORM查询、模板渲染、路由配置这些基础打不牢AI生成的代码就是一堆看似能跑但随时可能崩的积木。5.2 从DjangoBlog出发可以扩展的方向DjangoBlog是一个极好的起点跑通之后可以往很多方向扩展我这里列出几个我觉得性价比最高的。第一个方向是加API接口。用Django REST Framework给博客做一套JSON API把文章列表、详情、评论通过API暴露出来。这会让你接触到序列化、视图集、路由注册、权限认证这些新概念是单页面应用、移动端博客客户端的必经之路。第二个方向是完善搜索。博客内容多了之后简单的icontains过滤已经够用但学习Elasticsearch或Whoosh全文搜索的话博客是最好的练手场景。搜索排序、高亮、分词这些技术点都是在真实数据上操作才有感觉。第三个方向是部署上线。本地跑通和线上运行完全是两码事。学一下用gunicorn Nginx部署Django项目配置环境变量处理静态文件收集设置MySQL数据库让博客真正能被其他朋友访问。这一步做完你才算完成了从开发到交付的闭环。第四个方向是加缓存。博客的读多写少特性非常适合用Redis做缓存。缓存文章列表、缓存热门文章排行理解缓存穿透和缓存一致性。这些经验在整个后端开发领域都是通用的。6. 一点小经验把DjangoBlog完整跑通之后才真正理解MVT最后分享一个我自己的感受。很多人学Django看文档能看懂但总觉得知识点是散的。模型、视图、模板、路由每个概念单独拿出来都明白合在一起就不知道怎么串起来。我的建议是把DjangoBlog从头跑一遍并且不要只满足于能跑。从models.py的第一个模型开始到Admin后台发布一篇文章然后在页面上看到这篇文章再到给文章加上分类和标签最后把评论功能加上。每一步都亲自动手观察数据在Model、View、Template之间是怎么流动的。这个过程做完你对“MVT架构”这四个字的理解就不再是文档里的概念而是实实在在的代码路径。以后遇到任何Django项目哪怕再复杂你都能快速找到入口先看数据模型再看路由和视图最后看模板展示。有一点需要养成习惯不要连续几个小时看教程。看半小时动手敲半小时遇到问题再回去查教程。Django的报错信息本身也是很棒的学习材料每次看到红色的traceback先别急着复制粘贴去搜答案自己读一遍猜一下问题出在哪个环节再动手解决。用DjangoBlog练手这个问题排查的功夫被反复锤炼长远看比“把所有功能都跑通了”更有价值。
返回列表