资讯动态

学生选课系统:数据库课设的事务与并发实战指南

发布时间:2026/10/9 18:20:41 来源:尧图企业网站定制
简介本资源是一套完整的数据库课程设计实践项目面向计算机专业本科生及数据库初学者聚焦学生选课业务场景提供从需求分析、数据库建模到CS架构系统实现的全流程参考。系统基于MySQLJava开发支持学生、教师、管理员三类角色涵盖个人信息维护、课程/选课/成绩/奖惩等核心模块管理建表规范、逻辑清晰适合作为课程设计范例或课程实训补充材料。压缩包共112个文件含21个Java源码、66个编译后Class文件、1个SQL建库脚本、2个Word设计报告文档、14个URL配置及XML/DB/properties等辅助文件整体1.99MB结构完整、即下即用。已有16471人学习下载读者可直接运行调试、对照报告理解E-R建模与JDBC实现细节并通过GUI类如StudentGUI、TeacherGUI、AdmisterGUI深入掌握分层界面设计与权限控制逻辑。1. 为什么一个“学生选课信息管理系统”能成为数据库课设的黄金标尺不是所有课设都能扛住三轮答辩、四次压测、五位老师交叉审阅——但这个用 MySQL Java 搭建的学生选课信息管理系统常年稳居高校数据库课程设计TOP3。它表面是增删改查的“教科书案例”实则是一块压缩饼干事务隔离级别要调、外键级联要验、并发选课要锁、成绩录入要触发器、视图权限要分层、JDBC连接池要配、SQL注入要防……每一个模块都在逼你直面真实业务里的数据一致性、并发控制和边界校验。它不追求炫技但拒绝糊弄不强制微服务但天然暴露单体架构下数据流的脆弱点。适合刚学完范式理论、能写基础SQL、但还没在事务里踩过坑的本科生也适合想带学生从ER图一路推到连接池参数调优的指导教师。如果你的课设还卡在“登录界面能跳转”那这个系统就是你该拆解的第一份「可运行的数据库契约」。2. 从ER图到MySQL建库如何让三张核心表撑起整个业务逻辑学生选课系统看似简单但表结构设计稍有偏差后续所有Java层操作都会变成玄学调试。我带过的几届学生里超六成的“选课失败”问题根源都在建表阶段没吃透一对多、多对多的落地约束。下面这张表不是照抄模板而是按真实教学场景反复打磨出的最小可行结构——它能跑通全部功能且每条SQL都有明确业务语义。2.1 核心三表设计student / course / selection 的字段取舍逻辑提示不要一上来就加id BIGINT AUTO_INCREMENT PRIMARY KEY——先想清楚主键是否真需要全局唯一。学生表用学号作主键自然键课程表用课号作主键选课表用复合主键学号课号这才是符合业务语义的设计。-- 学生表学号为主键不设自增ID CREATE TABLE student ( stu_id CHAR(10) PRIMARY KEY COMMENT 10位学号如2023000001, name VARCHAR(20) NOT NULL, gender ENUM(男, 女) DEFAULT 男, major VARCHAR(30), enrollment_date DATE DEFAULT (CURRENT_DATE) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生基本信息; -- 课程表课号为主键含学分与容量约束 CREATE TABLE course ( course_id CHAR(8) PRIMARY KEY COMMENT 8位课号如CS101001, title VARCHAR(50) NOT NULL, credit TINYINT UNSIGNED CHECK (credit BETWEEN 1 AND 6), capacity SMALLINT UNSIGNED DEFAULT 60, current_enrolled SMALLINT UNSIGNED DEFAULT 0 COMMENT 实时选课人数由触发器维护 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程基本信息; -- 选课表复合主键 外键级联 唯一约束 CREATE TABLE selection ( stu_id CHAR(10) NOT NULL, course_id CHAR(8) NOT NULL, grade TINYINT NULL COMMENT 成绩NULL表示未录入, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (stu_id, course_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT, UNIQUE KEY uk_stu_course (stu_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生选课记录;关键参数说明CHAR(10)vsVARCHAR(10)学号固定长度用CHAR更省空间、索引效率更高课程名长度波动大必须用VARCHAR。ON DELETE CASCADE学生退学时自动清理其所有选课记录——这是业务强需求不是技术炫技。ON DELETE RESTRICT课程停开不能直接删必须先清空选课表否则报错——防止误操作导致数据断裂。current_enrolled字段不靠Java层计数而用触发器维护确保并发选课时不会超限见2.3节。2.2 为什么必须加事务控制一个选课动作背后的三步原子操作用户点击“选课”按钮背后至少涉及三个不可分割的数据变更向selection表插入一条记录将对应课程的current_enrolled值1检查current_enrolled是否超过capacity超限则回滚全部。如果这三步不在同一个事务里就会出现“插入成功但人数没加”或“人数加了但插入失败”的脏数据。下面是MySQL端最简事务封装DELIMITER $$ CREATE PROCEDURE sp_select_course(IN p_stu_id CHAR(10), IN p_course_id CHAR(8)) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 步骤1检查是否已选 IF EXISTS (SELECT 1 FROM selection WHERE stu_id p_stu_id AND course_id p_course_id) THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 该课程已选不可重复选择; END IF; -- 步骤2检查容量是否充足用SELECT ... FOR UPDATE锁住课程行 SELECT current_enrolled, capacity INTO enrolled, cap FROM course WHERE course_id p_course_id FOR UPDATE; IF enrolled cap THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 课程已满请选择其他课程; END IF; -- 步骤3插入选课记录 更新人数 INSERT INTO selection (stu_id, course_id) VALUES (p_stu_id, p_course_id); UPDATE course SET current_enrolled current_enrolled 1 WHERE course_id p_course_id; COMMIT; END$$ DELIMITER ;逻辑说明SELECT ... FOR UPDATE是关键它给目标课程行加行级写锁阻塞其他并发事务对该行的读写避免“检查时未满、插入时已满”的竞态。SIGNAL抛出自定义异常由Java层捕获并友好提示而不是让程序崩在SQLException上。整个存储过程封装了业务规则Java层只需调用CALL sp_select_course(?, ?)降低代码耦合度。2.3 触发器维护实时人数比Java计数更可靠的数据守门员有人问“为什么不用Java查总数再1更直观啊。” —— 因为并发场景下两次查询之间可能已有其他线程完成插入你的“查算更新”三步必然出错。触发器才是数据库层的最终防线DELIMITER $$ CREATE TRIGGER tr_after_insert_selection AFTER INSERT ON selection FOR EACH ROW BEGIN UPDATE course SET current_enrolled current_enrolled 1 WHERE course_id NEW.course_id; END$$ CREATE TRIGGER tr_after_delete_selection AFTER DELETE ON selection FOR EACH ROW BEGIN UPDATE course SET current_enrolled current_enrolled - 1 WHERE course_id OLD.course_id; END$$ DELIMITER ;参数说明AFTER INSERT/DELETE确保只有真实落库的操作才触发避免事务回滚后误更新。NEW.course_id和OLD.course_id分别指向新插入行和被删除行的课程ID无需额外参数传递。不用BEFORE触发器因为BEFORE无法保证事务最终提交若后续回滚人数更新就成脏数据。3. Java层落地JDBC连接池、DAO分层与防SQL注入的硬核写法建好库只是开始Java代码写得松散再严谨的数据库设计也会被绕开。我见过太多课设把SQL拼接写在Servlet里结果一个单引号就让全表被DROP。下面这套写法经受过某高校连续三年课设答辩的拷问——它不追求Spring Boot自动装配而是用最原始的JDBC讲清数据访问的本质。3.1 HikariCP连接池配置5个参数决定系统吞吐上限别用DriverManager.getConnection()硬编码连库课设虽小但连接池是并发安全的基石。HikariCP是当前事实标准配置文件hikari.properties如下# hikari.properties jdbcUrljdbc:mysql://localhost:3306/course_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue usernameroot password123456 driverClassNamecom.mysql.cj.jdbc.Driver # 核心参数按课设规模调优 maximumPoolSize10 minimumIdle3 connectionTimeout30000 idleTimeout600000 maxLifetime1800000 # 关键安全项 leakDetectionThreshold60000参数说明maximumPoolSize10课设并发量低10连接足够设太高反而增加MySQL线程开销。leakDetectionThreshold60000检测连接泄漏忘记close60秒未归还即报警——这是学生最容易犯的错。useSSLfalse本地开发可关SSL但必须显式声明避免MySQL 8.0默认报错。allowPublicKeyRetrievaltrue解决MySQL 8.0密码认证插件兼容问题课设环境必备。3.2 DAO层接口与实现用PreparedStatement封死SQL注入入口所有数据库操作必须通过DAOData Access Object抽象且禁止字符串拼接SQL。以选课为例// SelectionDAO.java public class SelectionDAO { private final DataSource dataSource; public SelectionDAO(DataSource dataSource) { this.dataSource dataSource; } // ✅ 正确使用PreparedStatement占位符 public boolean selectCourse(String stuId, String courseId) throws SQLException { String sql CALL sp_select_course(?, ?); try (Connection conn dataSource.getConnection(); CallableStatement stmt conn.prepareCall(sql)) { stmt.setString(1, stuId); stmt.setString(2, courseId); return stmt.execute(); // 存储过程无返回结果集用execute() } } // ❌ 错误示例绝对禁止 // String sql INSERT INTO selection VALUES ( stuId , courseId ); }逻辑说明CallableStatement调用存储过程复用2.2节的事务逻辑Java层只管传参和捕获异常。所有输入参数都用setString()绑定彻底杜绝 OR 11类注入。try-with-resources自动关闭连接、语句对象避免连接泄漏——配合leakDetectionThreshold双重保险。3.3 Service层事务边界Transactional不是万能膏药课设不用Spring那就手动管理事务。关键原则事务必须包裹完整的业务单元且只在Service层开启。例如成绩录入// GradeService.java public class GradeService { private final SelectionDAO selectionDAO; private final DataSource dataSource; public GradeService(SelectionDAO selectionDAO, DataSource dataSource) { this.selectionDAO selectionDAO; this.dataSource dataSource; } // 录入成绩需保证选课存在 成绩合法 原子更新 public boolean inputGrade(String stuId, String courseId, int grade) throws SQLException { if (grade 0 || grade 100) { throw new IllegalArgumentException(成绩必须在0-100之间); } String sql UPDATE selection SET grade ? WHERE stu_id ? AND course_id ?; try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交 try (PreparedStatement stmt conn.prepareStatement(sql)) { stmt.setInt(1, grade); stmt.setString(2, stuId); stmt.setString(3, courseId); int affected stmt.executeUpdate(); if (affected 0) { throw new SQLException(未找到该学生的选课记录); } conn.commit(); // 显式提交 return true; } catch (SQLException e) { conn.rollback(); // 出错回滚 throw e; } } } }关键点conn.setAutoCommit(false)必须在try块内获取连接后立即设置否则后续commit()无效。rollback()放在catch里且必须throw e让上层知道失败不能静默吞掉异常。不在DAO层开事务DAO只做单表CRUD事务是Service组合多个DAO的协调行为。4. 避坑指南课设答辩高频翻车现场与血泪修复方案学生交课设前最怕什么不是功能没做完而是答辩时老师随口一问当场哑火。下面5个坑是我带过37个小组、审过216份报告后总结的「答辩死亡清单」。每一条都附真实现象、根因分析和可立即执行的修复命令。4.1 现象选课成功但课程余量没变或者余量变负数原因current_enrolled字段未用触发器维护而是靠Java层SELECT COUNT(*)再UPDATE——并发时计数失准。解决立即执行以下SQL重建触发器覆盖旧版DROP TRIGGER IF EXISTS tr_after_insert_selection; DROP TRIGGER IF EXISTS tr_after_delete_selection; -- 然后粘贴2.3节的两个CREATE TRIGGER语句注意执行前先SELECT * FROM course确认当前余量是否正常若已错乱需手动UPDATE course SET current_enrolled (SELECT COUNT(*) FROM selection s WHERE s.course_id course.course_id)重置。4.2 现象中文姓名/课程名存进数据库变成???原因MySQL服务端、数据库、表、连接URL四层字符集不统一常见于未显式指定charsetutf8mb4。解决四步全检查MySQL服务端SHOW VARIABLES LIKE character_set%;→ 确保character_set_serverutf8mb4查数据库SHOW CREATE DATABASE course_db;→ 若非DEFAULT CHARSETutf8mb4执行ALTER DATABASE course_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;查表SHOW CREATE TABLE student;→ 同理ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;查JDBC URL确认含?useUnicodetruecharacterEncodingutf8mb4HikariCP中已配见3.1节。4.3 现象删除学生后选课记录还在但查不到学生信息原因建表时FOREIGN KEY (stu_id) REFERENCES student(stu_id)没加ON DELETE CASCADE或建表后手动删了外键。解决用SHOW CREATE TABLE selection;查看当前外键定义若缺失则重建ALTER TABLE selection DROP FOREIGN KEY fk_selection_stu_id, ADD CONSTRAINT fk_selection_stu_id FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE;4.4 现象调用存储过程时报PROCEDURE course_db.sp_select_course does not exist原因MySQL 8.0默认禁用log_bin_trust_function_creators导致存储过程创建失败但错误不明显。解决以root登录MySQL执行SET GLOBAL log_bin_trust_function_creators 1; -- 然后重新执行2.2节的DELIMITER脚本提示此设置重启MySQL会失效课设演示前务必检查。4.5 现象JDBC报java.sql.SQLException: Connection is closed原因DAO方法中Connection未用try-with-resources或Service层手动conn.close()后又在finally里再关一次。解决全局搜索conn.close()全部替换为try (Connection conn ...) { ... }结构。特别注意嵌套DAO调用时不要在内部DAO里关连接——连接应由最外层Service统一管理。5. 报告撰写技巧让评审老师一眼抓住你的数据库设计深度课设报告不是代码说明书而是你作为“数据库设计师”的思维留痕。老师扫一眼目录就要判断你是否真懂范式、事务、索引这些词背后的重量。我建议把报告拆成三块硬核内容每块都用对比表格截图一句话结论呈现拒绝大段文字堆砌。5.1 ER图到关系模式的映射验证表证明你不是瞎画的很多学生ER图画得漂亮转成表就漏了联系属性。用这张表向老师证明你每一步都经过推演ER图元素转换方式本系统落地位置是否满足3NF理由说明学生-课程多对多新建关联实体selectionselection表是无传递依赖所有非主属性完全依赖联合主键课程容量约束作为course表属性capacity字段是capacity仅依赖course_id无其他函数依赖选课时间作为selection属性select_time字段是时间戳是原子值不参与任何函数依赖提示在报告里放一张手绘ER图拍照 一张PowerPoint绘制的关系模式图标注主键下划线、外键FK、联系属性*比纯文字有力十倍。5.2 索引优化效果对比用EXPLAIN截图说话老师最反感“加了索引性能提升50%”这种虚话。必须给出真实EXPLAIN输出。例如为选课查询加索引-- 原始慢查询无索引 EXPLAIN SELECT * FROM selection WHERE stu_id 2023000001; -- 加索引后 CREATE INDEX idx_selection_stu ON selection(stu_id); EXPLAIN SELECT * FROM selection WHERE stu_id 2023000001;报告呈现方式左右并排两张EXPLAIN结果截图重点圈出type列从ALL→refrows从1000→3下方表格总结指标加索引前加索引后改进点typeALLref从全表扫描→索引查找rows12473扫描行数减少99.8%ExtraUsing whereUsing where无临时表/文件排序5.3 事务隔离级别实测用两个窗口模拟并发冲突这是答辩时最能拉开差距的环节。不要只写“我用了READ COMMITTED”要展示它怎么解决实际问题窗口A开启事务START TRANSACTION; SELECT current_enrolled FROM course WHERE course_id CS101001; -- 假设返回 59窗口B同时执行START TRANSACTION; SELECT current_enrolled FROM course WHERE course_id CS101001; -- 也返回 59READ COMMITTED保证不读脏数据 UPDATE course SET current_enrolled 60 WHERE course_id CS101001; COMMIT;窗口A继续UPDATE course SET current_enrolled 60 WHERE course_id CS101001; COMMIT; -- 成功但业务上应拒绝已满结论句写在报告里“READ COMMITTED隔离级别防止了脏读但无法避免‘幻读’导致的超选。因此本系统采用存储过程SELECT ... FOR UPDATE加行锁从应用层补足隔离缺陷——这比单纯调高隔离级别如SERIALIZABLE更高效。”最后说一句实在话我当年第一次写这个系统也在ON DELETE CASCADE上纠结了两天查了七遍MySQL文档才敢提交。数据库设计没有捷径只有把每个外键、每个事务、每个字符集都亲手拧紧你写的才不是Demo而是你工程能力的签名。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