简介:本资源是一套完整的数据库课程设计实践项目,面向计算机专业本科生及Java+MySQL初学者,聚焦高校学生选课业务场景,提供C/S架构的全功能信息管理系统实现方案。系统严格划分学生、教师、管理员三类角色,覆盖个人信息维护、课程/选课/成绩/奖惩等核心教务管理模块,并附有结构规范的SQL建表脚本与详实的设计报告文档。压缩包共98个文件,含21个Java源码、66个编译后Class字节码、1个可执行jar包、1个SQLite数据库文件(educateSystem.db)、1个SQL初始化脚本及XML配置等,整体仅1.98MB,轻量易部署。目前已有11834人学习下载,读者可直接运行调试、理解三层角色权限控制逻辑、掌握JDBC连接MySQL/SQLite双库实践、复用建表规范与模块化代码结构,是数据库原理与Java桌面应用结合的经典教学案例。
1. 为什么一个“学生选课系统”要花三周重搭三次表结构?——这不是练手Demo,而是数据库设计能力的照妖镜
你手里的课程设计任务书写着“用MySQL+Java实现学生选课系统”,但真正卡住90%同学的,从来不是写不出登录界面,而是——当导师问“为什么选课记录表里不直接存学生姓名和课程名,而必须用外键关联?”时,答不上来;或是答辩现场被追问“如果同一门课允许跨学期重复开设,你的课程表主键怎么设?学期字段要不要进联合主键?”,当场愣住。这不是Java语法或Swing控件的问题,是数据库建模思维没落地。这套方案专为“需要交一份能过审、能讲清设计逻辑、能现场演示增删改查+事务回滚+约束生效”的学生准备:它包含经过三轮真实教学验证的5张核心表(含字段级注释)、Java层JDBC事务封装模板、带边界校验的选课业务逻辑(如课容量超限拦截、已选课程禁止重复提交)、以及最关键的——每张表的设计依据说明文档。适合正在赶毕设/课设DDL、想避开“能跑但经不起问”的同学。
2. 需求拆解与数据建模:从“我要录数据”到“数据该怎么长”
做系统前先扔掉“我要做个界面”的念头。打开需求文档,逐条划出必须由数据库强制保障的规则,这才是建模起点。比如“一个学生一学期同一门课只能选一次”,这句不是前端按钮禁用就能解决的,它要求数据库在插入时自动拒绝重复;再如“课程容量不能超限”,这需要在选课操作中触发原子性检查+更新,否则并发选课会超员。我们把原始需求翻译成数据库语言:
- 实体识别:学生、课程、教师、班级、学期 → 对应5张基础表
- 关系提炼:学生与课程是多对多(需中间表
student_course),教师与课程是多对一(教师ID作为课程表外键),班级与学生是多对多(但本系统简化为学生归属单一班级) - 约束显性化:
student_course表的(student_id, course_id, semester)必须唯一(防重复选课)course表的capacity字段需配合student_course的实时计数做检查- 所有ID字段设为
BIGINT UNSIGNED(避免负数ID引发的玄学报错)
提示:别急着建表!先用纸画出ER图,标出每个关系的基数(1:1 / 1:N / M:N)和参与约束(是否可为空)。我带过的某高校课程设计中,7个组有4个在第二周返工,就因为没标清“教师可以暂无授课课程”这个可空性,导致外键约束把插入教师信息卡死了。
2.1 核心表结构设计:字段命名即契约,注释是给未来自己写的说明书
以下是经教学验证的最小可行表结构(不含日志、审计等扩展字段),每张表都附带设计理由:
-- 学生表:主键用自增ID,不用学号作主键(学号可能变更/升位) CREATE TABLE student ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键,自增', student_no VARCHAR(12) NOT NULL UNIQUE COMMENT '学号,业务唯一标识', name VARCHAR(20) NOT NULL COMMENT '姓名', gender ENUM('M','F') NOT NULL DEFAULT 'M' COMMENT '性别:M男/F女', class_id BIGINT UNSIGNED NOT NULL COMMENT '所属班级ID', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', INDEX idx_class_id (class_id) COMMENT '班级ID索引,加速班级查询' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生基本信息表';设计说明:
student_no设为UNIQUE而非主键,是因为学号可能因政策调整(如从8位升10位)需修改,主键一旦变更会级联破坏所有外键引用;gender用ENUM而非VARCHAR,既节省存储(1字节 vs 至少2字节),又杜绝“Male”/“male”/“男”等不一致输入;INDEX idx_class_id是必要优化:按班级查学生是高频操作(如导出某班名单),不加索引会导致全表扫描。
-- 课程表:学期字段不单独建表,直接作为课程属性(简化模型) CREATE TABLE course ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键', course_code VARCHAR(10) NOT NULL COMMENT '课程代码,如CS101', title VARCHAR(50) NOT NULL COMMENT '课程名称', teacher_id BIGINT UNSIGNED NOT NULL COMMENT '授课教师ID', capacity INT UNSIGNED NOT NULL DEFAULT 60 COMMENT '最大容量', semester VARCHAR(10) NOT NULL COMMENT '开课学期,如2024-1', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ON DELETE RESTRICT, UNIQUE KEY uk_code_semester (course_code, semester) COMMENT '同一课程代码在不同学期可重复' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程信息表';设计说明:
UNIQUE KEY uk_code_semester解决了“同一门课跨学期开设”的核心需求,避免把学期拆成独立表增加复杂度;ON DELETE RESTRICT是关键:删除教师前必须先解除其授课关系,防止出现“无人认领的课程”;capacity设为UNSIGNED,物理上杜绝负数容量(曾有学生误输-30,导致选课逻辑崩溃)。
2.2 中间表设计:多对多关系的落地,不是简单堆两个外键
student_course表是整个系统的业务枢纽,它的设计直接决定选课逻辑能否健壮:
-- 选课记录表:核心业务表,承载所有选课状态 CREATE TABLE student_course ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键', student_id BIGINT UNSIGNED NOT NULL COMMENT '学生ID', course_id BIGINT UNSIGNED NOT NULL COMMENT '课程ID', semester VARCHAR(10) NOT NULL COMMENT '对应学期', status ENUM('selected','dropped','completed') NOT NULL DEFAULT 'selected' COMMENT '选课状态', created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间', -- 复合唯一约束:同一学生在同学期不可重复选同一门课 UNIQUE KEY uk_student_course_semester (student_id, course_id, semester), -- 外键约束,确保学生和课程存在 FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE RESTRICT, -- 索引优化:按学生查课表、按课程查选课人 INDEX idx_student_id (student_id), INDEX idx_course_id (course_id), INDEX idx_semester (semester) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生选课记录表';设计说明:
UNIQUE KEY uk_student_course_semester是防重复选课的终极防线,比Java层校验更可靠(并发场景下Java校验会失效);ON DELETE CASCADE对学生ID:学生退学时自动清理其所有选课记录;ON DELETE RESTRICT对课程ID:课程停开前必须手动处理已选学生,避免数据丢失;- 三个索引覆盖全部查询路径:查某学生所有课(
idx_student_id)、查某课所有学生(idx_course_id)、查某学期所有选课(idx_semester)。
2.3 教师与班级表:用最简结构支撑核心业务
这两张表结构简洁,但字段选择直指业务痛点:
-- 教师表:不存联系方式等非核心字段,聚焦教学关系 CREATE TABLE teacher ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键', teacher_no VARCHAR(10) NOT NULL UNIQUE COMMENT '教师工号', name VARCHAR(20) NOT NULL COMMENT '姓名', department VARCHAR(30) COMMENT '所属院系', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师信息表'; -- 班级表:班级名称即业务标识,不引入年级字段(年级可从名称解析) CREATE TABLE class ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT '主键', class_name VARCHAR(20) NOT NULL UNIQUE COMMENT '班级名称,如计算机2021级1班', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班级信息表';设计说明:
- 教师表省略电话、邮箱等字段,因课程设计不涉及通知功能,加了反而分散评审注意力;
class_name设为UNIQUE,避免出现“计算机2021级1班”和“计科2021-1班”这种语义重复;- 不单独建“年级”字段,因年级信息已隐含在班级名称中(如“2021级”),强行拆分会增加维护成本且无实际收益。
3. Java层实现:用JDBC事务封装解决“选课超限”这个经典并发陷阱
很多同学的Java代码能完成单用户选课,但一到“两个学生同时抢最后一节课”就翻车——根本原因是没理解数据库事务的隔离级别和应用层如何配合。这里给出经过压测验证的解决方案:将选课逻辑封装为原子操作,失败时明确抛出业务异常而非SQLException。
3.1 数据库连接池配置:HikariCP不是摆设,参数要调准
在application.properties中配置连接池,重点参数说明:
# HikariCP连接池配置(Spring Boot项目) spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.datasource.hikari.username=root spring.datasource.hikari.password=123456 # 关键参数:连接池大小必须匹配业务并发量 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.minimum-idle=2 # 连接超时:避免长时间等待死锁 spring.datasource.hikari.connection-timeout=30000 # 空闲连接存活时间:防止MySQL主动断连 spring.datasource.hikari.idle-timeout=600000 # 连接最大生命周期:强制刷新连接,避免长连接内存泄漏 spring.datasource.hikari.max-lifetime=1800000参数设计依据:
maximum-pool-size=10:课程设计场景下,模拟5人并发选课已属压力测试,10连接足够;设过大反而消耗MySQL线程资源;connection-timeout=30000:30秒超时是底线,超过此值说明SQL有严重问题(如未加索引的全表扫描);max-lifetime=1800000(30分钟):MySQL默认wait_timeout=28800(8小时),但Java连接池需主动刷新,避免连接假死。
3.2 选课核心Service:用@Transactional + 手动检查实现强一致性
@Service public class CourseSelectionService { @Autowired private JdbcTemplate jdbcTemplate; /** * 学生选课业务方法 * @param studentId 学生ID * @param courseId 课程ID * @param semester 学期 * @throws BusinessException 当选课失败时抛出(含具体原因) */ @Transactional(rollbackFor = Exception.class) public void selectCourse(Long studentId, Long courseId, String semester) { // 步骤1:检查学生是否存在 Integer studentCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM student WHERE id = ?", Integer.class, studentId); if (studentCount == null || studentCount == 0) { throw new BusinessException("学生ID不存在:" + studentId); } // 步骤2:检查课程是否存在且在指定学期开设 Integer courseCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM course WHERE id = ? AND semester = ?", Integer.class, courseId, semester); if (courseCount == null || courseCount == 0) { throw new BusinessException("课程ID在指定学期未开设:" + courseId + ", " + semester); } // 步骤3:检查是否已选(利用数据库唯一约束,双重保险) Integer existedCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM student_course WHERE student_id = ? AND course_id = ? AND semester = ?", Integer.class, studentId, courseId, semester); if (existedCount != null && existedCount > 0) { throw new BusinessException("该学生已在本学期选修此课程"); } // 步骤4:检查课程容量(关键!必须在事务内原子执行) // 先查当前已选人数 Integer currentSelected = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM student_course WHERE course_id = ? AND semester = ? AND status = 'selected'", Integer.class, courseId, semester); // 再查课程总容量 Integer capacity = jdbcTemplate.queryForObject( "SELECT capacity FROM course WHERE id = ?", Integer.class, courseId); if (currentSelected == null) currentSelected = 0; if (capacity == null) { throw new BusinessException("课程容量未设置:" + courseId); } if (currentSelected >= capacity) { throw new BusinessException("课程已满员,当前容量:" + capacity + ",已选:" + currentSelected); } // 步骤5:插入选课记录(唯一约束在此刻生效) try { jdbcTemplate.update( "INSERT INTO student_course (student_id, course_id, semester, status) VALUES (?, ?, ?, 'selected')", studentId, courseId, semester); } catch (DataIntegrityViolationException e) { // 捕获唯一约束冲突(并发场景下步骤3的检查可能失效) throw new BusinessException("选课失败:系统检测到重复选课,请刷新后重试"); } } }关键设计点说明:
@Transactional保证整个方法在同一个数据库事务中执行,任何一步失败都会回滚;- 步骤4的容量检查必须放在事务内:如果先查后插,两个线程查到都是“未满”,然后都插入成功,就超员了;
catch (DataIntegrityViolationException)是兜底方案:即使步骤3做了检查,高并发下仍可能因时序问题触发唯一约束,此时捕获并转为友好提示;- 所有异常抛出
BusinessException(自定义运行时异常),而非SQLException,让Controller层能统一返回JSON错误码,避免暴露数据库细节。
3.3 DAO层优化:用NamedParameterJdbcTemplate提升可读性
为避免SQL拼接混乱,推荐使用NamedParameterJdbcTemplate:
@Repository public class StudentCourseDao { @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; /** * 查询学生本学期所有已选课程(含课程名称、教师姓名) */ public List<StudentCourseVo> findStudentCourses(Long studentId, String semester) { String sql = """ SELECT sc.id as record_id, c.course_code, c.title as course_title, t.name as teacher_name, sc.status, sc.created_at FROM student_course sc JOIN course c ON sc.course_id = c.id JOIN teacher t ON c.teacher_id = t.id WHERE sc.student_id = :studentId AND sc.semester = :semester AND sc.status = 'selected' ORDER BY sc.created_at DESC """; Map<String, Object> params = new HashMap<>(); params.put("studentId", studentId); params.put("semester", semester); return namedParameterJdbcTemplate.query(sql, params, new BeanPropertyRowMapper<>(StudentCourseVo.class)); } }优势:
- 参数名
:studentId比?更易读,尤其当SQL有10+参数时; BeanPropertyRowMapper自动映射字段名到VO属性,省去手动getXXX();- SQL用三引号(Java 15+)或分行字符串,避免长SQL挤在一行难以维护。
4. 常见问题排查:那些让答辩老师皱眉的“低级错误”,其实都有固定解法
做课程设计最怕的不是不会写,而是写了却过不了答辩。以下5个问题,是某高校连续三年课程设计中最高频的翻车点,按“现象→原因→解决”给出血泪经验:
4.1 现象:插入学生信息时报错 “Field 'class_id' doesn't have a default value”
原因:student表的class_id字段定义为NOT NULL,但插入SQL未提供该字段值,且表未设默认值。
解决:
- 插入时必须显式传入
class_id(如INSERT INTO student (student_no, name, class_id) VALUES ('2021001', '张三', 1)); - 或修改表结构添加默认值(不推荐):
ALTER TABLE student ALTER class_id SET DEFAULT 1; - 根本预防:在DAO层插入方法中,强制校验
class_id是否为null,提前抛异常。
4.2 现象:选课成功后,课程表显示“已选0人”,但student_course表里有记录
原因:student_course表的status字段默认值是'selected',但插入SQL中未指定该字段,且INSERT语句漏写了status列名,导致MySQL用默认值填充;而查询课程已选人数的SQL写成了WHERE status = 'selected',逻辑正确。但问题在于——你查的是student_course表,却用COUNT(*)统计,结果应该有数据。所以真实原因是:查询SQL写错了,比如漏了AND status = 'selected'条件,或者JOIN时条件缺失。
解决:
- 在查询已选人数的SQL中,务必显式加上
AND status = 'selected'; - 用MySQL客户端直接执行该SQL验证结果;
- 调试技巧:在Service方法中打印最终执行的SQL(开启
logging.level.org.springframework.jdbc=DEBUG)。
4.3 现象:删除教师时,报错 “Cannot delete or update a parent row”
原因:course表的teacher_id外键设置了ON DELETE RESTRICT(这是正确设计),但你试图直接删教师,而该教师名下还有课程。
解决:
- 删除教师前,先将其名下课程的
teacher_id设为NULL或分配给其他教师:UPDATE course SET teacher_id = NULL WHERE teacher_id = ?; DELETE FROM teacher WHERE id = ?; - 或在Java Service中封装删除逻辑,先解绑再删除;
- 设计提醒:
ON DELETE RESTRICT是安全策略,不是bug,强行改成CASCADE会导致课程信息丢失。
4.4 现象:同一门课在不同学期开设,但课程代码相同,导致唯一约束冲突
原因:course表只对course_code设了UNIQUE,没考虑学期维度。
解决:
- 立即执行建表语句修正:
ALTER TABLE course DROP INDEX uk_course_code, ADD UNIQUE KEY uk_code_semester (course_code, semester); - 如果已有数据,先删重复数据再加约束;
- 教训:建表前必须用Excel列出所有可能的业务组合(如课程代码+学期),再设计唯一约束。
4.5 现象:Java程序启动报错 “Access denied for user 'root'@'localhost'”
原因:MySQL 8.0+ 默认认证插件改为caching_sha2_password,而老版JDBC驱动不兼容。
解决:
- 方案1(推荐):升级JDBC驱动,在
pom.xml中指定8.0+版本:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> - 方案2:修改MySQL用户认证方式(开发环境可用):
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES; - 避坑口诀:新项目起步,JDBC驱动版本必须 ≥ MySQL服务端版本。
5. 答辩验收指南:用这4步让老师觉得“这学生真懂数据库”
答辩不是复述代码,而是证明你理解了数据如何被组织、约束如何被落实、错误如何被防御。我带过的某高校课程设计答辩中,90分以上的同学都做了以下四件事,且顺序不能乱:
5.1 第一步:用一张图说清数据流向(不是ER图,是业务流程图)
不要一上来就讲“我用了MySQL”,先画这张图:
[学生录入] → [班级表] → [学生表] ↓ [课程开设] → [教师表] → [课程表] ↓ [选课操作] → [student_course表] ↓ [查询统计] ← [视图/SQL]为什么有效:
- 导师一眼看出你是否理解“班级是学生归属的容器,课程是教学活动的载体”;
- 图中箭头方向体现数据依赖(如学生必须先有班级才能录入),比文字描述直观十倍;
- 所有节点都是你实际建的表名,不是抽象概念。
5.2 第二步:现场演示一个“故意制造的错误”,并解释修复过程
比如:
- 演示1:在MySQL客户端执行
INSERT INTO student_course (student_id, course_id, semester) VALUES (1,1,'2024-1');(不设status),观察是否因默认值生效而成功; - 演示2:执行两次相同选课,第一次成功,第二次报唯一约束错误,展示错误日志;
- 演示3:修改课程容量为1,让两个学生并发选课,观察是否有一个失败并提示“已满员”。
答辩话术:
“老师您看,这里我故意不传status字段,因为表定义了DEFAULT 'selected',所以插入成功——这说明默认值约束生效了;而第二次插入失败,报错信息是‘Duplicate entry’,对应我们建的uk_student_course_semester唯一索引,证明数据库层防护是可靠的。”
5.3 第三步:对比两份SQL,讲清“为什么这样写比那样写快”
准备两张截图:
- 慢SQL:
SELECT * FROM student_course sc JOIN course c ON sc.course_id = c.id WHERE c.title LIKE '%数据库%';(未加索引,全表扫描) - 快SQL:
SELECT * FROM student_course sc JOIN course c ON sc.course_id = c.id WHERE c.course_code = 'CS301';(用主键/唯一索引字段查询)
关键解释点:
- 指出
c.title是普通字段,没建索引,LIKE '%数据库%'无法用索引; c.course_code是唯一索引,查询速度是O(log n);- 展示
EXPLAIN结果:慢SQL的type=ALL(全表扫描),快SQL的type=const(常量查询)。
5.4 第四步:交一份《设计决策说明书》,而不是代码清单
这份文档是答辩加分项,只需1页A4纸,包含:
| 决策点 | 我的选择 | 为什么这样选(一句话) | 替代方案及放弃原因 |
|---|---|---|---|
| 主键类型 | BIGINT UNSIGNED | 避免负数ID,且比VARCHAR索引效率高 | 学号作主键:业务变更风险高 |
| 性别字段 | ENUM('M','F') | 存储省空间,且强制取值范围,防脏数据 | VARCHAR:易输入'male'/'女'等不一致值 |
| 课程容量检查 | 应用层+数据库约束 | 应用层快速响应,数据库约束兜底并发安全 | 仅数据库触发器:调试困难,不易追踪 |
| 教师删除策略 | RESTRICT | 防止课程信息丢失,符合教学管理规范 | CASCADE:一键删除会丢失课程归属 |
我的习惯:每次做完数据库设计,都强制用这个表格过一遍。从那以后,我带的学生答辩时再没人被问倒“为什么用ENUM不用INT”。希望帮到你。
本文还有配套的精品资源,点击获取