☰
Django工程创建与Models实战:从零构建图书管理系统
2026/10/2 10:03:56 网站建设 项目流程

1. 项目概述:为什么从Django工程创建和models入手,是后端开发绕不开的第一道门槛

你刚装好Python,pip install django成功,终端里敲下django-admin startproject mysite,回车——然后呢?文件夹里一堆py文件,settings.py密密麻麻几百行,manage.py像黑盒子,init.py空着却不能删……很多人卡在这一步,不是不会敲命令,而是根本不知道每个文件在系统里扮演什么角色。这就像拿到一辆新车钥匙,能点火,但不知道油箱在哪、雨刷怎么调、ESP开关藏在哪。Django的“工程创建”从来不只是执行一条命令,它是一套预设好的骨架结构,背后对应着Web应用最核心的分层逻辑:配置层(settings)、入口层(manage.py/wsgi.py/asgi.py)、路由层(urls.py)、模型层(models.py)——而models,正是整个骨架的心脏。

我带过几十个转行学员,90%的人第一次写models时栽在同一个地方:把数据库字段当Python变量用,写完class Student(models.Model): name = models.CharField()就以为完事了,结果makemigrations报错说max_length没填;或者直接在shell里Student.objects.create(name="张三"),发现数据库里name字段存的是乱码;更常见的是,改了models字段类型后,migrate死活不生效,最后只能删库重来。这些都不是bug,是Django在用强约束逼你建立“数据契约意识”——每个字段定义,本质是在和数据库签一份合同:这个字段叫什么、占多少空间、是否允许为空、默认值是什么、有没有唯一性要求。contract一旦签错,后续所有增删改查都会出问题。

所以这篇内容不讲“Django有多牛”,也不堆砌源码分析,而是完全按一个真实项目启动流程来组织:从终端敲下第一个命令开始,到你在Python shell里亲手完成一条记录的新增、查询、修改、删除为止。过程中你会看到,为什么settings.py里DATABASES要配mysqlclient而不是pymysql;为什么makemigrations生成的0001_initial.py文件里,字段定义和你写的models.py不完全一样;为什么admin界面能自动渲染表单,但前端页面却要手动写HTML模板。所有这些,都源于Django对“约定优于配置”原则的极致贯彻——它不阻止你自由发挥,但会用清晰的错误提示告诉你:这里该走哪条路。适合谁看?刚学完Python基础、想真正跑通第一个Web项目的新人;写了几年Flask或FastAPI、想理解Django设计哲学的开发者;还有那些被线上项目models频繁变更搞崩溃、急需理清迁移逻辑的在职工程师。核心关键词Python、Django、models、增删改查、工程创建,每一个都会在实操中落到具体文件、具体命令、具体报错信息上,而不是飘在概念层面。

2. 工程创建与目录结构解析:从django-admin startproject到可运行服务的完整链路

2.1 创建工程的两种方式及适用场景选择

Django提供两种工程创建入口:django-admin和python manage.py。初学者常混淆二者,其实它们分工明确。django-admin是Django安装后全局可用的命令行工具,负责“冷启动”——即在没有任何Django项目存在时,生成最原始的工程骨架。而manage.py是每个Django项目根目录下自动生成的本地脚本,它封装了当前项目的配置上下文,所有后续操作(如运行服务器、执行迁移、启动shell)都必须通过它。

提示:永远不要用django-admin runserver启动项目。它会因找不到settings模块而报错。正确姿势是cd进入项目根目录后,执行python manage.py runserver。

我们以实际项目为例:假设要开发一个“图书管理系统”,工程名定为bookstore。执行:

django-admin startproject bookstore .

注意末尾的英文句号“.”——这是关键细节。它表示将工程文件直接生成在当前目录,而非新建一层bookstore子目录。很多教程省略这点,导致新手在PyCharm里打开项目时,发现manage.py不在根目录,Django插件无法识别项目结构。如果你漏掉句号,会生成bookstore/bookstore/manage.py的嵌套结构,此时需手动把内层bookstore目录里的所有文件(包括manage.py)剪切到外层,再删除空的内层bookstore文件夹。

另一种创建方式是先用venv创建虚拟环境,再用pip安装Django后执行:

