简介:本资源是一套完整的数据库课程设计实践项目,面向计算机专业本科生及数据库初学者,聚焦学生选课业务场景,提供MySQL+Java CS架构的可运行系统与配套文档。系统涵盖学生、教师、管理员三类角色,完整实现个人信息维护、课程/选课/成绩/奖惩等核心教务管理功能,建表规范、逻辑清晰,适合作为数据库原理、Java程序设计或软件工程课程设计参考。压缩包共98个文件,含21个Java源码、66个编译后Class文件、1个SQL建库脚本、1个SQLite数据库文件、1个设计报告Word文档及XML配置、JAR依赖等,总大小1.98MB,结构分明,便于代码阅读与二次开发。已有11834人学习下载,读者可直接导入IDE运行,结合设计报告理解需求分析、ER建模、SQL编写、前后端交互等全流程实现细节,快速掌握数据库应用系统开发的关键实践能力。
1. 为什么一个“学生选课系统”要花三周重搭三次数据库表结构?
这不是又一个教科书式 CRUD 演示。我带过两届数据库课程设计指导,最常看到的翻车现场是:学生用 Java Swing 写完界面、连上 MySQL、跑通登录——结果答辩时被问一句“如果某门课限选 60 人,第 61 个学生点‘选课’按钮,系统怎么保证不超限?事务怎么回滚?并发选课时会不会出现幻读?”当场卡壳。更现实的是,导出 Excel 成绩单时字段错位、教师查课表查不到自己开的课、管理员删了课程但学生已选记录没级联清理……这些不是功能缺陷,是数据一致性设计缺位的直接后果。本方案专为「能过答辩、能跑通、能讲清设计逻辑」而生:基于真实教学场景提炼出 5 类核心业务约束(选课容量控制、先修课校验、学期隔离、成绩录入权限分级、历史数据归档),用 MySQL 8.0 原生特性(CHECK 约束、外键级联、视图封装、存储过程事务控制)+ Java SE 17(无 Spring 依赖,纯 JDBC + 自定义连接池)实现可验证、可调试、可讲解的完整闭环。适合正在赶毕设/课设 deadline 的你——代码即文档,SQL 即逻辑,每张表设计都附带“为什么这么建”的血泪经验。
2. 需求拆解与数据模型设计:从 3 个业务痛点倒推 7 张表的必要性
2.1 业务约束驱动的实体识别:拒绝“用户-课程-订单”万能三表论
很多同学一上来就建user、course、order表,结果发现根本无法表达“张三在 2024 春季学期选了《数据库原理》且成绩为 85 分”这个事实。关键在于:选课行为本身是一个独立实体,它承载着时间、状态、成绩等不可降维的属性。我们从三个高频问题反向推导:
问题1:同一门课不同学期开班,学生只能选当前学期的课
→ 必须分离course(课程基本信息)和class_session(开班实例),后者含semester字段,并作为选课主键的一部分。问题2:学生选课前需满足先修课要求,但先修关系是多对多(一门课可能有多个先修课)
→ 需独立prerequisite关系表,而非在course表里加prereq_id字段(否则无法支持多先修)。问题3:教师录入成绩后,学生端应立即可见,但管理员可批量导入历史成绩,且成绩一旦提交不可修改(仅允许教师修正)
→enrollment表必须包含status('selected'/'graded'/'archived')、grade_time、grader_id字段,并通过 CHECK 约束限制状态流转。
最终确定 7 张表:student、teacher、course、class_session、enrollment、prerequisite、admin_log(操作审计)。其中admin_log常被忽略,但它让“谁在何时删了哪门课”可追溯——这是答辩时体现工程素养的关键细节。
2.2 核心表结构与约束详解:MySQL 8.0 特性落地实录
以下为class_session和enrollment表的建表语句,每行约束均对应一个答辩必问题:
-- class_session:开班实例表(非课程本身!) CREATE TABLE class_session ( session_id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, semester VARCHAR(20) NOT NULL COMMENT '格式:2024-Spring', start_date DATE NOT NULL, end_date DATE NOT NULL, capacity INT NOT NULL DEFAULT 60, current_enrolled INT NOT NULL DEFAULT 0, teacher_id INT NOT NULL, -- 关键约束1:学期格式校验(避免存入'2024春'或'2024-03'等非法值) CONSTRAINT chk_semester_format CHECK (semester REGEXP '^[0-9]{4}-(Spring|Summer|Autumn|Winter)$'), -- 关键约束2:开班时间合理性(结束日期不能早于开始日期) CONSTRAINT chk_date_order CHECK (end_date >= start_date), -- 关键约束3:外键关联课程和教师(级联更新,但禁止级联删除——防止误删教师导致开班失效) FOREIGN KEY (course_id) REFERENCES course(course_id) ON UPDATE CASCADE, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON UPDATE CASCADE ); -- enrollment:选课记录表(核心业务实体) CREATE TABLE enrollment ( enroll_id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, session_id INT NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM('selected', 'graded', 'archived') NOT NULL DEFAULT 'selected', grade DECIMAL(4,1) NULL, grade_time DATETIME NULL, grader_id INT NULL, -- 关键约束4:选课状态机(禁止直接插入'graded'状态,必须由教师操作触发) CONSTRAINT chk_status_transition CHECK ( (status = 'selected' AND grade IS NULL AND grade_time IS NULL AND grader_id IS NULL) OR (status = 'graded' AND grade IS NOT NULL AND grade_time IS NOT NULL AND grader_id IS NOT NULL) OR (status = 'archived') ), -- 关键约束5:成绩范围校验(0-100分,含小数) CONSTRAINT chk_grade_range CHECK (grade IS NULL OR (grade >= 0 AND grade <= 100)), -- 复合唯一索引:同一学生同一开班只能选一次(防重复点击) UNIQUE KEY uk_student_session (student_id, session_id), -- 外键:级联删除选课记录(学生退学时自动清理其所有选课) FOREIGN KEY (student_id) REFERENCES student(student_id) ON DELETE CASCADE, FOREIGN KEY (session_id) REFERENCES class_session(session_id) ON DELETE RESTRICT );提示:
ON DELETE RESTRICT是关键设计。当class_session被删除时,若存在已选课记录,MySQL 直接报错阻止删除——这比ON DELETE CASCADE更安全,因为管理员必须先手动处理(如转课、退费),再删除开班。答辩时可强调:“这是对业务强一致性的主动保护,而非技术偷懒”。
2.3 视图封装:把复杂查询变成“一张表”
学生查课表、教师查所教班级、管理员看选课统计——这些高频查询若每次都在 Java 层拼 SQL,极易出错且难以维护。我们用视图封装业务逻辑:
-- 学生课表视图:含课程名、开班时间、教师名、当前状态、成绩(如有) CREATE VIEW student_schedule AS SELECT s.student_id, s.student_name, c.course_name, cs.semester, cs.start_date, cs.end_date, t.teacher_name, e.status, e.grade, e.enroll_time FROM enrollment e JOIN student s ON e.student_id = s.student_id JOIN class_session cs ON e.session_id = cs.session_id JOIN course c ON cs.course_id = c.course_id JOIN teacher t ON cs.teacher_id = t.teacher_id; -- 教师授课统计视图:含班级人数、平均分、及格率 CREATE VIEW teacher_class_stats AS SELECT t.teacher_id, t.teacher_name, c.course_name, cs.semester, COUNT(e.enroll_id) as enrolled_count, ROUND(AVG(e.grade), 2) as avg_grade, ROUND(COUNT(CASE WHEN e.grade >= 60 THEN 1 END) * 100.0 / COUNT(e.enroll_id), 1) as pass_rate FROM teacher t JOIN class_session cs ON t.teacher_id = cs.teacher_id JOIN course c ON cs.course_id = c.course_id LEFT JOIN enrollment e ON cs.session_id = e.session_id AND e.status = 'graded' GROUP BY t.teacher_id, t.teacher_name, c.course_name, cs.semester;Java 层只需SELECT * FROM student_schedule WHERE student_id = ?,无需关心 JOIN 逻辑。视图还天然支持权限控制:给学生账号只授予SELECT权限,杜绝其直接操作底层表。
3. Java 层核心模块实现:JDBC 原生写法下的事务与并发控制
3.1 连接池与事务管理:不用框架也能稳如磐石
放弃 HikariCP 等第三方池,手写轻量连接池(仅 200 行代码),重点在于事务边界的显式控制。以“学生选课”为例,需原子性完成:1)检查容量;2)插入选课记录;3)更新开班当前人数。任何一步失败必须全部回滚:
// StudentService.java public boolean selectCourse(int studentId, int sessionId) { Connection conn = null; PreparedStatement psCheck = null; PreparedStatement psInsert = null; PreparedStatement psUpdate = null; try { conn = DataSource.getConnection(); // 自定义连接池获取 conn.setAutoCommit(false); // 关键:关闭自动提交 // 步骤1:检查容量(SELECT FOR UPDATE 锁定该开班记录,防并发超限) psCheck = conn.prepareStatement( "SELECT capacity, current_enrolled FROM class_session WHERE session_id = ? FOR UPDATE"); psCheck.setInt(1, sessionId); ResultSet rs = psCheck.executeQuery(); if (!rs.next()) throw new RuntimeException("开班不存在"); int capacity = rs.getInt("capacity"); int current = rs.getInt("current_enrolled"); if (current >= capacity) { throw new RuntimeException("选课人数已达上限"); } // 步骤2:插入选课记录 psInsert = conn.prepareStatement( "INSERT INTO enrollment (student_id, session_id) VALUES (?, ?)", Statement.RETURN_GENERATED_KEYS); psInsert.setInt(1, studentId); psInsert.setInt(2, sessionId); psInsert.executeUpdate(); // 步骤3:更新开班人数 psUpdate = conn.prepareStatement( "UPDATE class_session SET current_enrolled = current_enrolled + 1 WHERE session_id = ?"); psUpdate.setInt(1, sessionId); psUpdate.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException("选课失败:" + e.getMessage(), e); } finally { // 关闭资源(省略具体 close 代码) closeResources(psCheck, psInsert, psUpdate, conn); } }设计说明:
SELECT ... FOR UPDATE是核心。它在检查容量时对class_session记录加行锁,确保并发请求不会同时读到current_enrolled=59并都执行+1,从而避免超限。这是 MySQL InnoDB 的悲观锁机制,比应用层加 synchronized 更可靠。
3.2 成绩录入模块:状态机驱动的权限校验
教师录入成绩时,必须验证:1)该教师是否教授此开班;2)该学生确属此开班;3)当前状态为 'selected'(未评分)。我们用存储过程封装校验逻辑,Java 层只调用:
-- 存储过程:安全录入成绩 DELIMITER $$ CREATE PROCEDURE safe_grade_student( IN p_session_id INT, IN p_student_id INT, IN p_grade DECIMAL(4,1), IN p_teacher_id INT, OUT p_result VARCHAR(50) ) BEGIN DECLARE v_status VARCHAR(20); DECLARE v_teacher_check INT DEFAULT 0; -- 校验1:教师是否教授此开班 SELECT COUNT(*) INTO v_teacher_check FROM class_session WHERE session_id = p_session_id AND teacher_id = p_teacher_id; IF v_teacher_check = 0 THEN SET p_result = '权限错误:您未教授此课程'; LEAVE proc_label; END IF; -- 校验2:学生是否在此开班且状态为 selected SELECT status INTO v_status FROM enrollment WHERE session_id = p_session_id AND student_id = p_student_id; IF v_status != 'selected' THEN SET p_result = CONCAT('状态错误:当前状态为 ', v_status); LEAVE proc_label; END IF; -- 执行更新 UPDATE enrollment SET status = 'graded', grade = p_grade, grade_time = NOW(), grader_id = p_teacher_id WHERE session_id = p_session_id AND student_id = p_student_id; SET p_result = '成绩录入成功'; END$$ DELIMITER ;Java 调用:
CallableStatement cs = conn.prepareCall("{CALL safe_grade_student(?, ?, ?, ?, ?)}"); cs.setInt(1, sessionId); cs.setInt(2, studentId); cs.setBigDecimal(3, BigDecimal.valueOf(85.5)); cs.setInt(4, teacherId); cs.registerOutParameter(5, Types.VARCHAR); cs.execute(); String result = cs.getString(5); // 获取返回结果优势:校验逻辑在数据库层,避免网络传输中被篡改;存储过程可被审计;Java 层代码极度简洁,专注 UI 交互。
3.3 批量导入模块:Excel 解析与事务分块
管理员需导入历史成绩(Excel 文件),但单次插入 5000 条易超 MySQLmax_allowed_packet。我们采用分块事务(每 100 条提交一次):
// AdminService.java public void importGradesFromExcel(File excelFile) throws Exception { List<GradeRecord> records = ExcelParser.parse(excelFile); // 自定义解析器 Connection conn = DataSource.getConnection(); conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement( "UPDATE enrollment SET status='graded', grade=?, grade_time=NOW(), grader_id=? " + "WHERE session_id=? AND student_id=?" ); for (int i = 0; i < records.size(); i++) { GradeRecord r = records.get(i); ps.setBigDecimal(1, r.getGrade()); ps.setInt(2, r.getAdminId()); ps.setInt(3, r.getSessionId()); ps.setInt(4, r.getStudentId()); ps.addBatch(); // 每100条提交一次 if ((i + 1) % 100 == 0 || i == records.size() - 1) { ps.executeBatch(); conn.commit(); // 重置批处理(重要!否则下次addBatch会累加) ps.clearBatch(); } } }注意:
ps.clearBatch()不可省略,否则第二次executeBatch()会执行前 100 条 + 新 100 条,造成重复更新。
4. 避坑指南:那些让答辩老师皱眉的 4 个致命细节
4.1 现象:学生选课后,开班人数没增加,但选课记录已存在
原因:UPDATE class_session SET current_enrolled = current_enrolled + 1语句未加 WHERE 条件,或 WHERE 条件写错(如session_id = ?写成course_id = ?),导致更新了错误记录甚至全表。
解决:在UPDATE后立即执行SELECT ROW_COUNT(),若返回 0 则抛异常;或在 Java 层用executeUpdate()返回值校验(if (rowsAffected != 1) throw new RuntimeException("更新开班人数失败"))。
4.2 现象:教师查自己所教班级,视图teacher_class_stats中pass_rate为 NULL
原因:LEFT JOIN enrollment后,COUNT(e.enroll_id)对空集返回 0,但COUNT(CASE WHEN e.grade >= 60 THEN 1 END)在无记录时也返回 0,导致0 * 100.0 / 0产生除零错误,MySQL 返回 NULL。
解决:改用COALESCE包裹分母:ROUND(COUNT(CASE WHEN e.grade >= 60 THEN 1 END) * 100.0 / NULLIF(COUNT(e.enroll_id), 0), 1)。NULLIF(x,0)在 x=0 时返回 NULL,使整个除法结果为 NULL,再经ROUND(NULL,1)得 NULL,符合业务语义(无学生则无及格率)。
4.3 现象:MySQL Workbench 导出的 SQL 文件,在另一台机器执行时报错 “Unknown storage engine ‘InnoDB’”
原因:目标 MySQL 版本过低(< 5.5)或配置禁用了 InnoDB(skip-innodb)。本方案所有表均指定ENGINE=InnoDB,因其支持事务、外键、行锁。
解决:导出时勾选 “Export to Self-Contained File”,并在文件开头添加SET default_storage_engine=INNODB;;或检查目标库SHOW ENGINES;,确认 InnoDB 为DEFAULT或YES。
4.4 现象:Java 运行时报java.sql.SQLException: The server time zone value 'XXX' is unrecognized
原因:MySQL 服务器时区与 JVM 时区不匹配,常见于 Windows 本地 MySQL 时区为CST(中国标准时间),而 Java 解析为美国中部时间。
解决:在 JDBC URL 中强制指定时区:jdbc:mysql://localhost:3306/studentdb?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。切记:serverTimezone参数必须存在,且值必须为 IANA 时区名(如Asia/Shanghai),不可用GMT+8。
提示:以上四点均来自真实答辩翻车案例。建议在项目 README.md 中单独建 “FAQ” 章节,把解决方案复制粘贴进去——老师看到你会主动预判问题,印象分会直线上升。
5. 答辩验收清单:用 5 个可演示动作证明你真懂设计
5.1 动作1:现场演示“并发选课不超限”的压测验证
别只说“我用了 SELECT FOR UPDATE”。打开两个命令行窗口,分别执行:
# 窗口1:模拟学生A选课(先执行,不提交) mysql -u root -p -e "START TRANSACTION; SELECT capacity, current_enrolled FROM class_session WHERE session_id = 101 FOR UPDATE;" # 窗口2:模拟学生B同时选课(此时会被阻塞) mysql -u root -p -e "SELECT capacity, current_enrolled FROM class_session WHERE session_id = 101 FOR UPDATE;"然后在窗口1执行COMMIT;,窗口2 立即返回结果——证明行锁生效。再用 Java 程序发起 100 个并发选课请求,监控class_session.current_enrolled最终值严格等于capacity。这是答辩时最硬核的证据。
5.2 动作2:用一条 SQL 揭示你的数据完整性设计
向老师展示这条语句的执行结果:
-- 查询所有违反先修课规则的选课记录(应为空集) SELECT s.student_name, c1.course_name as target_course, c2.course_name as prerequisite_course FROM enrollment e1 JOIN class_session cs1 ON e1.session_id = cs1.session_id JOIN course c1 ON cs1.course_id = c1.course_id JOIN prerequisite p ON c1.course_id = p.course_id JOIN course c2 ON p.prereq_id = c2.course_id LEFT JOIN enrollment e2 ON e1.student_id = e2.student_id JOIN class_session cs2 ON e2.session_id = cs2.session_id JOIN course c3 ON cs2.course_id = c3.course_id WHERE c2.course_id = c3.course_id AND e2.status = 'graded'; -- 学生已通过先修课如果结果为空,说明你的先修课校验逻辑(在 Java 层选课前查询)真正起效;如果不为空,则设计有漏洞。把“如何保证数据正确”转化为可验证的 SQL,比讲一百遍理论都有力。
5.3 动作3:导出一份“可审计”的操作日志
运行管理员删除课程操作后,执行:
SELECT action_type, table_name, record_id, old_value, new_value, operator_name, action_time FROM admin_log WHERE action_time > DATE_SUB(NOW(), INTERVAL 1 HOUR) ORDER BY action_time DESC;展示日志中清晰记录了“谁、何时、删了哪门课、原课程名是什么”。这证明你理解“系统可维护性”不仅是功能,更是责任追溯。答辩老师最爱问“如果数据错了,怎么查?”——这份日志就是你的后悔药。
5.4 动作4:对比“有视图”和“无视图”的 Java 代码复杂度
准备两张截图:
- 左图:未用视图时,Java 中查询学生课表需写 5 表 JOIN 的 20 行 SQL;
- 右图:使用
student_schedule视图后,SQL 缩减为SELECT * FROM student_schedule WHERE student_id = ?,Java 层ResultSet取值逻辑从 15 行减至 5 行。
结论:视图不是炫技,是降低耦合、提升可维护性的工程实践。
5.5 动作5:现场修改一个约束并验证其效果
在答辩现场,执行:
-- 临时放宽成绩范围(原为0-100),允许录入105分 ALTER TABLE enrollment DROP CONSTRAINT chk_grade_range; ALTER TABLE enrollment ADD CONSTRAINT chk_grade_range CHECK (grade IS NULL OR (grade >= 0 AND grade <= 105)); -- 尝试插入105分(应成功) INSERT INTO enrollment (student_id, session_id, grade) VALUES (1, 101, 105.0); -- 恢复原约束 ALTER TABLE enrollment DROP CONSTRAINT chk_grade_range; ALTER TABLE enrollment ADD CONSTRAINT chk_grade_range CHECK (grade IS NULL OR (grade >= 0 AND grade <= 100));此举证明你不仅会写约束,更理解其动态管理能力——这是高级工程师和初级开发者的分水岭。
从那以后我每次设计数据库,都会先问自己三个问题:1)这个约束能否被业务方理解?2)它的失效会引发什么连锁反应?3)我能用一条 SQL 现场证明它有效吗?希望帮到你。
本文还有配套的精品资源,点击获取