☰
Django官方教程后实战指南:ORM查询、Token认证与WebSocket推送
2026/9/30 7:42:24 网站建设 项目流程

1. 项目概述:官方教程跑通之后,真正的实战才刚刚开始

如果你刚把 Django 官方教程里那个投票应用从头到尾做完,可能会有一种微妙的错觉:觉得自己已经会 Django 了。但等你打开浏览器,看着那个勉强能用的后台和几个表单页面,又会很快清醒——距离一个能真正上线的项目,中间还隔着无数个“细节地狱”。这篇博文就是记录我完成官方教程之后,继续用这个底子去做真实项目的全过程。包含了我是如何整理目录结构、怎么理解 QuerySet 的增删改查、如何给接口补上 token 认证,以及最后怎么用 WebSocket 把后端数据实时推到浏览器前端。

这篇内容适合两类人:一类是刚做完官方教程、想继续往前走的新手,另一类是写过一点 Django 但总觉得基础不扎实、想系统梳理一下查询和认证细节的同学。我会把每一步背后的理由也讲清楚,而不是只丢结论。官方教程给的是骨架,这篇文章补的是血肉。

先说一个总体判断:官方教程的投票应用虽然简单,但它搭建的路是正路——MTV 分层、ORM 映射、模板渲染、Admin 后台、表单处理全都有了。问题在于教程把这些串得太顺,导致你容易忽略很多设计上的取舍。而在真实项目里,几乎每一步都要你亲自做决策。

2. 整体设计思路:先看懂官方教程在教什么,再思考怎么改

2.1 官方教程真正教会你的四件事

不要小看这个投票应用。它把 Django 的使用链路完整走了一遍:创建项目、创建应用、编写模型、迁移数据库、注册后台、写视图、配路由、用模板渲染数据、处理表单提交。这套链路就是你未来所有 Django 项目的基本盘。官方教程还有一个容易被忽略的细节:它对 URL 命名使用了name参数,在模板中用{% url %}反向解析路径——这个习惯很多人做项目时反而会丢掉,喜欢硬编码 URL,结果改路由之后满屏报错。

另外教程里的self.question_text[:50]这类__str__魔方法写法也值得学到。后台列表页显示的不是对象内存地址,而是有意义的描述文本,全靠这个。我后来在真实项目里维护十几个模型,每次都会庆幸当初养成了给模型写__str__的习惯。

2.2 从 demo 到项目:目录结构先重组

跑完教程后我做的第一件事不是急着加功能,而是重新检查项目根目录。官方教程默认生成的结构里,polls应用和mysite项目配置放在同一层。小 demo 没问题,但真实项目一般会调整成apps目录统一管理多个子应用,再配一个common或utils目录放通用模块。

我的做法是建一个apps文件夹,把业务应用全部移进去,然后在settings.py里加一行:

import sys from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent sys.path.insert(0, str(BASE_DIR / "apps"))

这样INSTALLED_APPS里写"user_app"、"blog_app"就能直接识别,不用写一长串相对路径。看起来是小改动,但对后续项目扩展很关键。你要是用 PyCharm 打开项目,记得把apps目录标记为 Sources Root,不然 IDE 会提示找不到模块。

2.3 视图层选型:函数视图还是类视图

官方教程用的是函数视图(FBV),写起来直观,适合理解请求处理流程。但真实项目里我大量使用类视图(CBV),尤其是ListView、DetailView、CreateView这一组。核心原因是 CBV 把常见的 CRUD 流程抽象成了类属性,代码量大幅减少。

不过我的建议是:新手时期先用函数视图写几个页面,把手动获取数据、渲染模板、处理 POST 请求的完整流程走一遍。因为 CBV 封装太狠,很容易出现“代码能跑但完全不知道发生了什么”的情况。等 FBV 手感有了,再切 CBV 就会觉得是在简化工作而不是黑魔法。

具体到一个页面怎么选?我的习惯是:

  • 需要自定义复杂业务逻辑、多步骤表单、依赖外部 API 的——用 FBV,好调试。
  • 纯数据库读取、模板展示、权限控制简单的——用 CBV,代码少,且自带get_queryset扩展点。

有同学说 CBV 不好调试,其实django.views.generic的源码并不难读,花半小时把View、TemplateView、ListView三个类看一遍,你会打开新世界。

3. 核心细节解析:创建 app 的正确姿势与 settings 配置避坑

3.1 一条命令背后发生了什么

python manage.py startapp myapp是每个 Django 新手最先接触的命令之一。它会在当前目录下生成一个包含models.py、views.py、admin.py、apps.py、migrations/等文件的包结构。但很多人没注意到,这个命令只是生成文件,不会帮你注册到项目。