python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows pip install django django-admin startproject bookstore .

这种方式更安全,避免全局Python环境被污染。我建议所有正式项目都采用此流程,哪怕只是本地练习。因为Django不同版本对Python解释器有严格要求(如Django 4.2要求Python 3.8+),虚拟环境能彻底隔离依赖冲突。

2.2 标准工程目录结构逐层拆解

执行完startproject后,当前目录下会出现以下文件和文件夹:

bookstore/ ├── manage.py ├── bookstore/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── asgi.py │ └── wsgi.py
  • manage.py:项目操作中枢。它内部做了三件事:1)设置DJANGO_SETTINGS_MODULE环境变量指向bookstore.settings;2)加载Django配置;3)根据传入的子命令(如runserver、migrate)调用对应模块。你可以把它理解成Django项目的“遥控器”,所有功能都通过它触发。

  • bookstore/init.py:标识该目录为Python包。内容为空,但不可或缺。没有它,Python解释器无法将bookstore识别为可导入模块,后续import bookstore.settings会失败。

  • bookstore/settings.py:项目配置总控台。这里定义了数据库连接、静态文件路径、中间件列表、安全密钥等核心参数。新手最容易犯的错误是直接修改DEBUG=True上线,或硬编码SECRET_KEY。正确的做法是:开发环境保持DEBUG=True便于调试;生产环境必须设为False,并通过环境变量注入SECRET_KEY(如os.environ.get('DJANGO_SECRET_KEY'))。

  • bookstore/urls.py:URL路由总入口。它不直接处理请求,而是将URL模式分发给各App的urls.py。主urls.py通常只保留管理后台(/admin/)和API根路径(/api/)的路由,其他业务路由全部下沉到独立App中。这种设计让项目具备天然的模块化能力——比如图书管理、用户管理、订单管理可分别作为三个App,互不影响。

  • bookstore/asgi.py与wsgi.py:应用服务器网关接口。wsgi.py用于传统同步服务器(如Gunicorn),asgi.py支持异步处理(如WebSocket、长连接)。对于纯HTTP CRUD接口,wsgi足够;若需实时通知(如新书上架推送),则需启用ASGI。

注意:Django 4.0+默认启用ASGI,但大部分初学者项目仍用WSGI部署。两者共存不冲突,只需在部署时指定对应入口文件即可。

2.3 数据库配置实战:MySQL client安装与settings.py关键参数详解

Django默认使用SQLite,轻量但无法支撑高并发。真实项目必选MySQL或PostgreSQL。以MySQL为例,配置前需解决两个前置问题:

第一,安装mysqlclient驱动
这是Django连接MySQL的官方推荐驱动,比PyMySQL性能更高且兼容性更好。安装命令:

pip install mysqlclient

但Windows用户常遇到编译失败。根本原因是缺少Microsoft Visual C++ Build Tools。解决方案:下载预编译wheel包。访问https://www.lfd.uci.edu/~gohlke/pythonlibs/#mysqlclient,根据你的Python版本(如cp39)和系统架构(win_amd64)下载对应whl文件,然后:

pip install mysqlclient‑2.1.1‑cp39‑cp39‑win_amd64.whl

第二,配置settings.py中的DATABASES
在bookstore/settings.py中找到DATABASES字典,替换为:

DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'bookstore_db', # 数据库名,需提前在MySQL中创建 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', # 本地连接用127.0.0.1,不用localhost(避免socket连接) 'PORT': '3306', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", 'charset': 'utf8mb4', }, 'TEST': { 'CHARSET': 'utf8mb4', 'COLLATION': 'utf8mb4_unicode_ci', } } }

关键参数说明:

  • HOST用127.0.0.1而非localhost:MySQL在localhost下默认走Unix socket连接,而Django驱动强制走TCP/IP。用IP地址可避免连接异常。
  • OPTIONS中init_command:强制开启严格模式,防止插入超长字符串时被静默截断(如CharField(max_length=10)存入15位字符)。
  • charset设为utf8mb4:支持emoji和四字节UTF-8字符。MySQL旧版utf8仅支持三字节,存微信昵称“𠮷野家”会报错。
  • TEST配置:单元测试时自动创建测试数据库,避免污染主库。

