ARTICLE DETAIL

资讯详情

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

Django工程创建与Models实战:从零构建图书管理系统

Django工程创建与Models实战:从零构建图书管理系统 1. 项目概述为什么从Django工程创建和models入手是后端开发绕不开的第一道门槛你刚装好Pythonpip 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.pymanage.py项目操作中枢。它内部做了三件事1设置DJANGO_SETTINGS_MODULE环境变量指向bookstore.settings2加载Django配置3根据传入的子命令如runserver、migrate调用对应模块。你可以把它理解成Django项目的“遥控器”所有功能都通过它触发。bookstore/init.py标识该目录为Python包。内容为空但不可或缺。没有它Python解释器无法将bookstore识别为可导入模块后续import bookstore.settings会失败。bookstore/settings.py项目配置总控台。这里定义了数据库连接、静态文件路径、中间件列表、安全密钥等核心参数。新手最容易犯的错误是直接修改DEBUGTrue上线或硬编码SECRET_KEY。正确的做法是开发环境保持DEBUGTrue便于调试生产环境必须设为False并通过环境变量注入SECRET_KEY如os.environ.get(DJANGO_SECRET_KEY)。bookstore/urls.pyURL路由总入口。它不直接处理请求而是将URL模式分发给各App的urls.py。主urls.py通常只保留管理后台/admin/和API根路径/api/的路由其他业务路由全部下沉到独立App中。这种设计让项目具备天然的模块化能力——比如图书管理、用户管理、订单管理可分别作为三个App互不影响。bookstore/asgi.py与wsgi.py应用服务器网关接口。wsgi.py用于传统同步服务器如Gunicornasgi.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_modeSTRICT_TRANS_TABLES, charset: utf8mb4, }, TEST: { CHARSET: utf8mb4, COLLATION: utf8mb4_unicode_ci, } } }关键参数说明HOST用127.0.0.1而非localhostMySQL在localhost下默认走Unix socket连接而Django驱动强制走TCP/IP。用IP地址可避免连接异常。OPTIONS中init_command强制开启严格模式防止插入超长字符串时被静默截断如CharField(max_length10)存入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等文件。此时需做两件事将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, # 新增这一行 ]在books/models.py中定义数据模型。以图书信息为例from django.db import models from django.core.validators import MinValueValidator, MaxValueValidator class Author(models.Model): name models.CharField(max_length100, verbose_name作者姓名) birth_date models.DateField(nullTrue, blankTrue, verbose_name出生日期) def __str__(self): return self.name class Book(models.Model): title models.CharField(max_length200, verbose_name书名) isbn models.CharField(max_length13, uniqueTrue, verbose_nameISBN号) price models.DecimalField( max_digits6, decimal_places2, validators[MinValueValidator(0)], verbose_name定价 ) author models.ForeignKey( Author, on_deletemodels.CASCADE, related_namebooks, verbose_name作者 ) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) updated_at models.DateTimeField(auto_nowTrue, verbose_name更新时间) class Meta: verbose_name 图书 verbose_name_plural 图书 ordering [-created_at] def __str__(self): return self.title这段代码看似简单实则暗含多个设计决策verbose_name控制admin界面和表单的显示名称中文友好必备。nullTrue, blankTrue前者影响数据库字段是否允许NULL后者影响Django表单是否允许空提交。两者常被混用需严格区分。ForeignKey的on_deleteDjango 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( nameAuthor, fields[ (id, models.AutoField(auto_createdTrue, primary_keyTrue, serializeFalse, verbose_nameID)), (name, models.CharField(max_length100, verbose_name作者姓名)), (birth_date, models.DateField(blankTrue, nullTrue, verbose_name出生日期)), ], options{ verbose_name: 作者, verbose_name_plural: 作者, }, ), ]注意两点Django自动添加了id主键字段即使models.py中未声明Django也会为每个Model添加自增整数主键。若需自定义主键如用UUID需显式声明id models.UUIDField(primary_keyTrue, defaultuuid.uuid4, editableFalse)。字段定义与models.py不完全一致如birth_date字段在models.py中是nullTrue, blankTrue而迁移文件里拆分为blankTrue, nullTrue。这是因为Django内部将校验逻辑blank与数据库约束null分离处理。迁移文件本质是“数据库操作指令集”可手动编辑。例如你想把Author.name字段的max_length从100改为150直接修改models.py后执行makemigrations会生成新迁移文件。但若想合并到初始迁移中避免线上迁移步骤过多可删除0001_initial.py修改models.py后再重新makemigrations —— 前提是数据库尚未migrate且无历史数据。实操心得团队协作中严禁手动修改已提交到Git的迁移文件。若多人同时修改同一ModelDjango会生成依赖型迁移如0002_auto_20230101_1200.py依赖0001_initial.py。此时应执行python manage.py makemigrations --empty books创建空迁移再手动编写合并逻辑。3.3 执行迁移与数据库状态验证生成迁移文件后执行python manage.py migrateDjango会按数字顺序执行所有未应用的迁移文件并在数据库中创建django_migrations表记录执行状态。此时检查MySQLUSE 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测试书, isbn1234567890123, price59.99, author_id1)查看created_at和updated_at是否自动写入当前时间。唯一性约束重复插入相同ISBN的图书Django会抛出IntegrityError异常。若migrate失败常见原因有MySQL用户无建表权限GRANT ALL PRIVILEGES ON bookstore_db.* TO rootlocalhost; FLUSH PRIVILEGES;字段名含Python关键字如定义class Order(models.Model): passOrder是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_date1881-09-25) author.id 1 # 创建图书新增关联作者 book Book.objects.create( ... title呐喊, ... isbn9787020000000, ... price35.00, ... authorauthor ... ) book.id 1 # 查询所有图书查询 Book.objects.all() QuerySet [Book: 呐喊] # 条件查询查询 Book.objects.filter(price__gt30) # 价格大于30 QuerySet [Book: 呐喊] Book.objects.get(isbn9787020000000) # 精确匹配不存在则抛异常 Book: 呐喊 # 修改更新 book.price 39.99 book.save() # 必须调用save()才写入数据库 # 删除删除 book.delete() # 返回(1, {books.Book: 1})表示删除1条Book记录这里揭示Django ORM的核心机制惰性查询Lazy EvaluationBook.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) # 预加载作者信息避免N1查询 return render(request, books/list.html, {books: books}) def book_detail(request, book_id): 图书详情页 book get_object_or_404(Book, idbook_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( titletitle, isbnisbn, priceprice, author_idauthor_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, idbook_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_idbook.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, idbook_id) book.delete() return redirect(book_list)对应URL配置books/urls.pyfrom django.urls import path from . import views urlpatterns [ path(, views.book_list, namebook_list), path(int:book_id/, views.book_detail, namebook_detail), path(create/, views.book_create, namebook_create), path(int:book_id/update/, views.book_update, namebook_update), path(int:book_id/delete/, views.book_delete, namebook_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中可带?page2参数。实操心得初学者建议先用函数式视图理解流程再迁移到类视图。类视图不是银弹复杂业务逻辑如多表联合校验仍需函数式视图。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 methodpost action{% url book_delete book.id %} styledisplay:inline; {% csrf_token %} button typesubmit onclickreturn confirm(确定删除)删除/button /form /li {% endfor %} /ul !-- 分页导航 -- {% if books.has_other_pages %} div classpagination {% if books.has_previous %} a href?page1laquo; first/a a href?page{{ books.previous_page_number }}previous/a {% endif %} span classcurrentPage {{ 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再migratedjango.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面板显示TTFBTime 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(fQuery 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_indexTrue)为常用查询字段添加索引isbn models.CharField(..., db_indexTrue)使用select_related()预加载关联对象避免N1查询# 错误循环中查询作者 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问题2Admin界面编辑图书时作者下拉框加载慢上千作者原因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__lt100) # 仅显示前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/JS404浏览器控制台报错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_rootsettings.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行为。
返回列表