ARTICLE DETAIL

资讯详情

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

JavaWeb图书管理系统项目实战:JSP+Servlet+JDBC从设计到部署

JavaWeb图书管理系统项目实战:JSP+Servlet+JDBC从设计到部署 做JavaWeb课程设计或者毕业设计图书管理系统算是经典中的经典了。我之前带过不少同学做这类项目自己也完整落地过一版源码、数据库、设计文档全都齐活正好借这篇博文把整个项目的核心设计思路、表结构、类图建模和实际编码中的坑都梳理一遍给正在为选题发愁或者写到一半卡住的同学做个参考。先交代一下这个项目的定位这是一个基于JavaWeb的图书管理系统技术栈用的是传统的JSP Servlet JDBC MySQL没有引入Spring这类重量级框架。这么选是刻意为之因为课程设计和本科毕业设计的核心考察点是能不能把Web开发的基础链路打通——从前端页面到后端控制再到数据库读写每一步都自己写过、坑都踩过才算真正掌握了JavaWeb的核心脉络。整个项目包含完整的源码、数据库建表脚本以及一份规范的设计文档含摘要和UML类图拿来直接跑、直接改、直接交作业都没问题。1. 项目整体设计与思路拆解做这种管理系统最忌讳一上来就写代码。先把业务捋清楚后面的开发效率能提升好几倍。1.1 核心需求解析图书管理系统面向的典型使用场景是小型图书馆或者资料室核心诉求其实就三件事管书、管人、管借还。管书指的是图书信息的增删改查包括书名、作者、出版社、ISBN、库存数量这些基础字段管人指的是读者信息和系统用户的管理管借还则是整个业务的灵魂——借书要登记还书要销账超期要能查出来。基于这个诉求我设计的角色模型是两层的管理员Admin和普通读者Reader。管理员拥有全部权限读者只能查询图书、查看自己的借阅记录、办理借书还书。这个权限模型虽然简单但已经能覆盖市面上绝大多数中小型图书管理系统的核心场景了。1.2 技术栈选型为什么是JSPServletJDBC我见过很多同学一上来就想用Spring Boot MyBatis Plus理由是“企业现在都用这个”。这话本身没错但放到课程设计里有时候反而会弄巧成拙。原因有三第一课程设计的评分标准里往往有“能否讲清楚请求处理流程”这一项你用Spring Boot前端请求怎么经过Filter进入Servlet、再如何操作数据库这些底层机制容易被框架掩盖答辩时一问就露馅第二传统JavaWeb项目的部署方式是用外置Tomcat你能接触到web.xml配置、Servlet生命周期、JSP九大内置对象这些基础知识点这些都是面试八股常考的第三轻量级项目用JDBC原生操作MySQLSQL是自己写的事务是自己控制的对数据库的理解会更扎实。所以我最终定的技术组合是JSP负责页面展示Servlet负责请求分发和业务调度JDBC负责数据库访问MySQL作为底层存储Tomcat 8.5作为Web容器开发工具是IDEA Navicat。这套组合只有一个字稳。2. 功能模块拆解与业务流程设计系统功能看着简单但设计的时候得想清楚每个模块的边界和它们之间的调用关系。2.1 用户角色与权限控制管理员功能清单如下管理员登录与退出图书信息管理新增、修改、删除、按条件搜索读者信息管理新增读者、禁用读者、重置密码借书操作登记还书操作登记借阅记录查询与统计读者功能清单如下读者登录浏览图书列表与详情借书操作还书操作查看个人历史借阅记录修改个人密码权限控制我用的是最经典的Session判断法登录成功后把当前用户对象塞进Session然后写一个LoginFilter拦截所有需要登录才能访问的路径如果Session里没有用户就重定向到登录页。对于管理员专属接口再加一层角色判断在Servlet内部检查当前用户是否为管理员类型。这个方案虽然朴素但在传统JavaWeb阶段是标准答案。2.2 核心业务流程梳理借书流程是这样的首先读者登录系统输入图书编号和目标图书信息系统查询该图书当前库存是否大于0如果库存充足生成一条借阅记录状态置为“借出中”同时把图书表中对应的库存数量减1。整个操作必须在同一个数据库事务里完成否则会出现“借阅记录生成了但库存没扣”或者反过来“库存扣了但记录没生成”的脏数据情况。还书流程就是借书的逆操作系统根据借阅记录ID找到对应记录把还书日期填上状态改为“已归还”同时图书库存加1。注意还书时一定要校验这条借阅记录是否已经归还过避免重复还书导致库存越加越多这是个很典型的业务漏洞我在第一版代码里就踩过。借阅周期我设置的是30天超期还书的判定用还书日期减去应还日期大于0就是超期在借阅记录列表里用醒目标记提示。2.3 功能清单速查表功能模块子功能角色核心操作表登录认证用户登录、退出、密码修改全部t_admin / t_reader图书管理图书录入、编辑、删除、搜索管理员t_book读者管理读者新增、禁用、搜索管理员t_reader借阅管理借书登记、还书登记全部t_borrow记录查询借阅记录查询、超期标注全部t_borrow统计展示图书总数、读者总数、借出总数管理员联表查询这个表看起来简单但设计的时候花了不少心思尤其是t_borrow表怎么跟t_book、t_reader关联稍后在数据库设计部分细说。3. 数据库设计与建表方案数据库设计是整个项目的地基。表结构没设计好后面写SQL能写到怀疑人生。我来说说我这一版的设计思路。3.1 核心表结构解析我一共设计了四张表分别是管理员表、读者表、图书表、借阅记录表。四个表互相配合足以覆盖系统的全部功能。管理员表 t_admin 字段如下id主键自增username登录用户名唯一password登录密码MD5加密存储real_name真实姓名create_time创建时间读者表 t_reader 字段如下id主键自增reader_no读者编号唯一用于登录password登录密码name读者姓名phone联系电话status状态1正常 0禁用create_time注册时间图书表 t_book 字段如下id主键自增book_no图书编号唯一book_name书名author作者publisher出版社isbnISBN号category分类price价格stock库存数量total总量create_time入库时间借阅记录表 t_borrow 字段如下id主键自增book_id关联t_book表的idreader_id关联t_reader表的idborrow_time借出时间due_time应还时间return_time实际归还时间为空表示未还status借阅状态1借出中 2已归还这里有个关键设计点为什么借阅记录不直接存图书编号字符串和读者编号字符串而是存外键ID答案是为了数据一致性。比如某本书被删除之后如果借阅记录存的是图书编号的字符串那这条记录就成了孤儿数据查不到书的信息了。但如果存的是t_book表的id即使是逻辑上需要保留历史记录也可以根据ID关联到图书信息。更重要的是用外键ID联表查询时可以JOIN出多张表的字段比如同时展示图书名、读者名、借书时间、应还时间一个SQL全搞定。3.2 建表SQL参考直接贴建表脚本CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4; USE library_system; CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录用户名, password VARCHAR(64) NOT NULL COMMENT MD5密码, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT管理员表; CREATE TABLE t_reader ( id INT PRIMARY KEY AUTO_INCREMENT, reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT 读者编号, password VARCHAR(64) NOT NULL COMMENT 登录密码, name VARCHAR(50) NOT NULL COMMENT 读者姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; CREATE TABLE t_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_no VARCHAR(20) NOT NULL UNIQUE COMMENT 图书编号, book_name VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) DEFAULT NULL COMMENT 作者, publisher VARCHAR(100) DEFAULT NULL COMMENT 出版社, isbn VARCHAR(30) DEFAULT NULL COMMENT ISBN号, category VARCHAR(50) DEFAULT NULL COMMENT 分类, price DECIMAL(10,2) DEFAULT 0 COMMENT 价格, stock INT DEFAULT 0 COMMENT 当前库存, total INT DEFAULT 0 COMMENT 总数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; CREATE TABLE t_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL COMMENT 图书ID, reader_id INT NOT NULL COMMENT 读者ID, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借出时间, due_time DATETIME NOT NULL COMMENT 应还时间, return_time DATETIME DEFAULT NULL COMMENT 实际归还时间, status TINYINT DEFAULT 1 COMMENT 1借出中 2已归还, KEY idx_book_id (book_id), KEY idx_reader_id (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES t_book(id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES t_reader(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;几个细节说明一下字符集一定要用utf8mb4而不是utf8utf8mb4是utf8的超集能存emoji和一些生僻字而且兼容性更好。MySQL 8.0默认就是utf8mb4但为了防止移植到旧环境时出问题建库建表最好显式声明。密码字段我用的是VARCHAR(64)而不是VARCHAR(32)因为MD5虽然固定是32位十六进制但我预留了可能升级加密算法的空间。真做项目时建议应用加盐哈希至少不要明文存密码明文在答辩时被老师指出来很尴尬。外键约束我加了。虽然有些开发者因为性能问题不喜欢用外键但在这个体量的系统里外键能帮你兜住很多数据完整性的问题比如删除已经被借阅的图书时会报错提醒你先处理关联数据。3.3 测试数据准备建完表之后一定要先插入几条测试数据再开始写代码。我整理的初始化数据大致是这样的INSERT INTO t_admin (username, password, real_name) VALUES (admin, MD5(123456), 系统管理员); INSERT INTO t_reader (reader_no, password, name, phone) VALUES (R2024001, MD5(123456), 张三, 13800001111), (R2024002, MD5(123456), 李四, 13800002222); INSERT INTO t_book (book_no, book_name, author, publisher, isbn, category, price, stock, total) VALUES (B001, Java编程思想, Bruce Eckel, 机械工业出版社, 9787111213826, 计算机, 108.00, 5, 5), (B002, 深入理解Java虚拟机, 周志明, 机械工业出版社, 9787111641247, 计算机, 129.00, 3, 3), (B003, 三体, 刘慈欣, 重庆出版社, 9787536692930, 科幻, 93.00, 6, 6);插入测试数据的时候顺手验证一下建表语句有没有报错这比等代码写完再排查数据库问题要省事得多。4. 类图设计与UML建模文档里面包含类图是这个项目的一个加分项。很多同学会画类图但画出来的类图跟实际代码完全对不上这在大作业和答辩里是大忌。我的建议是先按类图画代码代码写完再反向调整类图保证两者一致。4.1 实体类设计Entity我设计了四个实体类对应四张表。图书实体 Book属性id, bookNo, bookName, author, publisher, isbn, category, price, stock, total, createTime方法getter/settertoString读者实体 Reader属性id, readerNo, password, name, phone, status, createTime管理员实体 Admin属性id, username, password, realName, createTime借阅记录实体 BorrowRecord属性id, bookId, readerId, borrowTime, dueTime, returnTime, status额外这两个展示用字段在实体里我也加上——bookName, readerName用于联表查询结果的承载有人可能会问在BorrowRecord里加bookName和readerName会不会破坏实体设计的纯粹性我的看法是在传统JavaWeb项目里这种“展示扩展字段”的做法很常见因为JSP页面需要一次性获取所有展示数据不想再二次查表。如果你特别在意设计模式规范也可以新建一个VO类来承载联表数据BorrowRecordVO extends BorrowRecord加两个字段就行。两种方案都可以看你怎么跟答辩老师解释。4.2 数据访问层与业务层类图数据访问层我设计的是DAO接口加实现类BookDao增删改查图书ReaderDao增删改查读者AdminDao根据用户名查询管理员BorrowDao新增借阅记录、更新归还信息、根据条件查询借阅列表Servlet控制层的设计思路是一个Servlet对应一个模块而不是一个Servlet对应一个功能。比如图书模块就用一个BookServlet通过action参数区分是add、update、delete还是searchAdminServlet处理管理员登录、退出ReaderServlet处理读者注册、管理、状态变更BookServlet处理图书CRUD和搜索BorrowServlet处理借书、还书、借阅记录查询LoginServlet处理两类用户的登录入口这种“一个模块一个Servlet”的设计代码组织起来非常清晰而且类图画出来也好看。相比每个功能写一个Servlet类的数量少了一大半维护成本也更低。4.3 类图画法要点画类图我用的工具是StarUML和IDEA自带插件两个工具各有优缺点。IDEA的Diagrams功能对Java类支持得特别好——右键类文件选择Diagrams再选择Show Diagram就能自动生成类图箭头关系自动识别根本不用手画。这个功能对于“代码已经写完根据代码生成类图”的场景极其好用而且保证跟代码零偏差。StarUML更灵活适合从零开始设计类图但需要手动控制关系线。画的时候注意几种关系符号的区别泛化关系继承空心三角形箭头实线实现关系空心三角形箭头虚线关联关系普通箭头实线依赖关系普通箭头虚线类图画好后建议在文档里放两张图一张是实体类图重点关注属性设计一张是DAOServlet整体架构图重点关注方法的调用关系。5. 核心功能模块实现要点代码实现环节我挑几个核心模块说说技术点。这些点也是答辩时老师最喜欢追问的地方。5.1 登录与权限拦截实现登录逻辑在Servlet里实现核心代码思路如下String username request.getParameter(username); String password request.getParameter(password); String role request.getParameter(role); // 密码MD5加密后再与数据库比对 String md5Password MD5Util.md5(password); if (admin.equals(role)) { Admin admin adminDao.findByUsernameAndPassword(username, md5Password); if (admin ! null) { session.setAttribute(loginUser, admin); session.setAttribute(role, admin); response.sendRedirect(admin/index.jsp); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } else { // reader角色逻辑类似 }这里有个很容易被忽视的细节用forward和sendRedirect的区别。登录失败时我用forward转发到登录页这样可以携带错误提示信息且URL不会变化登录成功时用sendRedirect重定向到首页为了防止表单重复提交——如果用户按F5刷新页面不会再次提交登录请求。权限拦截我用的是FilterWebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(); String uri request.getRequestURI(); // 放行登录页、静态资源 if (uri.endsWith(login.jsp) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } // 放行登录接口 if (uri.endsWith(/login)) { chain.doFilter(req, resp); return; } // 其他请求需要登录 if (session.getAttribute(loginUser) null) { response.sendRedirect(login.jsp); return; } chain.doFilter(req, resp); } }Filter里一定要记得放行静态资源不然CSS和JS全部加载不出来页面会变成纯HTML文字特别影响观感。5.2 图书管理模块的增删改查图书新增页面是一个表单提交到BookServlet的add方法Servlet接收请求参数封装成Book对象调用BookDao的insert方法。删除图书时需要先检查该图书是否有未归还的借阅记录有的话要拦截删除操作提示“当前图书有借阅记录未归还无法删除”。这个检查逻辑看着简单但实际会遇到一个问题是写一条SQL去count尚未归还的记录还是查出所有记录再在内存里判断我的建议是直接在DAO层写一条SQLpublic int countUnreturnedByBookId(int bookId) { String sql SELECT COUNT(*) FROM t_borrow WHERE book_id ? AND status 1; // 执行查询 }这样数据库只返回一个数字省去对象装配的开销。图书搜索我用的是动态SQL拼接StringBuilder sql new StringBuilder(SELECT * FROM t_book WHERE 11); ListObject params new ArrayList(); if (bookName ! null !bookName.isEmpty()) { sql.append( AND book_name LIKE ?); params.add(% bookName %); } if (category ! null !category.isEmpty()) { sql.append( AND category ?); params.add(category); }这里的11是个小技巧目的是让后面所有AND条件都不用在开头单独判断。字符串拼接SQL时字段名如果用关键字会直接报错。我建议代码里统一给字段名加反引号或者避免使用keyword做字段名这也是为什么建表的时候我把分类字段叫category而不是type因为type在某些数据库版本下可能有特殊含义。5.3 借书和还书的完整实现借书逻辑核心代码片段public void borrowBook(int bookId, int readerId) { Connection conn null; try { conn DBUtil.getConnection(); // 开启事务 conn.setAutoCommit(false); // 1. 查询图书库存 Book book bookDao.findById(conn, bookId); if (book null || book.getStock() 0) { throw new RuntimeException(图书库存不足); } // 2. 查询读者状态 Reader reader readerDao.findById(conn, readerId); if (reader null || reader.getStatus() ! 1) { throw new RuntimeException(读者被禁用或不存在); } // 3. 扣库存 bookDao.decreaseStock(conn, bookId); // 4. 插入借阅记录 Timestamp borrowTime new Timestamp(System.currentTimeMillis()); // 应还时间 当前时间 30天 Timestamp dueTime new Timestamp(System.currentTimeMillis() 30L * 24 * 3600 * 1000); borrowDao.insert(conn, bookId, readerId, borrowTime, dueTime); // 提交事务 conn.commit(); } catch (Exception e) { // 回滚事务 if (conn ! null) { conn.rollback(); } throw new RuntimeException(借书失败 e.getMessage()); } finally { DBUtil.close(conn); } }核心点在于事务处理。借书操作涉及库存扣减和借阅记录新增两个动作这两个动作必须保证同时成功或者同时失败。如果不加事务假设库存扣了但插入借阅记录时SQL报错就会出现“书没了但借阅记录没生成”的严重bug。还书逻辑相似把库存加回来、更新借阅记录状态和归还时间同样要开事务。我在第一版实现里踩过一个挺经典的坑数据库连接工具类每次getConnection都是新连接导致开始事务后后续的DAO方法里又各自调用了getConnection拿到的是不同的连接事务控制完全失效。解决办法是让DAO方法接收外部传入的Connection参数共用同一个连接。这个点在文档里一定要写清楚答辩时能说明白老师会觉得你是真做过这个系统。5.4 读者管理模块与状态控制读者管理的核心在于状态控制。管理员可以手动禁用某个读者禁用后该读者不能登录系统但历史借阅记录保留。我用的方案是在t_reader表加status字段登录时检查status代码片段// 登录时校验状态 if (reader.getStatus() 0) { request.setAttribute(error, 账号已被禁用请联系管理员); request.getRequestDispatcher(login.jsp).forward(request, response); return; }这个校验不能在Filter里做因为Filter里只能拿到用户名和密码还没有查询过数据库。所以位置放在登录的LoginServlet里查询出读者对象之后、塞Session之前。6. 项目文档撰写与答辩材料准备源码、数据库、文档是这一类JavaWeb项目交付的三大件。很多同学重代码轻文档结果代码写得不错文档一塌糊涂最后分数被拉低。我自己的经验是文档占的分数比重往往不小而且文档写得好能帮你提前理清答辩思路。6.1 文档的完整结构规范的课程设计/毕业设计文档通常包含这些部分摘要300字左右中英文各一份更佳绪论项目背景、国内外现状、研究意义需求分析功能需求、非功能需求、可行性分析系统设计架构设计、数据库设计、类图、时序图系统实现核心功能截图和代码片段说明系统测试测试用例、测试结果总结与展望6.2 摘要撰写的技巧摘要的核心目标是让读者在300字内了解系统的全貌。我常用的摘要结构是四句话第一句描述项目背景——图书馆信息化管理的需求和意义第二句明确系统的目标读者和技术方案——基于JavaWeb技术栈采用JSPServletMySQL实现的一个图书管理系统第三句概括系统的核心功能——实现了图书管理、读者管理、借阅管理、记录查询等功能模块第四句总结本设计的意义——提升图书馆管理工作效率同时加深了对JavaWeb开发技术的理解。照着这个框架写摘要基本上不会跑偏。关键的术语带上就行不用写太多形容词。6.3 类图在文档中的呈现方式文档里的类图建议放两类一类是系统整体用例图展示管理员和读者两个角色各自的功能访问范围另一类是实现类的类图包含实体类、DAO类、Servlet类的属性与方法用IDEA自动生成后截图保证和源码一致。在文档正文中类图前面最好加一段文字说明“本系统采用分层架构设计实体层负责数据承载数据访问层负责数据库操作控制层负责请求处理与路由分发。下图是系统的核心类图结构。”配图加文字这个板块就很充实了。7. 常见坑点与调试实录这一部分是我在整个开发过程中最想分享的内容全是实操中踩出来的坑每一条都对应了具体的排查过程。7.1 数据库连接失败的排查TypeError、ClassNotFoundException、Communications link failure这三个错在开发阶段几乎必现一个。最常见的两个原因是MySQL服务没启动或者JDBC连接串写错。连接MySQL 8.0以上版本时驱动类必须是com.mysql.cj.jdbc.Driver而不是老版本的com.mysql.jdbc.Driver而且连接串要加时区参数serverTimezoneAsia/Shanghai否则会报时区错误。我建议把数据库连接参数统一放到db.properties配置文件中jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/library_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456利用useUnicodetruecharacterEncodingutf8解决中文乱码问题。这个配置我从项目开始就一边配一边测试等代码写完从来没在连接层面出过错。7.2 JSP页面中文乱码问题乱码问题在JavaWeb里是个高频坑。我一共总结出三个层面的乱码第一层是JSP页面本身乱码解决办法是在JSP第一行声明% page contentTypetext/html;charsetUTF-8 languagejava %并且确保文件编码是UTF-8。第二层是页面提交的中文参数在Servlet里变成问号解决办法是在Servlet里加request.setCharacterEncoding(UTF-8)这条语句要放在读取任何请求参数之前。第三层是数据库存进去的中文查出来乱码解决办法是JDBC连接串加useUnicodetruecharacterEncodingutf8同时建表时指定utf8mb4。很多同学只排查了其中一个层面结果错误依然存在其实是因为三层都要同时满足。7.3 相对路径与资源加载问题JSP页面里引入CSS和JS路径写错的话会出现排版全乱的“裸奔页面”。我的经验是在JSP页面顶部用${pageContext.request.contextPath}取上下文路径。比如项目部署后访问路径是http://localhost:8080/library_war_exploded/那么加载CSS的正确写法是link relstylesheet href${pageContext.request.contextPath}/css/style.css用相对路径css/style.css在主页可能正常但跳转到子目录下的页面后就失效了。这个坑在课程设计里特别常见因为本地跑的时候往往直接访问根路径根本发现不了问题。7.4 借阅记录联表查询时的字段重复问题我联查借阅记录和图书名、读者名时写了这样一个SQLString sql SELECT b.*, bk.book_name, r.name AS reader_name FROM t_borrow b JOIN t_book bk ON b.book_id bk.id JOIN t_reader r ON b.reader_id r.id;直接SELECT b.*会把t_borrow表所有字段取出来然后额外取图书和读者的名字。封装结果集的时候要注意t_borrow里也有id字段联表结果的第一列就是借阅记录的id如果写代码时不注意取字段的索引顺序很容易串数据。解决方法是给t_borrow表起别名b然后手动指定需要的字段别偷懒用SELECT *String sql SELECT b.id, b.book_id, b.reader_id, b.borrow_time, b.due_time, b.return_time, b.status, bk.book_name, r.name AS reader_name FROM t_borrow b JOIN t_book bk ON b.book_id bk.id JOIN t_reader r ON b.reader_id r.id;这样代码可读性大大提高封装结果集时字段位置清清楚楚。7.5 外键约束导致的删除失败如果加了外键约束删除一本已经被借阅过的图书时MySQL会报错Cannot delete or update a parent row: a foreign key constraint fails遇到这种情况不要急着改外键约束先想清楚业务上允不允许删除。我建议的处理方式有两种第一种是在删除前检查是否存在关联借阅记录有记录就禁止删除提示用户先处理借阅关系第二种是用逻辑删除给t_book表加一个is_deleted字段删除时把is_deleted置为1查询时默认过滤。对于课程设计来说第二种方案更稳妥还能在系统里展示软删除的设计思路。7.6 分页查询的实现细节图书数据量大之后不分页的查询一页几百条记录页面加载慢到怀疑人生。我给图书列表加了分页三个核心参数当前页码pageNum、每页条数pageSize默认10、总记录数total。SQL用LIMIT实现String sql SELECT * FROM t_book LIMIT ?, ?; ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize);另外还需要一条COUNT查询获取总记录数然后计算总页数int totalPages (int) Math.ceil(total * 1.0 / pageSize);分页的页码栏建议带上首页、上一页、下一页、末页四个操作中间显示当前页/总页数的信息。注意LIMIT第一个参数是从0开始的所以数据库层面的offset是(pageNum - 1) * pageSize这个细节很容易少减一个1导致第二页重复显示第一页的最后一条数据。8. 项目部署与验收的完整流程写到这里再补一个我觉得特别实用的部署笔记。很多同学代码在IDEA里跑得飞起但把项目导出成war包部署到独立Tomcat后就傻了各种问题层出不穷。我梳理了一套完整流程。8.1 IDEA导出发布到Tomcat先在IDEA里确认项目能正常运行然后构建war包。传统JavaWeb项目打war包的过程是File - Project Structure - Artifacts - 点加号 - Web Application: Archive - 选择项目。构建完成后在IDEA底部找到Build Artifacts点击Build就会生成war文件。把生成的war包复制到Tomcat的webapps目录下启动Tomcat它会自动解压项目。这时访问路径就是http://localhost:8080/项目名/比如war包叫library.war访问路径就是http://localhost:8080/library/。注意项目里的数据库连接参数要检查一下IDEA里连的localhost:3306在服务器上也要通MySQL的密码环境变量和本地可能不一样这是最常见的部署翻车点。8.2 初始化数据库部署时数据库脚本要重跑一遍。把建库建表SQL在目标MySQL执行同时插入初始管理员账号和测试数据。我在文档里特地附了一节“数据库初始化说明”把每一步操作写清楚方便答辩时老师现场验证。8.3 功能验证清单部署完成后按以下顺序快速过一遍核心场景用管理员账号登录检查首页统计数字是否正确新增一本图书再去列表页确认记录出现新增一个读者切换到该读者登录测试借书借书后检查库存是否减1还书后检查库存是否加回来借阅记录状态是否变为已归还搜索功能分别测试书名关键词和分类搜索删除一本有借阅记录的图书确认被拦截这套验证流程我每次部署完都跑一遍漏网之鱼极少。回想起来这个图书管理系统项目麻雀虽小五脏俱全从需求分析、数据库设计到类图建模、编码实现再到文档撰写和部署验证完整走完一遍之后JavaWeb的整个知识体系基本就串起来了。过程中踩过的那些坑——事务失效、中文乱码、外键删除失败、分页边界错位——每一处都是提升经验值的好机会。如果你正在做同类项目我的建议非常直接先把数据库四个表建好把测试数据插进去然后从登录功能开始写写完一个模块测一个模块最后再统一部署验证。别贪功能多把借书、还书、图书CRUD、读者禁用这几个核心场景写扎实设计文档里把类和数据库关系表述清楚再准备几个能回答上来的技术难点这个项目基本就能稳稳交付了。
返回列表