配置完成后,执行python manage.py dbshell可直接进入MySQL命令行验证连接。若报错“Unknown database 'bookstore_db'”,需先登录MySQL手动创建:CREATE DATABASE bookstore_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

3. Models定义与数据库迁移:从Python类到物理表的映射原理与避坑指南

3.1 创建App与Models定义的规范流程

Django强调“功能模块化”,所有业务逻辑必须放在独立App中。创建图书管理App的命令:

python manage.py startapp books

执行后生成books/目录,包含models.py、views.py等文件。此时需做两件事:

  1. 将'app'注册到settings.py的INSTALLED_APPS列表:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'books', # 新增这一行 ]
  1. 在books/models.py中定义数据模型。以图书信息为例:
from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class Author(models.Model): name = models.CharField(max_length=100, verbose_name="作者姓名") birth_date = models.DateField(null=True, blank=True, verbose_name="出生日期") def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=200, verbose_name="书名") isbn = models.CharField(max_length=13, unique=True, verbose_name="ISBN号") price = models.DecimalField( max_digits=6, decimal_places=2, validators=[MinValueValidator(0)], verbose_name="定价" ) author = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='books', verbose_name="作者" ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") class Meta: verbose_name = "图书" verbose_name_plural = "图书" ordering = ['-created_at'] def __str__(self): return self.title

这段代码看似简单,实则暗含多个设计决策:

  • verbose_name:控制admin界面和表单的显示名称,中文友好必备。
  • null=True, blank=True:前者影响数据库字段是否允许NULL,后者影响Django表单是否允许空提交。两者常被混用,需严格区分。
  • ForeignKey的on_delete:Django 2.0+强制要求指定。models.CASCADE表示删除作者时级联删除其所有图书;models.PROTECT则阻止删除操作。
  • Meta类ordering:定义默认排序规则,避免每次查询都写order_by()。

3.2 迁移文件生成原理与手动编辑技巧

执行python manage.py makemigrations后,Django会在books/migrations/目录下生成0001_initial.py文件。打开它,你会看到:

operations = [ migrations.CreateModel( name='Author', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=100, verbose_name='作者姓名')), ('birth_date', models.DateField(blank=True, null=True, verbose_name='出生日期')), ], options={ 'verbose_name': '作者', 'verbose_name_plural': '作者', }, ), ]

注意两点:

  1. Django自动添加了id主键字段:即使models.py中未声明,Django也会为每个Model添加自增整数主键。若需自定义主键(如用UUID),需显式声明id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)。
  2. 字段定义与models.py不完全一致:如birth_date字段在models.py中是null=True, blank=True,而迁移文件里拆分为blank=True, null=True。这是因为Django内部将校验逻辑(blank)与数据库约束(null)分离处理。

迁移文件本质是“数据库操作指令集”,可手动编辑。例如,你想把Author.name字段的max_length从100改为150,直接修改models.py后执行makemigrations,会生成新迁移文件。但若想合并到初始迁移中(避免线上迁移步骤过多),可删除0001_initial.py,修改models.py后再重新makemigrations —— 前提是数据库尚未migrate,且无历史数据。

实操心得:团队协作中,严禁手动修改已提交到Git的迁移文件。若多人同时修改同一Model,Django会生成依赖型迁移(如0002_auto_20230101_1200.py依赖0001_initial.py)。此时应执行python manage.py makemigrations --empty books创建空迁移,再手动编写合并逻辑。

3.3 执行迁移与数据库状态验证

生成迁移文件后,执行:

python manage.py migrate

Django会按数字顺序执行所有未应用的迁移文件,并在数据库中创建django_migrations表记录执行状态。此时检查MySQL:

USE bookstore_db; SHOW TABLES; DESCRIBE books_book;

会看到books_book表已创建,字段与Book模型完全对应,且author_id字段为外键,关联到books_author表。

关键验证点:

  • 外键约束是否生效:尝试插入author_id不存在的图书记录,MySQL应报错Cannot add or update a child row: a foreign key constraint fails。
  • 时间字段自动填充:在shell中执行Book.objects.create(title="测试书", isbn="1234567890123", price=59.99, author_id=1),查看created_at和updated_at是否自动写入当前时间。
  • 唯一性约束:重复插入相同ISBN的图书,Django会抛出IntegrityError异常。