你需要去settings.py的INSTALLED_APPS里手动添加应用名,Django 才知道要同步这个 app 的数据表、读取它的模板和静态文件、加载它的路由。如果漏了这一步,你migrate时会发现数据表没有创建,python manage.py startapp生成的migrations/目录里也没有任何迁移记录——不是命令坏了,是你根本没告诉项目这个应用的存在。

另一个坑是 app 名称与 Python 包名冲突。我见过有人把一个内部业务应用命名为test,结果导入test.py模块时和 Python 内置测试库撞车。遇到奇怪导入错误时,先怀疑你的命名有没有撞标准库。

3.2 settings.py 里最容易翻车的三个配置项

第一是STATIC_URL和静态文件目录。官方教程只用了少量内联样式,真实项目里 CSS、JS、图片都需要单独管理。推荐在项目根目录建static/,在settings.py中配置:

STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']

开发环境这样够了。部署时的collectstatic是另一个大坑,后面会提。

第二是TEMPLATES里的DIRS。虽然 Django 默认会去每个 app 下的templates/找模板,但如果你想放全局模板(比如base.html、通用 404 页面),就要在DIRS里加上全局模板目录。否则模板继承时extends "base.html"会报找不到模板。

第三是ALLOWED_HOSTS。本地开发写空列表没问题,但一旦部署到服务器,不把域名或 IP 加进去,访问就会得到Bad Request (400)。这个报错新手几乎必遇一次。

3.3 官方教程没讲的 app 划分逻辑

真实项目里一个常见问题是:一个models.py里堆了太多模型。官方教程只有一个Question和Choice,看不出问题。但当你做一个博客系统时,如果用户、文章、评论、标签、友链全塞在一个 app 里,代码会迅速失控。

推荐思路是按业务边界切分:

  • user_app:用户扩展信息、登录注册相关逻辑
  • blog_app:文章、分类、标签
  • comment_app:评论、点赞

每个 app 有自己的models.py、views.py、urls.py,应用之间通过外键或明确接口关联。你要知道 Django 的 app 不是微服务,项目内部它们共享同一个数据库,但代码边界清晰能让你后期维护时少掉头发。官方教程里没有讲这套组织方法论,但这恰恰是实战结果里最有价值的部分。

4. 查询与删除对象:ORM 实操里那些教程没强调的细节

4.1 懒加载:为什么查完数据库没被执行

官方教程里写了Question.objects.all()和filter(),但没有明确告诉你 QuerySet 是惰性的。简单说:Question.objects.filter(pub_date__year=2024)这一行不会立刻打 SQL,只有当你真正“使用”这个结果时(遍历它、取长度、判断是否存在),Django 才去数据库查询。

这意味着两件事。第一,你可以链式叠加过滤条件,最后只执行一次查询:

qs = Question.objects.filter(pub_date__year=2024) qs = qs.filter(question_text__contains="投票") qs = qs.order_by("-pub_date")

第二,如果你不小心把 QuerySet 存起来,在模板或视图里多次使用它,每次使用都可能触发新查询。比如在if判断里用一次,在 for 循环里又用一次,底层就是两次 SQL。小项目没问题,数据量大了以后这就是性能隐患。解决办法是及时list()转成列表,或者在循环外先取一次。

4.2 删除对象:delete() 到底删了什么

官方教程的投票应用没有删除功能,所以很多新手对删除只有model.objects.get(id=1).delete()这一步的理解。这个理解太浅了。

delete()会真正从数据库里删掉这一行数据。如果你是ForeignKey的“一”方,被删后与之关联的“多”方会根据on_delete参数行动。官方教程的Question和Choice之间用的就是级联删除(models.CASCADE),删掉问题,选项会被一起带走。

但在真实项目里,直接物理删除很多时候不是好选择。比如用户注销账号,你真的希望把用户的所有评论、订单历史全删干净吗?正常做法是软删除:给模型加一个is_deleted字段或者deleted_at时间字段,删除时只更新这个标记,查询时默认过滤掉已标记的。这样数据还在,需要审计时能查到,想恢复也容易。

我这里给你一个软删除常用的设计思路:

class Article(models.Model): title = models.CharField(max_length=200) content = models.TextField() is_deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True)

查询时这样过滤:

Article.objects.filter(is_deleted=False)

再用自定义管理器把这条规则固化:

class ArticleManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_deleted=False)

