数据库课设实战:学生选课系统设计与事务优化 简介本资源是一套完整的数据库课程设计实践项目面向计算机专业本科生及数据库初学者聚焦学生选课信息管理这一典型教学场景提供从需求分析、系统开发到数据库部署的全流程参考实现。压缩包共278个文件涵盖40个核心Java源码含AccountController、XuankeInfoService等分层模块、87个XML配置与映射文件、26个HTML前端页面、20个JS交互脚本及12个CSS样式文件辅以MySQL建库脚本与运行日志整体仅2.75MB轻量易部署。已有637人学习下载说明其在课程设计与期末大作业场景中具备较强实用性与认可度。读者可直接导入IDE运行系统功能完整覆盖学生、教师、管理员三类角色操作界面简洁、注释详尽特别适合理解MVC分层架构、JDBC连接池、MyBatis映射逻辑及基础CRUD业务闭环是夯实数据库应用开发能力的优质入门范例。1. 为什么一个“学生选课信息管理系统”能成为数据库课设的硬通货这不是又一个 CRUD 练手 Demo。当你在课程设计答辩现场看到评审老师盯着你系统里「选课冲突检测」弹窗时微微点头或在导出 Excel 的按钮按下后后台 0.3 秒内返回含学分统计、开课院系分布、教师授课负荷的聚合报表——那一刻你就知道这个 Java MySQL 的选课系统已经越过了“能跑”踩进了“像真系统”的临界区。它背后是事务隔离级别的实际取舍比如选课高峰期如何避免超选、外键约束与级联删除的边界权衡删一个专业要不要连带清空其下所有课程记录、以及 JDBC 批量插入时rewriteBatchedStatementstrue这种 MySQL 特有优化的真实生效路径。适合刚学完范式理论、正卡在“怎么把 E-R 图变成带索引和约束的表结构”的同学也适合想用最小技术栈不加 Spring Boot、不碰 Redis验证数据库设计完整链路的实践者。它不炫技但每一步都踩在数据库课程核心能力点上建模 → 建表 → 约束 → 事务 → 查询优化 → 应用集成。2. 从 E-R 图到可运行的 MySQL 表结构5 张核心表的设计逻辑与建表语句课程设计最容易翻车的起点不是写 Java 代码而是建错表。很多同学直接照着需求文档字段堆表结果在实现“学生同一学期不能重复选同一门课”时才发现没主键组合、没唯一索引、没外键引用最后靠 Java 层 if 判断硬拦——这已经脱离数据库课设的本质了。下面这 5 张表是我带过三届学生验证过的最小完备集合每张表都对应一个明确的业务契约且约束全部落在数据库层。2.1 学生表student主键、非空、唯一性三重锚定CREATE TABLE student ( stu_id CHAR(10) PRIMARY KEY COMMENT 学号主键, stu_name VARCHAR(20) NOT NULL COMMENT 姓名, gender ENUM(男, 女) NOT NULL DEFAULT 男, major VARCHAR(30) NOT NULL COMMENT 专业, enrollment_year YEAR NOT NULL COMMENT 入学年份, UNIQUE KEY uk_stu_id (stu_id), INDEX idx_major (major) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生基本信息表;逻辑说明stu_id用CHAR(10)而非INT是因为学号含字母如2023CS001强制字符类型避免隐式转换UNIQUE KEY显式声明比仅靠PRIMARY KEY更清晰表达“学号全局唯一”这一业务规则INDEX idx_major是为后续按专业统计选课人数埋下的伏笔——别等写完 Java 才发现GROUP BY major慢得无法忍受。2.2 课程表course软删除标记与开课状态分离CREATE TABLE course ( course_id CHAR(8) PRIMARY KEY COMMENT 课程编号如 CS101, course_name VARCHAR(50) NOT NULL COMMENT 课程名称, credit TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT 学分, dept VARCHAR(30) NOT NULL COMMENT 开课院系, is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT 是否启用1-启用0-停开软删除, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_dept_active (dept, is_active) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程基本信息表含软删除;参数说明is_active字段是血泪经验——千万别用DELETE FROM course WHERE ...真删一旦删掉某门课所有历史选课记录里的course_id就变成孤儿键报表全崩。TINYINT(1)存布尔值是 MySQL 兼容性最优解比BOOLEAN更稳复合索引idx_dept_active直接支撑“查询计算机学院当前开设的所有课程”这类高频 SQL避免全表扫描。2.3 教师表teacher职称字段预留扩展空间CREATE TABLE teacher ( tch_id CHAR(8) PRIMARY KEY COMMENT 教师工号, tch_name VARCHAR(20) NOT NULL, title ENUM(助教, 讲师, 副教授, 教授) NOT NULL DEFAULT 讲师, dept VARCHAR(30) NOT NULL, contact VARCHAR(15) COMMENT 手机号/邮箱 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师信息表;2.4 开课计划表class_schedule解决“同一门课多个班次”的关键枢纽CREATE TABLE class_schedule ( schedule_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 排课ID自增主键, course_id CHAR(8) NOT NULL COMMENT 关联课程, tch_id CHAR(8) NOT NULL COMMENT 授课教师, semester VARCHAR(10) NOT NULL COMMENT 学期如 2023-2024-2, class_time VARCHAR(50) COMMENT 上课时间如 周一 3-4 节, location VARCHAR(30) COMMENT 上课地点, max_capacity SMALLINT UNSIGNED NOT NULL DEFAULT 60 COMMENT 最大容量, current_enroll SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 当前已选人数, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT, FOREIGN KEY (tch_id) REFERENCES teacher(tch_id) ON DELETE RESTRICT, UNIQUE KEY uk_course_semester (course_id, semester), INDEX idx_semester (semester), INDEX idx_tch (tch_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程排班计划表一门课可多班次;关键设计点这是整个系统最易被忽略的表。没有它“数据结构”课在 2023-2024-1 学期开了两个班A班机房、B班阶梯教室你就只能在course表里硬塞两条记录彻底破坏课程实体唯一性。FOREIGN KEY ... ON DELETE RESTRICT是重点——删教师前必须先清空其排课否则数据库直接报错逼你写出清理逻辑而不是让 Java 层默默吞掉异常。2.5 选课记录表enrollment事务安全与并发控制的主战场CREATE TABLE enrollment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_id CHAR(10) NOT NULL, schedule_id BIGINT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM(已选, 退选, 审核中) NOT NULL DEFAULT 已选, FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (schedule_id) REFERENCES class_schedule(schedule_id) ON DELETE CASCADE, UNIQUE KEY uk_stu_schedule (stu_id, schedule_id), INDEX idx_schedule_status (schedule_id, status), INDEX idx_stu_status (stu_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生选课记录表含退选状态;为什么用BIGINT自增主键因为stu_id schedule_id组合虽可作联合主键但 Java 层处理外键关联如查某学生所有选课详情时JDBCResultSet取值、MyBatis 映射、Spring Data JPA 实体定义全要适配复合主键徒增复杂度。BIGINT单主键 UNIQUE KEY uk_stu_schedule是更务实的选择。ON DELETE CASCADE表明学生注销账号时自动清空其所有选课记录——这是数据库层兜底不是靠 Java 循环 delete。3. Java 层核心逻辑落地用原生 JDBC 写透“选课”与“退选”的事务边界不引入任何框架只用mysql-connector-java:8.0.33和原生 JDBC恰恰能暴露数据库课设最该掌握的硬核能力事务控制粒度、SQL 注入防御、连接泄漏预防。下面两段代码是学生最容易抄错、也最容易在答辩时被问住的部分。3.1 “选课”操作四步原子化缺一不可public boolean enrollStudent(String stuId, long scheduleId) { String sql INSERT INTO enrollment (stu_id, schedule_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE status 已选; ; String updateCapacitySql UPDATE class_schedule SET current_enroll current_enroll 1 WHERE schedule_id ? AND current_enroll max_capacity; ; try (Connection conn dataSource.getConnection(); PreparedStatement psInsert conn.prepareStatement(sql); PreparedStatement psUpdate conn.prepareStatement(updateCapacitySql)) { conn.setAutoCommit(false); // 关键关闭自动提交 // 步骤1尝试插入选课记录利用唯一索引防重复 psInsert.setString(1, stuId); psInsert.setLong(2, scheduleId); int insertRows psInsert.executeUpdate(); // 步骤2检查是否真插入成功而非 ON DUPLICATE 触发更新 if (insertRows 0) { conn.rollback(); // 已存在记录回滚 return false; // 提示“已选过” } // 步骤3更新排课表容量带容量校验的原子更新 psUpdate.setLong(1, scheduleId); int updateRows psUpdate.executeUpdate(); // 步骤4容量不足则整体回滚 if (updateRows 0) { conn.rollback(); return false; // 提示“名额已满” } conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { rollbackQuietly(conn); // 自定义静默回滚方法 throw new RuntimeException(选课失败, e); } }参数与逻辑深挖ON DUPLICATE KEY UPDATE是 MySQL 特性依赖enrollment表的uk_stu_schedule唯一索引。它比“先 SELECT 再 INSERT”少一次网络往返且避免竞态条件两个请求同时查到“未选”然后都 INSERT 成功。current_enroll max_capacity放在UPDATE的WHERE子句里是数据库层硬校验。哪怕 Java 层算错数据库也绝不允许超限。rollbackQuietly()必须自己写if (conn ! null !conn.isClosed()) try { conn.rollback(); } catch (SQLException ignored) {}—— 否则conn.rollback()抛异常时外层catch会再抛掩盖原始错误。3.2 “退选”操作状态更新 容量回退的双向一致性public boolean dropCourse(String stuId, long scheduleId) { String sql UPDATE enrollment SET status 退选 WHERE stu_id ? AND schedule_id ? AND status 已选; ; String updateCapacitySql UPDATE class_schedule SET current_enroll current_enroll - 1 WHERE schedule_id ? AND current_enroll 0; ; try (Connection conn dataSource.getConnection(); PreparedStatement psUpdateEnroll conn.prepareStatement(sql); PreparedStatement psUpdateCapacity conn.prepareStatement(updateCapacitySql)) { conn.setAutoCommit(false); psUpdateEnroll.setString(1, stuId); psUpdateEnroll.setLong(2, scheduleId); int enrollRows psUpdateEnroll.executeUpdate(); if (enrollRows 0) { conn.rollback(); return false; // 未找到“已选”状态的记录 } psUpdateCapacity.setLong(1, scheduleId); int capacityRows psUpdateCapacity.executeUpdate(); if (capacityRows 0) { conn.rollback(); return false; // 容量已为0防止负数 } conn.commit(); return true; } catch (SQLException e) { rollbackQuietly(conn); throw new RuntimeException(退选失败, e); } }为什么不用 DELETE因为历史选课数据要用于教学评估如某课程连续三年选课人数下降需分析原因。status 退选保留记录current_enroll准确反映实时容量二者必须同步——这就是事务存在的根本意义。4. 避坑指南5 个让答辩老师当场皱眉的典型问题与根治方案这些坑90% 的课设报告里都藏着只是学生自己没意识到。我整理了近三年指导中高频出现的 5 类问题按“现象 → 原因 → 解决”给出可立即执行的修正动作。4.1 现象Java 控制台打印com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation原因MySQL 表字段长度如VARCHAR(20)小于 Java 传入的字符串如学生姓名“欧阳修远”共 5 字UTF8MB4 下占 20 字节但VARCHAR(20)按字符计实际只能存 20 个 ASCII 字符或 5 个中文。解决所有VARCHAR字段统一按字节数预估中文名按 4 字节/字计算stu_name VARCHAR(20)→ 改为VARCHAR(80)在建表语句末尾加ROW_FORMATDYNAMICMySQL 5.7 默认但显式声明更稳妥Java 层PreparedStatement.setString()前加日志log.debug(stu_name length: {}, name.length())快速定位超长输入源。4.2 现象选课高峰期模拟 50 并发出现“同一门课被选超 1 人”原因INSERT ... ON DUPLICATE KEY UPDATE只保证单条 SQL 原子性但UPDATE class_schedule是另一条 SQL两个操作之间存在微小时间窗口被其他线程钻空。解决将两步合并为一条 SQL需 MySQL 8.0.19INSERT INTO enrollment (stu_id, schedule_id) VALUES (?, ?) ON DUPLICATE KEY UPDATE status 已选; UPDATE class_schedule cs JOIN enrollment e ON cs.schedule_id e.schedule_id SET cs.current_enroll cs.current_enroll 1 WHERE e.stu_id ? AND e.schedule_id ? AND e.status 已选 AND cs.current_enroll cs.max_capacity;或更简单在class_schedule表上对schedule_id加SELECT ... FOR UPDATE锁需开启事务// 在事务内先锁住排课记录 String lockSql SELECT * FROM class_schedule WHERE schedule_id ? FOR UPDATE; PreparedStatement psLock conn.prepareStatement(lockSql); psLock.setLong(1, scheduleId); psLock.executeQuery(); // 执行即加锁4.3 现象导出 Excel 时内存溢出OOMjava.lang.OutOfMemoryError: Java heap space原因用ListEnrollment一次性查出 10 万条选课记录再遍历写 ExcelJVM 堆撑爆。解决改用流式查询MySQL JDBC 参数useCursorFetchtruedefaultFetchSize1000Java 层用ResultSet边读边写不缓存全量对象try (Statement stmt conn.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ResultSet rs stmt.executeQuery(SELECT ...)) { rs.setFetchSize(1000); // 关键启用游标 while (rs.next()) { // 直接写入 SXSSFRow不存 List } }4.4 现象student表删不掉报错Cannot delete or update a parent row: a foreign key constraint fails原因enrollment表外键stu_id引用student.stu_id但建表时ON DELETE设为RESTRICT默认而非CASCADE。解决方案一推荐重建表显式声明ON DELETE CASCADE方案二应急手动删子表再删父表DELETE FROM enrollment WHERE stu_id IN (SELECT stu_id FROM student WHERE ...); DELETE FROM student WHERE ...;方案三生产思维永远不要物理删除student改用is_deleted TINYINT(1)字段 全局查询加AND is_deleted 0条件。4.5 现象GROUP BY查询极慢EXPLAIN显示type: ALL全表扫描原因class_schedule表semester字段无索引但查询“2023-2024-2 学期所有课程”时却WHERE semester ?。解决立即执行ALTER TABLE class_schedule ADD INDEX idx_semester (semester);进阶若常查“某学期某院系课程”建复合索引ADD INDEX idx_sem_dept (semester, dept)注意字段顺序等值查询字段放前验证EXPLAIN SELECT * FROM class_schedule WHERE semester 2023-2024-2 AND dept 计算机学院;必须显示type: ref且key列命中索引名。5. 让课设脱颖而出的 3 个进阶技巧从“能运行”到“有思考”做到前面四章你的系统已稳过答辩线。但若想拿高分、被老师记住得在三个地方埋下“设计思考”的钩子——不是炫技而是证明你真正理解了数据库作为系统基石的价值。5.1 用触发器自动维护冗余统计字段替代应用层计算很多同学在“查看某课程选课人数”时每次执行SELECT COUNT(*) FROM enrollment WHERE schedule_id ?。这在数据量小时没问题但当enrollment表超 10 万行COUNT 全表扫描就是性能黑洞。更好的做法在enrollment表上建触发器自动更新class_schedule.current_enroll。DELIMITER $$ CREATE TRIGGER tr_enroll_after_insert AFTER INSERT ON enrollment FOR EACH ROW BEGIN IF NEW.status 已选 THEN UPDATE class_schedule SET current_enroll current_enroll 1 WHERE schedule_id NEW.schedule_id; END IF; END$$ CREATE TRIGGER tr_enroll_after_delete AFTER DELETE ON enrollment FOR EACH ROW BEGIN IF OLD.status 已选 THEN UPDATE class_schedule SET current_enroll current_enroll - 1 WHERE schedule_id OLD.schedule_id AND current_enroll 0; END IF; END$$ DELIMITER ;为什么比 Java 层更新更可靠触发器在数据库内核执行不受应用重启、网络中断、代码 bug 影响。即使 Java 程序崩溃在INSERT enrollment之后、UPDATE class_schedule之前触发器仍会补上。这是用数据库能力兜底业务一致性的经典范式。5.2 为高频查询生成物化视图雏形用事件存储过程定期刷新汇总表课程设计通常不要求物化视图MySQL 原生不支持但你可以用“事件 存储过程”模拟。例如每天凌晨 2 点生成一张course_enroll_summary表存各课程近一学期选课人数、平均分假设有成绩表、教师授课负荷-- 创建汇总表 CREATE TABLE course_enroll_summary ( course_id CHAR(8), semester VARCHAR(10), total_enroll INT DEFAULT 0, avg_score DECIMAL(3,1), last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (course_id, semester) ) ENGINEInnoDB; -- 创建存储过程 DELIMITER $$ CREATE PROCEDURE refresh_course_summary() BEGIN REPLACE INTO course_enroll_summary (course_id, semester, total_enroll, avg_score) SELECT c.course_id, cs.semester, COUNT(e.stu_id), AVG(g.score) FROM course c JOIN class_schedule cs ON c.course_id cs.course_id LEFT JOIN enrollment e ON cs.schedule_id e.schedule_id AND e.status 已选 LEFT JOIN grade g ON e.stu_id g.stu_id AND cs.schedule_id g.schedule_id WHERE cs.semester DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 1 DAY), %Y-%Y-%m) GROUP BY c.course_id, cs.semester; END$$ -- 创建事件每天执行 CREATE EVENT ev_refresh_summary ON SCHEDULE EVERY 1 DAY STARTS 2023-09-01 02:00:00 DO CALL refresh_course_summary(); DELIMITER ;价值点答辩时展示这张表老师立刻明白你在思考“OLAP 场景如何与 OLTP 分离”。查询报表时直查course_enroll_summary毫秒级响应而不再拖慢核心选课事务。5.3 在 JDBC URL 中启用关键性能参数让连接池真正“懂”MySQL很多同学的application.properties里只写jdbc:mysql://localhost:3306/db?useSSLfalse殊不知这会让连接池和数据库互相“猜心思”。必须显式配置# 推荐的最小生产级 JDBC URLMySQL 8.0 spring.datasource.urljdbc:mysql://localhost:3306/student_db?\ useUnicodetrue\ characterEncodingutf8mb4\ serverTimezoneAsia/Shanghai\ useSSLfalse\ allowPublicKeyRetrievaltrue\ rewriteBatchedStatementstrue\ cachePrepStmtstrue\ useServerPrepStmtstrue\ zeroDateTimeBehaviorCONVERT_TO_NULL # HikariCP 连接池关键参数 spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.minimum-idle5 spring.datasource.hikari.connection-timeout30000 spring.datasource.hikari.idle-timeout600000 spring.datasource.hikari.max-lifetime1800000参数作用速查rewriteBatchedStatementstrue将addBatch()多条 INSERT 合并为INSERT INTO ... VALUES (),(),()批量插入性能提升 5~10 倍cachePrepStmtstrueuseServerPrepStmtstrue开启服务端预编译语句缓存避免重复解析 SQLzeroDateTimeBehaviorCONVERT_TO_NULL防止 MySQL 的0000-00-00日期导致 JavaSQLExceptionmax-lifetime180000030 分钟强制连接到期重建规避 MySQLwait_timeout断连导致的Connection reset异常。我带过的某高校课程设计中一个学生就因为把rewriteBatchedStatementstrue写进 URL并在答辩时演示了“1000 条选课记录插入耗时从 8.2 秒降到 0.9 秒”被老师当场追问原理最终拿了满分。技术细节本身不难难的是你愿不愿意在别人忽略的地方多想一层、多试一次、多写一行注释。希望帮到你。本文还有配套的精品资源点击获取