若migrate失败,常见原因有:

  • MySQL用户无建表权限:GRANT ALL PRIVILEGES ON bookstore_db.* TO 'root'@'localhost'; FLUSH PRIVILEGES;
  • 字段名含Python关键字:如定义class Order(models.Model): pass,Order是Python内置函数,会导致语法错误。应改用BookOrder等名称。

4. 增删改查(CRUD)全流程实现:从Django Shell到视图函数的完整闭环

4.1 Django Shell交互式操作:快速验证模型逻辑

Django Shell是调试模型的黄金工具,比写View再启服务快十倍。启动命令:

python manage.py shell

进入交互环境后,导入模型并操作:

# 导入模型 >>> from books.models import Author, Book # 创建作者(新增) >>> author = Author.objects.create(name="鲁迅", birth_date="1881-09-25") >>> author.id 1 # 创建图书(新增,关联作者) >>> book = Book.objects.create( ... title="呐喊", ... isbn="9787020000000", ... price=35.00, ... author=author ... ) >>> book.id 1 # 查询所有图书(查询) >>> Book.objects.all() <QuerySet [<Book: 呐喊>]> # 条件查询(查询) >>> Book.objects.filter(price__gt=30) # 价格大于30 <QuerySet [<Book: 呐喊>]> >>> Book.objects.get(isbn="9787020000000") # 精确匹配,不存在则抛异常 <Book: 呐喊> # 修改(更新) >>> book.price = 39.99 >>> book.save() # 必须调用save()才写入数据库 # 删除(删除) >>> book.delete() # 返回(1, {'books.Book': 1}),表示删除1条Book记录

这里揭示Django ORM的核心机制:

  • 惰性查询(Lazy Evaluation):Book.objects.all()不立即执行SQL,只返回QuerySet对象。只有遍历(for循环)、切片([:5])、转列表(list())时才真正查询数据库。
  • save()的双重作用:对新对象,save()执行INSERT;对已有对象,save()执行UPDATE。若想强制更新特定字段(避免覆盖其他字段),用book.save(update_fields=['price'])。
  • get() vs filter():get()返回单个对象,查不到抛DoesNotExist异常,查到多条抛MultipleObjectsReturned异常;filter()始终返回QuerySet(可能为空)。

注意:在Shell中修改对象属性后,必须调用save()。直接赋值book.price = 39.99只是内存操作,数据库无变化。

4.2 视图函数实现CRUD:从函数式视图到类视图的演进

4.2.1 函数式视图(Function-Based View)

在books/views.py中编写:

from django.shortcuts import render, get_object_or_404, redirect from django.http import HttpResponse from .models import Book, Author def book_list(request): """图书列表页""" books = Book.objects.all().select_related('author') # 预加载作者信息,避免N+1查询 return render(request, 'books/list.html', {'books': books}) def book_detail(request, book_id): """图书详情页""" book = get_object_or_404(Book, id=book_id) # 自动处理404 return render(request, 'books/detail.html', {'book': book}) def book_create(request): """新增图书""" if request.method == 'POST': title = request.POST.get('title') isbn = request.POST.get('isbn') price = request.POST.get('price') author_id = request.POST.get('author_id') Book.objects.create( title=title, isbn=isbn, price=price, author_id=author_id ) return redirect('book_list') else: authors = Author.objects.all() return render(request, 'books/create.html', {'authors': authors}) def book_update(request, book_id): """修改图书""" book = get_object_or_404(Book, id=book_id) if request.method == 'POST': book.title = request.POST.get('title') book.isbn = request.POST.get('isbn') book.price = request.POST.get('price') book.author_id = request.POST.get('author_id') book.save() return redirect('book_detail', book_id=book.id) else: authors = Author.objects.all() return render(request, 'books/update.html', {'book': book, 'authors': authors}) def book_delete(request, book_id): """删除图书""" if request.method == 'POST': book = get_object_or_404(Book, id=book_id) book.delete() return redirect('book_list')

对应URL配置(books/urls.py):