之后所有业务代码里写Article.objects.all()就自动排除已删除数据。这是我从实际项目里体会最深的一个模式:ORM 的默认查询入口就是软删除过滤层,比你在每个视图里手动加filter(is_deleted=False)可靠得多。

4.3 实战里最有用的几个 QuerySet 技巧

values()和values_list()是官方教程没教但实战高频使用的。当你只需要模型里的几个字段,不想加载整个对象时,用values()返回字典列表,values_list()返回元组列表。这样省内存、省时间,适合导出报表或 AJAX 接口。

Question.objects.values("id", "question_text") # 输出: [{'id': 1, 'question_text': '今天吃什么?'}, ...]

select_related和prefetch_related是用来处理关联对象查询性能的。简单来说,select_related适合“一对一”和“外键”这种单条关联,一次 JOIN 查出来;prefetch_related适合“多对多”和“反向外键”,先查主表再批量查关联表。官方教程里看不到这两者的必要,因为数据少,等你页面一卡,通常就是 N+1 查询在作怪。

F 表达式我也提一下。它用于在查询中引用字段值本身,不查出来再赋值,直接由数据库完成运算。比如给阅读量加一:

from django.db.models import F Article.objects.filter(id=1).update(read_count=F("read_count") + 1)

这个路径不会出现并发时先读后写导致计数丢失的问题。

5. 登录认证与 Cookie Token 设置:给投票应用补上用户体系

5.1 官方教程留的坑:登录只做了一半

官方教程的投票应用没有用户注册和登录,只有一个 Admin 后台的登录。Admin 登录是 Django 内置的,开箱即用。但真实项目需要用户在站前端登录、注册,并且后续请求能识别“你是谁”。

Django 内置的django.contrib.auth包含完整的认证框架:用户模型、会话、权限。官方教程没讲透的是它如何在 HTTP 无状态请求里维持登录状态——靠的是 Session,默认存在数据库的django_session表中,浏览器记住的是 sessionid 这个 Cookie。

方案一:保持使用服务端 Session。对于小型项目,这是最省事的,安全性也有保证。你在视图里写request.user.is_authenticated就能判断是否登录。

方案二:前端需要频繁访问这些接口但不想每次都带 CSRF token、不想依赖浏览器 Cookie 同源策略时,就要考虑 token 认证。这个方法在前后端分离项目里用得最多。

5.2 用 Cookie 设置 token 的完整做法

这里用一个实战思路:用户登录成功后,我生成一个 token 写入数据库(或缓存),同时通过response.set_cookie()把它写到浏览器。

为什么要用 Cookie 而不是让前端把 token 存到 localStorage?因为 localStorage 的 token 很容易被第三方脚本通过 XSS 窃取;Cookie 设置了HttpOnly后,JavaScript 读不到它的内容,能有效降低 XSS 风险。

简单实现如下:

import uuid from django.http import JsonResponse from django.contrib.auth import authenticate def login_view(request): if request.method == "POST": username = request.POST.get("username") password = request.POST.get("password") user = authenticate(request, username=username, password=password) if user is not None: token = uuid.uuid4().hex # 存到缓存,有效期 7 天 cache.set(f"auth_token:{token}", user.id, timeout=7 * 24 * 3600) response = JsonResponse({"code": 0, "msg": "ok"}) response.set_cookie( "auth_token", token, httponly=True, samesite="Lax", max_age=7 * 24 * 3600, secure=False, # 生产环境记得改为 True,走 HTTPS path="/", ) return response return JsonResponse({"code": 1, "msg": "用户名或密码错误"})

取用户时写一个简单的认证后端,或者直接在视图中解析 Cookie:

from django.core.cache import cache from django.contrib.auth.models import User def get_login_user(request): token = request.COOKIES.get("auth_token") if not token: return None user_id = cache.get(f"auth_token:{token}") if not user_id: return None return User.objects.filter(id=user_id).first()

这段代码我已经在多个项目里用过,足够简单,也够用。如果要更标准,可以把这部分包成基于AuthenticationMiddleware的认证后端,但核心逻辑就是上面这些。

5.3 CSRF 和 CORS:两个新手容易混的问题

官方教程里的表单都要写{% csrf_token %},这是 Django 的 CSRF 防护。但如果你用第三方 API 工具(Postman、Apifox)测试 POST 接口,经常会报 CSRF 验证失败。开发阶段可以先豁免,生产环境建议保留。

当你做前后端分离时,登录接口从另一个域名或端口发出请求时,就会有跨域问题,这由 CORS 配置管辖。简单说:同源请求靠 Cookie,跨域请求需要在响应头中告诉浏览器允许哪些域名访问。Django 没有内置 CORS 处理,通常装django-cors-headers。配置如下:

