简介:数据库应用课程设计.zip 面向计算机相关专业学生与 Web 课程设计开发者,围绕教室管理系统整合数据库建模、前后端交互与安全控制等实践内容,可用于课程设计参考、答辩准备或二次开发。压缩包共 39 个文件,约 1.94MB,包含 8 个 JSP 页面、4 个 CSS 与 4 个 JS 前端资源,以及 Java 源码、项目配置、SQL Server 数据库文件(mdf/ldf)、字体文件和一份 docx 说明文档,目录按 WebContent、src、.settings 等模块组织。已有 2114 人学习,适合需要完整课程项目结构、数据库表设计与页面实现思路的读者,可从中梳理实体关系、MVC 分层、查询优化和用户认证等要点,并对照源码理解教室预订、课程信息维护等功能的落地方式。
1. 教室管理系统作业的真正难点在表设计
课设题目一发下来,多数人的第一反应是打开编辑器找模板,页面抄得挺像样,结果答辩时被问一句“你这个预约表满足第几范式”“两个人同时抢同一间教室怎么办”就卡住了。像教室管理系统这类数据库应用课程设计,分数基本都压在库表设计和 SQL 上,web 大作业的页面只是把设计意图展示出来。这份资源给的是完整可跑的 Java Web 版本:JSP 负责展示,Servlet 接请求,JDBC 直连 MySQL,没有堆框架,恰好适合当作数据库课设的底座,也适合当二次开发的起点。它面向两类人:刚学完 SQL、想找个真实 CRUD 场景练手的同学,以及需要交一份既能演示、又能讲清楚 ER 图的 web 大作业的人。下面我按自己拆包的顺序讲:结构、跑通、核心链路、踩坑、改造。
2. 项目骨架:MVC 三层目录与六张核心表怎么对上
拿到一个陌生工程,我习惯先不碰业务代码,只看两样东西:目录和建表脚本。目录告诉你作者怎么分层,建表脚本告诉你他到底想解决什么问题。这两样看完,一个 CRUD 项目基本就透明了,剩下的只是细节填充。
2.1 目录结构:src 下三个包各自负责什么
解开压缩包,典型的 Eclipse 风格动态 Web 工程长这样:
ClassroomMS/ ├── src/ │ ├── com/cms/entity/ # 实体类,与数据库表一一对应 │ │ ├── Classroom.java │ │ ├── Booking.java │ │ └── SysUser.java │ ├── com/cms/dao/ # 只写 SQL,不写业务判断 │ │ ├── ClassroomDao.java │ │ └── BookingDao.java │ ├── com/cms/service/ # 业务规则:冲突校验、状态流转、事务 │ │ └── BookingService.java │ ├── com/cms/servlet/ # 收参数、调 service、转发 JSP │ │ └── BookingAddServlet.java │ └── com/cms/util/ # JDBCUtils、日期工具、分页封装 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器、欢迎页 │ │ └── lib/ # mysql-connector-java-x.jar、jstl.jar │ ├── jsp/ # 教室列表、预约表单、审批页 │ └── css/ js/ └── sql/ └── cms.sql # 建库建表 + 初始化数据,导入即用分层的关键不在“有这几个包”,而在依赖方向:servlet 只能调 service,service 只能调 dao,dao 里不允许出现if (用户是管理员)这种判断。我带过几个同学的课设,最典型的问题是 Servlet 里直接new JdbcTemplate()拼 SQL,表面能跑,一旦要加“同一教室同一时段只能有一条有效预约”的规则,就得在三个 JSP 对应的三个 Servlet 里各改一遍,改漏一个就出脏数据。这个工程把冲突校验压在BookingService一个方法里,是这个骨架最值钱的地方。
entity包我一般建议保持“哑对象”:只有字段、getter/setter、无参构造,别塞业务方法。JSP 用 EL 表达式取值时依赖 getter 命名规范,getRoomNo()对应${room.roomNo},写成getRoomno()页面就直接空白,还不报错,这种玄学问题排查起来非常费时间。
2.2 库表设计:四张主表加两张字典表撑起全部功能
课设的评分点八成在这里。教室管理系统的核心实体其实就四个:教室、用户、课程、预约。常见的建表脚本如下,我把注释留全了,方便直接改:
-- 教室表:只存物理属性,容量用于预约时的人数校验 CREATE TABLE classroom ( room_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL UNIQUE COMMENT '教室编号,如 A301', building VARCHAR(30) NOT NULL COMMENT '教学楼', capacity INT NOT NULL DEFAULT 0 COMMENT '容纳人数', room_type TINYINT NOT NULL DEFAULT 0 COMMENT '0普通 1多媒体 2机房', status TINYINT NOT NULL DEFAULT 1 COMMENT '1可用 0停用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约表:时间拆成开始/结束两个 DATETIME,冲突判断全靠它 CREATE TABLE booking ( booking_id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, user_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, purpose VARCHAR(100) COMMENT '用途说明', state TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回 3已取消', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_room_time (room_id, start_time, end_time), CONSTRAINT fk_booking_room FOREIGN KEY (room_id) REFERENCES classroom(room_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表:角色用单字段区分,课设阶段够用,别急着拆权限表 CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '存 MD5,别存明文', real_name VARCHAR(30), role TINYINT NOT NULL DEFAULT 0 COMMENT '0学生 1教师 2管理员' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关于时间字段,是这套设计里最容易被质疑也最容易被夸的地方。很多同学照抄教务系统,用“第几周 + 第几节”存课表,结果一到“临时借用 14:00 到 15:30 这种整点半点的时段”就傻眼,因为节次和真实时间对不上。用start_time/end_time两个 DATETIME,时间轴是连续的,冲突判断退化成一次区间比较,SQL 一行就写完;代价是前端要做时间段下拉框,不能让学生随便填。这是典型的“用一点前端麻烦换数据库干净”,课设答辩讲这一句,比背十条范式定义有用。
索引idx_room_time (room_id, start_time, end_time)的顺序也有讲究。查询条件永远带room_id,所以它必须放最左,符合最左前缀原则;后面两个字段跟着走,能覆盖掉区间过滤。我见过把start_time放第一位的写法,单教室数据量小时看不出差别,等到初始化脚本插了两千条预约记录,列表页翻页就开始卡,那时候再改索引就得跟答辩时间赛跑了。
2.3 实体关系:外键、状态机与那几条必写进答辩稿的约束
四张表的关系不复杂:classroom与booking是一对多,sys_user与booking也是一对多,course表和booking通过course_id弱关联,字典表存教室类型和楼栋名称,纯为下拉框服务。真正需要在文档里写清楚的是约束,不是连线。
第一,外键fk_booking_room限制“不能给不存在的教室下单”,同时意味着删除教室前必须先处理它的历史预约。常见做法是教室不物理删除,只把status置 0,历史数据留着,答辩演示时也好看。第二,state是个状态机:0 待审 → 1 通过 / 2 驳回,1 通过 → 3 已取消,除此之外的跳转都应该在 service 层拦掉。第三,(room_id, start_time)上其实应该有个业务唯一性约束,但因为有“驳回”和“已取消”这两种不占用的状态,没法直接用 UNIQUE 索引,只能靠 service 查冲突,这个取舍要能讲明白。
3. 环境搭建与跑通:Tomcat 加 MySQL 的最小闭环
课设项目最怕的是“代码没错,环境不对”。这个工程用的是原生 JDBC,没有 Maven 自动拉包,所以依赖和版本必须自己对齐,一步对不上就起不来。下面这套组合是我在旧机器和新机器上都验证过的。
3.1 版本组合:JDK 8 + Tomcat 8.5 + MySQL 5.7 为什么还值得用
| 组件 | 建议版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 工程用javax.servlet,JDK 8 最省事 |
| Tomcat | 8.5.x | 支持 Servlet 3.0 注解,@WebServlet才能生效 |
| MySQL | 5.7 或 8.0 | 8.0 需换com.mysql.cj.jdbc.Driver并加时区参数 |
| 驱动 jar | mysql-connector-java 5.1.x / 8.0.x | 必须与 MySQL 版本匹配 |
| 构建 | Eclipse Dynamic Web Project | 无 Maven,jar 直接放WEB-INF/lib |
这张表里最容易出问题的是驱动版本。MySQL 8.0 用 5.1 的驱动连,会抛Unknown system variable 'query_cache_size';反过来 5.7 用 8.0 驱动,常见的是Public Key Retrieval is not allowed。判断依据很简单:看db.properties里的 driver 类名,com.mysql.jdbc.Driver配 5.x,com.mysql.cj.jdbc.Driver配 8.x,两边必须一致。
Tomcat 版本也别随手换 10。Tomcat 10 之后 Servlet API 包名从javax.servlet改成jakarta.servlet,这个工程的 import 全是前者,扔进 Tomcat 10 会满屏红色,改起来是体力活。课设阶段没有理由折腾这个,锁死 8.5 最稳。
3.2 建库与连接配置:db.properties 和 JDBCUtils
导入sql/cms.sql之后,第一件事是改连接参数。参数放在src/db.properties,跟着 classpath 走,不写死在代码里:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/cms?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456URL 里那串参数不是装饰。useUnicode+characterEncoding=utf8决定中文能不能进库;serverTimezone不写,8.0 驱动在插入 DATETIME 时可能整体偏移 8 小时,预约时间全错;allowPublicKeyRetrieval=true是 8.0 默认加密方式下本地连接的通行证。少写一个,报错信息都指向别处,非常难猜。
配套的工具类把Class.forName和getConnection收在一处:
public class JDBCUtils { private static String url, username, password; static { // 类加载时读一次配置,避免每次取连接都做 IO try (InputStream in = JDBCUtils.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties p = new Properties(); p.load(in); url = p.getProperty("jdbc.url"); username = p.getProperty("jdbc.username"); password = p.getProperty("jdbc.password"); Class.forName(p.getProperty("jdbc.driver")); // 注册驱动 } catch (Exception e) { // 配置错就别让应用半死不活地跑起来 throw new ExceptionInInitializerError("数据源初始化失败: " + e.getMessage()); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection c, Statement s, ResultSet r) { // 关闭顺序:ResultSet -> Statement -> Connection,反过来会抛异常 try { if (r != null) r.close(); } catch (SQLException ignored) {} try { if (s != null) s.close(); } catch (SQLException ignored) {} try { if (c != null) c.close(); } catch (SQLException ignored) {} } }静态块只在类第一次被加载时执行一次,配置读一次就够;把异常包成ExceptionInInitializerError而不是吞掉打印,是为了让错误在启动阶段就暴露,而不是等到第一个查询才报NullPointerException。close方法的顺序不能反,先关 Connection 会导致 Statement 关闭时拿不到连接,抛SQLException: 连接已关闭,这种日志看着吓人,其实只是顺序问题。
注意:db.properties必须放在源码根目录(即 classpath 根),放错位置getResourceAsStream返回 null,紧接着p.load(null)抛空指针,报错信息里不会出现“文件没找到”这几个字。
3.3 启动自检:从登录页到教室列表的三步验证
配好之后别急着点业务,按顺序验证三层,出问题能立刻定位在哪一环。
# 第一步:确认库和表在 mysql -uroot -p -e "use cms; show tables; select count(*) from classroom;" # 第二步:确认工程能编译并部署(Eclipse 下等价于 Add and Remove) # 把项目加到 Tomcat,启动后看 catalina.out 有没有 SEVERE # 第三步:访问入口页,能出数据说明 JDBC 通了 curl -i http://localhost:8080/ClassroomMS/jsp/login.jsp第一步输出少于四张表,说明cms.sql只导了一半,最常见的原因是脚本里有DROP DATABASE语句而客户端中途断了。第二步看到ClassNotFoundException,去WEB-INF/lib看 jar 在不在、有没有被 Eclipse 的 Deployment Assembly 漏掉。第三步能打开页面但列表空白,去浏览器控制台看请求返回码,200 但无数据就往 DAO 的 SQL 里加打印,500 就直接看堆栈第一行的Caused by。这三步走完,工程基本就活了。
4. 核心链路拆解:一次教室预约从表单到 SQL 的完整走法
这是整个项目唯一有“业务”的地方,其他都是增删改查。把这条件链路讲透,课设答辩被追问并发、事务、校验,都有话可说。
4.1 Servlet 入口:参数校验和防重复提交
Servlet 只做三件事:取参数、转对象、调 service。业务判断一行都不要写在这里。
@WebServlet("/booking/add") public class BookingAddServlet extends HttpServlet { private final BookingService service = new BookingService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 必须写在 getParameter 之前 HttpSession session = req.getSession(); SysUser login = (SysUser) session.getAttribute("loginUser"); if (login == null) { // 未登录直接打回登录页 resp.sendRedirect(req.getContextPath() + "/jsp/login.jsp"); return; } String roomIdStr = req.getParameter("roomId"); String startStr = req.getParameter("startTime"); // yyyy-MM-dd'T'HH:mm String endStr = req.getParameter("endTime"); String purpose = req.getParameter("purpose"); // 服务端校验不能省,前端 required 拦不住 curl if (roomIdStr == null || startStr == null || endStr == null) { req.setAttribute("msg", "参数不完整"); req.getRequestDispatcher("/jsp/bookingForm.jsp").forward(req, resp); return; } Booking b = new Booking(); b.setRoomId(Integer.parseInt(roomIdStr)); b.setUserId(login.getUserId()); b.setStartTime(DateUtil.parse(startStr)); b.setEndTime(DateUtil.parse(endStr)); b.setPurpose(purpose); try { String err = service.book(b.getRoomId(), b.getUserId(), b.getStartTime(), b.getEndTime(), purpose); if (err == null) { resp.sendRedirect(req.getContextPath() + "/booking/list"); } else { req.setAttribute("msg", err); // 把失败原因带回表单 req.getRequestDispatcher("/jsp/bookingForm.jsp").forward(req, resp); } } catch (SQLException e) { log("预约失败", e); req.setAttribute("msg", "系统繁忙,请稍后重试"); req.getRequestDispatcher("/jsp/bookingForm.jsp").forward(req, resp); } } }几个参数值得说。req.setCharacterEncoding("UTF-8")必须放在第一次getParameter之前,放后面等于没写,这是 JSP 时代的老规矩。login.getUserId()从 session 取,绝不从表单取,否则改个隐藏域就能替别人预约。service.book返回String而不是boolean,是为了把“时间已过”“教室容量不够”“该时段已被占用”这类不同原因如实带回页面,比统一提示“操作失败”好得多。
防重复提交这道题,前端disabled按钮只能挡住手快点两次。真正兜底是在表单里放一个一次性 token,提交时比对 session 里的值,用完即删。课设里如果想加分,把这块补上,成本很低,讲起来也扛得住追问。
4.2 冲突检测:一条 SQL 判掉时间重叠
判断两段时间是否重叠,不用把区间拆成分钟去比对,一个不等式就够了:已有开始 < 新结束 AND 已有结束 > 新开始。
SELECT COUNT(1) FROM booking WHERE room_id = ? AND state IN (0, 1) -- 只看待审和已通过,驳回/取消不占教室 AND start_time < ? -- 参数:新预约的 end_time AND end_time > ?; -- 参数:新预约的 start_time这段 SQL 的妙处在于它是标准半开区间语义:[9:00, 10:00)和[10:00, 11:00)判定为不冲突,符合直觉,也不会出现“10 点整到底算前一节还是后一节”的歧义。写成BETWEEN或者start_time >= ? AND start_time <= ?都会漏掉“新预约完全包住旧预约”的情况,这是课设里最典型的设计漏洞,答辩老师一问“如果已有 9 到 11 点,我再约 10 点到 10 点半呢”,答不上来就直接扣分。
参数顺序容易搞混:第一个?是新预约的结束时间,第二个才是开始时间,因为不等式方向反了。我一般会在 DAO 方法上直接写死参数名注释,或者干脆封装成一个TimeRange对象,别让调用方去猜。
索引能不能用上,取决于state IN (0,1)的选择性。数据量小的时候全表扫也无所谓;如果初始化数据造了上千条,EXPLAIN会告诉你它走了idx_room_time,那就说明索引顺序是对的。
4.3 事务边界:把冲突检测和插入关进同一把锁
上面那条 SQL 单独跑没问题,放进业务流程就有一个漏洞:A 查完发现没冲突,还没插入,B 也查完发现没冲突,两个人一起插入,同一间教室同一时段出现两条通过记录。这就是典型的 check-then-act 竞态。
public String book(int roomId, int userId, Date start, Date end, String purpose) throws SQLException { Connection conn = null; try { conn = JDBCUtils.getConn(); conn.setAutoCommit(false); // 关自动提交,事务从这里开始 BookingDao dao = new BookingDao(); // 用 FOR UPDATE 锁住该教室当天的预约行,第二个请求会在这里等待 if (dao.hasConflict(conn, roomId, start, end)) { conn.rollback(); return "该教室在所选时段已被预约"; } dao.insert(conn, roomId, userId, start, end, purpose); conn.commit(); // 全部成功才落盘 return null; } catch (SQLException e) { if (conn != null) conn.rollback(); // 任何一步失败整体回滚 throw e; } finally { if (conn != null) { conn.setAutoCommit(true); // 还原,避免连接池复用时带着脏状态 conn.close(); } } }setAutoCommit(false)是对连接生效的,所以 DAO 的所有方法都必须接收同一个conn参数,不能图省事在 DAO 内部再调JDBCUtils.getConn(),那样拿到的是另一个连接,事务根本没覆盖到插入操作——这是我在别人的课设代码里见过最多的“假事务”,代码里有commit,但并没有起到事务作用。
SELECT ... FOR UPDATE锁的是索引命中的行,配合(room_id, start_time, end_time)索引,锁粒度能控制在单教室范围内,不会把整张表锁死。数据量小的课设里,这一步甚至体现不出差别,但写上去并且能解释清楚“为什么需要它”,是区分 80 分和 95 分的地方。
finally里把setAutoCommit(true)还原这一步经常被漏掉。用DriverManager直连时问题不大,一旦换成连接池,这个连接还回去时仍是手动提交模式,下一个借到它的请求会莫名其妙不生效——数据看着插了,刷新就没了,血泪级的排查体验。
5. 避坑与排查:五处最容易出问题的地方
这些坑我基本都在自己的机器和同学的机器上见过,按出现频率排。
5.1 中文乱码:页面、请求、数据库三层都可能断
现象:表单填“多媒体教室”,列表页显示成???或者教室。
原因:三层编码没对齐。JSP 页面缺<%@ page contentType="text/html;charset=UTF-8" %>,请求没设setCharacterEncoding,库或表不是utf8mb4,任何一层断了都乱码。
解决:先看浏览器里页面本身的中文正不正常,正常就说明是请求或库的问题;再SHOW CREATE TABLE classroom看字符集;最后检查 Tomcat 8 的server.xml里 Connector 有没有加URIEncoding="UTF-8"。三处统一成 UTF-8,问题必消。
5.2 时间插入偏移 8 小时
现象:页面上选了 14:00,存进库变成 06:00,或者列出来差八小时。
原因:MySQL 8.0 驱动的serverTimezone没配,或者配成了UTC,而 JVM 默认时区是Asia/Shanghai,两边对不上。
解决:URL 里显式写serverTimezone=Asia/Shanghai,别依赖默认值。另外如果实体类用的是java.util.Date,DAO 里务必用ps.setTimestamp(index, new Timestamp(date.getTime())),用setDate会把时分秒直接裁掉,预约时间全变成 00:00。
5.3 外键约束导致删除教室失败
现象:管理页点“删除教室”,抛Cannot delete or update a parent row。
原因:booking表里有该教室的历史记录,fk_booking_room拦住了删除。
解决:别急着改 SQL 加ON DELETE CASCADE,那会把别人的预约记录一起删掉,属于事故级操作。正确做法是把教室status置 0 做逻辑删除,列表查询加WHERE status = 1,历史数据保留,这也是真实系统的常规做法。
5.4 连接不关导致 Too many connections
现象:演示到一半所有页面报Too many connections,重启 Tomcat 才能继续。
原因:DAO 里遇到异常直接throw,没走finally关连接,每失败一次泄漏一个。
解决:把关闭逻辑统一放进JDBCUtils.close,DAO 里用 try-finally 包住,或者至少改用 try-with-resources。课设不必上连接池,但关闭纪律一定要有,否则答辩现场翻车只需要点几下。
5.5 分页总数和列表对不上
现象:列表显示 10 条,页码却算出 30 页,或者最后一页是空的。
原因:SELECT COUNT(1)和SELECT *的WHERE条件不一致,常见于复制粘贴时漏改了某个筛选条件;或者LIMIT的偏移量算错。
解决:把WHERE子句抽成一个字符串常量两边复用,参数也复用同一个List。LIMIT写成LIMIT ?, ?,第一个参数是(pageNo - 1) * pageSize,第二个是pageSize,别把顺序写反——写反了不报错,只是数据看起来“少了”。
6. 二次开发改造:三个改动让课设从“能跑”到“拿得出手”
跑通只是及格线,真正拉开差距的是你在这个骨架上加了什么。按投入产出比排,我建议动这三处。
第一处,把列表页换成带条件的分页检索。原工程如果是一次性SELECT *全查,教室多起来页面就会变得很长。做法是 DAO 加一个findByPage(String building, Integer type, int pageNo, int pageSize),内部拼动态WHERE,count 和 list 复用同一套条件。加分点在于讲清楚“为什么用LIMIT而不是查出全部再在内存里截断”——后者在数据量上千时会把内存和响应时间一起拉爆。
第二处,加一个登录权限过滤器,而不是在每个 Servlet 里写if (login == null)。
@WebFilter(urlPatterns = {"/booking/*", "/admin/*"}) public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r = (HttpServletRequest) req; HttpServletResponse p = (HttpServletResponse) resp; HttpSession session = r.getSession(false); // false:没有就别新建 if (session == null || session.getAttribute("loginUser") == null) { p.sendRedirect(r.getContextPath() + "/jsp/login.jsp"); return; } chain.doFilter(req, resp); // 放行 } }getSession(false)这个参数有讲究:传true或省略,会给每个未登录请求也创建一个 session,白白占内存,还会干扰“登录超时”的判定。过滤器统一拦,Servlet 里就能删掉一堆重复判断,代码短了,逻辑也清楚了。
第三处,如果时间还够,把原生 JDBC 换到 MyBatis,只换 DAO 层,service 和 servlet 一行不动。这正好能证明你的分层是有效的,也是答辩时最容易讲出彩的一段。改造前先测一遍现有功能,记下返回结果,换完之后逐条对照——别一上来就改业务,先让mvn test或者手工用例全绿,再动别的。
| 改造项 | 改动范围 | 工作量 | 风险点 |
|---|---|---|---|
| 条件分页检索 | DAO + JSP | 半天 | count 与 list 条件不一致 |
| 权限过滤器 | 新增 Filter + web.xml | 2 小时 | 忘记放行登录页和静态资源 |
| JDBC 换 MyBatis | dao 包 + 配置文件 | 1 天 | 事务边界从 Connection 转到 SqlSession |
我自己吃过的一次亏是换 ORM 时把事务一起换了:原来service里手动commit,换成 MyBatis 之后SqlSession没有提交,插入当场“成功”,刷新页面全没了,查了快两个小时才发现是openSession()默认不自动提交。从那以后我每次换持久层,都强制先跑一遍“插入后立刻查询”的验证用例,确认事务边界落在哪儿,再动其他代码。希望这套拆解帮到你,把课设从“交得出去”做成“讲得清楚”。
本文还有配套的精品资源,点击获取