from django.urls import path from . import views urlpatterns = [ path('', views.book_list, name='book_list'), path('<int:book_id>/', views.book_detail, name='book_detail'), path('create/', views.book_create, name='book_create'), path('<int:book_id>/update/', views.book_update, name='book_update'), path('<int:book_id>/delete/', views.book_delete, name='book_delete'), ]

主urls.py中包含:

from django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('books/', include('books.urls')), # 前缀路由 ]
4.2.2 类视图(Class-Based View)重构

函数式视图代码重复多(如POST/GET分支、对象获取逻辑)。Django提供通用类视图简化:

from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.urls import reverse_lazy from .models import Book class BookListView(ListView): model = Book template_name = 'books/list.html' context_object_name = 'books' paginate_by = 10 # 自动分页 class BookDetailView(DetailView): model = Book template_name = 'books/detail.html' context_object_name = 'book' class BookCreateView(CreateView): model = Book fields = ['title', 'isbn', 'price', 'author'] # 自动生成表单字段 template_name = 'books/create.html' success_url = reverse_lazy('book_list') # 重定向URL需用reverse_lazy class BookUpdateView(UpdateView): model = Book fields = ['title', 'isbn', 'price', 'author'] template_name = 'books/update.html' success_url = reverse_lazy('book_list') class BookDeleteView(DeleteView): model = Book template_name = 'books/confirm_delete.html' success_url = reverse_lazy('book_list')

类视图优势:

  • DRY原则:无需手动写POST/GET逻辑,字段验证、表单渲染、重定向全部内置。
  • 可扩展性强:通过重写get_context_data()、form_valid()等方法定制行为。
  • SEO友好:ListView自动支持分页,URL中可带?page=2参数。

实操心得:初学者建议先用函数式视图理解流程,再迁移到类视图。类视图不是银弹,复杂业务逻辑(如多表联合校验)仍需函数式视图。

4.3 模板层实现:Django Template语言核心用法

在templates/books/目录下创建list.html:

<!-- templates/books/list.html --> <h1>图书列表</h1> <a href="{% url 'book_create' %}">新增图书</a> {% if books %} <ul> {% for book in books %} <li> <a href="{% url 'book_detail' book.id %}">{{ book.title }}</a> - {{ book.author.name }} - ¥{{ book.price }} <a href="{% url 'book_update' book.id %}">编辑</a> <form method="post" action="{% url 'book_delete' book.id %}" style="display:inline;"> {% csrf_token %} <button type="submit" onclick="return confirm('确定删除?')">删除</button> </form> </li> {% endfor %} </ul> <!-- 分页导航 --> {% if books.has_other_pages %} <div class="pagination"> {% if books.has_previous %} <a href="?page=1">&laquo; first</a> <a href="?page={{ books.previous_page_number }}">previous</a> {% endif %} <span class="current">Page {{ books.number }} of {{ books.paginator.num_pages }}.</span> {% if books.has_next %} <a href="?page={{ books.next_page_number }}">next</a> <a href="?page={{ books.paginator.num_pages }}">last &raquo;</a> {% endif %} </div> {% endif %} {% else %} <p>暂无图书。</p> {% endif %}

关键语法说明:

  • {% url 'book_create' %}:反向解析URL,避免硬编码路径。若日后修改URL配置,模板无需改动。
  • {% csrf_token %}:防止跨站请求伪造,所有POST表单必需。
  • {{ book.author.name }}:自动调用外键关联对象的属性,Django内部执行SELECT JOIN优化。
  • books.has_other_pages:分页对象的内置方法,判断是否有其他页。

5. 常见问题与排查技巧实录:从迁移冲突到性能瓶颈的实战解决方案

5.1 迁移相关高频问题速查表