INSTALLED_APPS = [ ... "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", ... ] CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", ]

我见过很多人把 CSRF 和 CORS 搞混:CSRF 防的是伪造请求,CORS 管的是浏览器同源策略导致的跨域权限。互相之间有联系,但不是一个东西。排查问题时先分清你遇到的到底是谁。

6. 用 WebSocket 实现后台数据推送:让页面不再等人刷新

6.1 为什么需要 WebSocket,轮询到底差在哪

传统 HTTP 请求是请求-响应模型:前端发起请求,后端返回结果。如果后台数据变化了想推给前端,通常只能让前端隔几秒轮询一次接口。这种方案在小规模场景能用,但有几方面局限性:实时性差(几秒延迟)、浪费资源(大部分轮询请求都是空回来的)、服务端压力大(频繁建连、频繁查询)。

WebSocket 解决的是全双工通信的问题:前端和服务器建立起一条长连接后,服务器可以随时主动推送消息给前端,前端也可以随时发消息给服务器。适合实时通知、在线状态、数据看板、聊天消息这类场景。

Django 原生不支持 WebSocket,因为它走的是 HTTP 的 WSGI 协议。要支持 WebSocket,得切换到异步——这就是 Django Channels 的用途。Channels 基于 ASGI,可以同时处理 HTTP 和 WebSocket。

6.2 用 Django Channels 实现后台推送

这里我以一个“后台数据变化后,前端实时收到通知”的场景为例。

第一步,安装 Channels:

pip install channels

第二步,把项目从 WSGI 模式转换成 ASGI 模式。找到asgi.py文件,改成这样:

import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path from your_app.consumers import NotificationConsumer os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter([ path("ws/notifications/", NotificationConsumer.as_asgi()), ]), })

第三步,编写消费者。消费者相当于 WebSocket 的视图,负责处理连接建立、收到消息、断开连接:

from channels.generic.websocket import AsyncJsonWebsocketConsumer class NotificationConsumer(AsyncJsonWebsocketConsumer): async def connect(self): await self.accept() # 可以把连接分组,方便后台定向推送 await self.channel_layer.group_add("notifications", self.channel_name) async def disconnect(self, close_code): await self.channel_layer.group_discard("notifications", self.channel_name) async def notify(self, event): # 后台调用 group_send 时会触发这个方法 await self.send_json({ "message": event["message"], })

第四步,在后台任意位置推送消息:

from channels.layers import get_channel_layer from asgiref.sync import async_to_sync channel_layer = get_channel_layer() def push_notification(message): async_to_sync(channel_layer.group_send)( "notifications", { "type": "notify", "message": message, } )

这样只要调用push_notification("有新订单"),前端就能实时收到消息。

6.3 前端接入与踩坑实录

前端用原生 JavaScript 接入很简单:

const socket = new WebSocket("ws://localhost:8000/ws/notifications/"); socket.onmessage = function(event) { const data = JSON.parse(event.data); console.log("收到推送:", data.message); };

但我实际使用中踩过几个坑,值得记下来。

第一个坑是通道层配置。生产环境 Channels 需要 Redis 作为通道层,开发环境可以只用 InMemory 通道层:

CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer", }, }

但 InMemory 只在单进程、单 worker 下有效。一旦你用多个 worker,推送消息很可能发不到你连接所在的那个进程。生产环境务必换 Redis,我最早上线时忘了换,结果推送消息时好时坏,排查了大半天。

第二个坑是async_to_sync。普通 Django 视图是同步的,调用 Channels 的异步方法要包一层async_to_sync。很多人第一次写的时候漏掉这个,会报SynchronousOnlyOperation错误。

第三个坑是部署时的 ASGI 服务器选择。开发阶段用uvicorn或者 Daphne 都可以,生产环境也需要对应配置反向代理支持 WebSocket 升级。Nginx 层需要设置Upgrade头,这个在常规 HTTP 配置里不会自带,忘了配置的话客户端永远连不上 wss。

第四个坑是连接鉴权。WebSocket 连接建立时如何知道用户身份?我常在connect()中通过查询字符串或 Cookie 拿到 token,再解析用户。需要注意 WebSocket 的握手阶段没有普通视图的request,但 Channels 提供了一个scope对象,里面带着 Cookie 等 HTTP 会话信息。简单示例如下:

from django.contrib.auth.models import AnonymousUser async def connect(self): cookies = self.scope.get("cookies", {}) token = cookies.get("auth_token") # 解析 token,找到 user,存到 self.scope["user"] 中 self.scope["user"] = await get_user_by_token(token)

7. 常见问题与排查技巧:实战中踩过的那些坑

7.1 新手必踩的五个坑

官方教程之后的独立开发,最容易踩的坑我列一下,都是真实项目中多次见到的。

第一个是静态文件 404。本地开发时DEBUG=True的情况下静态文件通常没问题,但部署后DEBUG=False时,Django 不再帮你处理静态文件,需要collectstatic收集到指定目录并交给 Web 服务器托管。很多人部署后页面样式全丢,就是漏了这一步。

第二个是数据库迁移冲突。多人协作时经常出现,你本地改了模型,同事也改了同一个模型,两边各自生成迁移文件,合并时就会有冲突。不要怕,Django 有内置合并命令:

python manage.py makemigrations --merge

第三种是时区问题。USE_TZ = True时,Django 存储和处理的都是带时区的时间,如果你在前端直接用datetime.now()跟模型里的时间比较,容易差 8 小时。统一使用django.utils.timezone.now()。

第四种是query.get()的异常处理。get()匹配不到数据会抛DoesNotExist,匹配到多条会抛MultipleObjectsReturned。如果你不确定数据唯一性,最好用 try-except 包住,或者用filter().first()。

第五种是表单验证的坑。官方教程里的form.is_valid()看起来简单,但实际项目里你往往需要自定义验证逻辑。一个常见误区是在clean()里直接改self.cleaned_data,但忘了cleaned_data可能还没有字段值。正确做法是逐个字段定义clean_字段名()方法。

7.2 排查问题的一个固定顺序

我调 Django 问题一般遵循这个顺序:

  1. 看浏览器 Network 面板,确认请求 URL、状态码、响应体。很多时候问题不在后端,而是前端没发对请求。
  2. 看后端日志。Django 开发服务器的终端输出会打印 SQL 和异常堆栈,这是定位问题的宝藏。
  3. 如果异常信息不明,用python manage.py shell复现。可以手动执行模型查询和函数调用,比在视图里盲目打日志快得多。
  4. 模型、视图逻辑都没问题时,再怀疑配置。检查settings.py里的INSTALLED_APPS、中间件顺序、URL 路由。

这套顺序帮我排掉过六成以上的问题。真正复杂的往往不是技术,而是数据问题——比如旧数据的脏数据导致新逻辑报错,这种只能靠写脚本清洗。

7.3 调试技巧:少走冷门弯路

print()大法虽然不高级,但配合python manage.py shell真的很实用。我经常把要验证的代码片段直接粘进 shell 里跑,省去了反复重启开发服务器的时间。

另外一个冷门技巧是django-debug-toolbar。它会在页面侧边显示 SQL 查询数量、耗时、模板加载情况,对性能优化很有帮助。装上之后你会发现一个页面居然查了几十次数据库——这就是 N+1 问题的证据,然后用select_related和prefetch_related修复。

关于日志,建议早点用标准库的logging而不是print。配置一个文件日志处理器,把异常写入 logs 文件,对排查线上问题是长期投资。Django 有个内置的django.request日志器,配置好以后 5xx 错误会自动记录请求路径和异常信息。

8. 扩展方向与我的个人体会

官方教程只是一个开始,这句话很多人说,但实践之后才能体会到它的分量。拍卖完投票应用之后,如果你想继续深入,我建议按这样的顺序来:先做一个带用户注册登录的简单内容发布系统,把用户体系、session、表单验证、soft delete 都用进去;然后做接口化改造,试着用 JsonResponse 返回数据,接入 token 认证;最后再碰 WebSocket 和异步任务,做实时通知或者后台一键推送。

我个人的实际体会是,Django 的坑多数不在框架本身,而在那些你没理解透的概念上——懒惰查询、时区、CSRF、CORS、ASGI 和 WSGI 的区别。每一个概念初看都很简单,但组合起来之后,问题会变得诡异。这篇记录里很多内容也是我自己踩坑之后才想明白的。希望你看完能少走一些弯路。

最后分享一个小技巧:官方教程的投票应用项目不要删,把它当成一个实验场。你学到的新东西,比如 token 登录、WebSocket、类视图,都可以先在这个小项目里试一遍。小项目不会因为搞挂而有负担,但你能完整地跑通整个链路,这个经验积累对下一个正式项目非常宝贵。

如果你是刚把官方教程做完,先别急着找更复杂的教程。把投票应用从“能跑”改到“像样”:加上用户注册、让用户可以投票后看到结果、给后台加上一些筛选功能。这个改造过程,比看十个新教程都更有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询