问题现象根本原因解决方案
No migrations to apply.但数据库表未创建执行migrate前未运行makemigrations先执行python manage.py makemigrations,再migrate
django.db.utils.ProgrammingError: relation "books_book" does not exist迁移文件未执行,或执行时数据库连接错误检查settings.py DATABASES配置,确认MySQL服务运行,执行python manage.py showmigrations查看未应用迁移
django.db.migrations.exceptions.InconsistentMigrationHistory迁移记录表(django_migrations)与磁盘迁移文件不一致执行python manage.py migrate --fake-initial(仅适用于首次初始化)或手动清理django_migrations表
修改models后makemigrations生成空文件Django未检测到模型变更(如只改了verbose_name)强制生成:python manage.py makemigrations --empty books,然后手动编辑
多人协作时出现迁移冲突(0002_auto_xxx.py与0002_alter_xxx.py)两人同时基于同一父迁移创建子迁移执行python manage.py makemigrations --no-input生成合并迁移,或手动编辑迁移文件的dependencies字段

排查技巧:python manage.py showmigrations显示所有迁移状态([X]已应用,[ ]未应用);python manage.py sqlmigrate books 0001查看某次迁移对应的原始SQL语句,确认字段类型是否符合预期。

5.2 查询性能问题诊断与优化

问题1:列表页加载慢,Chrome Network面板显示TTFB(Time To First Byte)超2秒
诊断:在views.py中添加日志:

import logging logger = logging.getLogger(__name__) def book_list(request): logger.info("Start querying books") books = Book.objects.all() logger.info(f"Query returned {books.count()} books") return render(...)

若日志显示查询耗时长,执行python manage.py dbshell后运行:

EXPLAIN SELECT * FROM books_book; EXPLAIN SELECT b.*, a.name FROM books_book b JOIN books_author a ON b.author_id = a.id;

若type列为ALL(全表扫描),说明缺少索引。

优化方案:

  • 为外键字段添加数据库索引:author = models.ForeignKey(..., db_index=True)
  • 为常用查询字段添加索引:isbn = models.CharField(..., db_index=True)
  • 使用select_related()预加载关联对象,避免N+1查询:
    # 错误:循环中查询作者 for book in Book.objects.all(): print(book.author.name) # 每次循环触发一次SQL # 正确:一次JOIN查询 for book in Book.objects.select_related('author'): print(book.author.name) # 无额外SQL

问题2:Admin界面编辑图书时,作者下拉框加载慢(上千作者)
原因:Admin默认执行Author.objects.all()加载全部作者。
解决方案:在admin.py中限制查询:

from django.contrib import admin from .models import Book, Author @admin.register(Book) class BookAdmin(admin.ModelAdmin): list_display = ['title', 'isbn', 'price', 'author'] list_filter = ['author'] # 添加侧边筛选栏 search_fields = ['title', 'isbn'] # 启用搜索框 # 优化作者下拉框 def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "author": kwargs["queryset"] = Author.objects.filter(id__lt=100) # 仅显示前100个 return super().formfield_for_foreignkey(db_field, request, **kwargs)

5.3 开发环境典型陷阱与绕过方案

陷阱1:修改models后,shell中import模型报错AttributeError: 'Book' object has no attribute 'xxx'
原因:Python模块缓存。Django Shell未重新加载修改后的models.py。
绕过方案:退出shell后重启,或在shell中执行:

>>> import importlib >>> import books.models >>> importlib.reload(books.models)

陷阱2:静态文件(CSS/JS)404,浏览器控制台报错GET /static/css/style.css HTTP/1.1 404
原因:Django开发服务器默认不提供静态文件服务(生产环境由Nginx处理)。
解决方案:在主urls.py中添加:

from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... 其他URL ] if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

并在settings.py中配置:

STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / "static"] # 开发时存放源文件 STATIC_ROOT = BASE_DIR / "staticfiles" # collectstatic后存放位置

陷阱3:中文字段在Admin界面显示为乱码(如“鲁迅”显示为“é%B3%81%E9%B2%81”)
原因:数据库字符集非utf8mb4,或MySQL连接未指定charset。
验证:执行SHOW VARIABLES LIKE 'character_set%';,确保character_set_database为utf8mb4。
修复:修改MySQL配置文件my.cnf:

[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci

重启MySQL后,重建数据库并重新migrate。

最后分享一个小技巧:Django Debug Toolbar是性能分析神器。安装后在settings.py中启用,页面右下角会出现调试面板,可实时查看SQL查询次数、执行时间、缓存命中率等。对于刚入门的开发者,它比读文档更能直观理解ORM行为。

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

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

立即咨询