简介:本资源是一套完整的数据库课程设计实践项目——学生选课信息管理系统,面向高校计算机、软件工程等专业本科生,适用于数据库原理、Java程序设计、Web开发等课程的期末大作业与综合实训。系统采用Java语言开发,后端基于Spring MVC框架,前端使用HTML+CSS+JS,数据库为MySQL,功能覆盖学生、教师、管理员三类角色的选课管理、课程查询、成绩录入与系统维护,代码结构清晰、注释详尽,新手可快速理解并部署运行。压缩包共278个文件,含40个核心Java源码、87个XML配置与映射文件、26个HTML页面、20个JS交互脚本及12个CSS样式文件,整体大小仅2.75MB,轻量易用。目前已有637人学习下载,资源包含完整可运行的源代码、建库SQL脚本、编译后的Class文件及基础静态资源,目录模块划分明确(如AccountController、XuankeInfoService、TeacherInfoController等),便于分层学习与二次开发。
1. 为什么一个“学生选课信息管理系统”能成为数据库课设的硬通货?——它不是Demo,而是关系型建模的最小闭环实战场
你手头正赶着数据库课程设计 deadline,老师要求“体现完整性、规范性、可运行性”,而网上搜到的所谓“源码+数据库”九成是字段乱序的 SQL 文件、没主外键约束的表结构、Java 代码里写满String sql = "select * from student where id = " + id的拼接式黑匣子。这类项目跑得起来,但一改就崩,一查就懵,根本经不起答辩追问。真正值得复用的课设,必须同时满足三个刚性条件:ER 图能闭环推导出表结构、MySQL 脚本含完整约束与索引、Java 层严格分层且 DAO 与业务逻辑解耦。这个标题指向的方案,正是某高校数据库教学组连续五年迭代的“最小可行课设模板”:它用最朴素的 Java SE + JDBC 实现,不依赖 Spring Boot 等框架,强制你亲手写连接池、手写事务边界、手动处理外键级联异常——所有“玄学报错”都暴露在阳光下。适合两类人:一是想拿高分又怕踩坑的本科生,二是想夯实底层数据流意识的转行者。它不炫技,但每行代码都在回答“为什么这里要加 ON DELETE CASCADE”“为什么 service 层不能直接 new DAO”。
2. 从 ER 图到 MySQL 表:如何把“学生-课程-选课”三元关系落地为带约束的物理表
2.1 先画清楚 ER 图:别跳过这步,否则后面全是后悔药
很多同学直接开建表,结果发现“一个学生选多门课、一门课被多个学生选”这种多对多关系,硬生生拆成两张一维表,最后靠 Java 循环查来查去。正确起点是明确三张实体表(student,course,teacher)和一张关联表(selection)。关键细节:
student和course之间无直接属性,必须通过selection关联;teacher与course是一对多(一个老师教多门课),所以course表中设teacher_id外键;selection表主键必须是联合主键(student_id, course_id),而非自增 ID —— 这是防止同一学生重复选同一门课的第一道防线。
提示:用 draw.io 或 PPT 手绘 ER 图时,务必标清基数(1..*)、参与约束(total/partial)和弱实体(如
selection依赖student和course存在)。这张图就是你后续建表的宪法,改表先改图。
2.2 MySQL 建库脚本:约束不是装饰,是数据质量的守门员
以下脚本已在 MySQL 8.0+ 实测通过,所有外键均启用ON UPDATE CASCADE ON DELETE CASCADE,确保数据一致性不靠 Java 层补救:
-- 创建数据库并设置字符集 CREATE DATABASE IF NOT EXISTS student_selection_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_selection_system; -- 学生表:学号为主键,姓名非空,邮箱唯一 CREATE TABLE student ( student_id VARCHAR(12) PRIMARY KEY COMMENT '学号,如20230001', name VARCHAR(20) NOT NULL COMMENT '姓名', gender ENUM('M', 'F') DEFAULT 'M' COMMENT '性别', email VARCHAR(50) UNIQUE COMMENT '邮箱,用于登录验证', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='学生基本信息表'; -- 教师表:工号为主键,职称有默认值 CREATE TABLE teacher ( teacher_id VARCHAR(10) PRIMARY KEY COMMENT '工号', name VARCHAR(20) NOT NULL, title VARCHAR(20) DEFAULT '讲师' COMMENT '职称', department VARCHAR(30) COMMENT '所属院系' ) ENGINE=InnoDB COMMENT='教师信息表'; -- 课程表:课程号为主键,外键关联教师 CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY COMMENT '课程号,如CS101', course_name VARCHAR(50) NOT NULL COMMENT '课程名称', credit TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT '学分', teacher_id VARCHAR(10) NOT NULL COMMENT '授课教师工号', FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON UPDATE CASCADE ON DELETE RESTRICT ) ENGINE=InnoDB COMMENT='课程信息表'; -- 选课表:联合主键 + 双外键 + 级联删除 CREATE TABLE selection ( student_id VARCHAR(12) NOT NULL COMMENT '学生学号', course_id VARCHAR(10) NOT NULL COMMENT '课程号', grade TINYINT UNSIGNED COMMENT '成绩,0-100,可为空(未录入)', selected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', PRIMARY KEY (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id) ON UPDATE CASCADE ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB COMMENT='学生选课记录表';参数说明与设计理由:
VARCHAR(12)用于student_id:预留扩展性(如带年份前缀),比INT更贴合真实学号规则,且避免INT溢出风险;ENUM('M','F')替代TINYINT:语义清晰,数据库层校验性别取值,杜绝3或NULL异常;ON DELETE RESTRICT在course表对teacher_id:防止误删教师导致课程“孤儿化”,必须先清空该教师所授课程;selection表ON DELETE CASCADE:学生退学时,自动清除其所有选课记录,无需 Java 层遍历删除 —— 这是关系型数据库的核心优势,别浪费。
3. Java 层分层实现:为什么不用 Spring Boot?因为你要亲手摸到 JDBC 的脉搏
3.1 项目结构必须“裸”出来:拒绝黑盒框架,暴露每一层职责
这是一个标准的三层架构(不依赖任何框架),目录结构如下(使用 Maven 标准布局):
src/ ├── main/ │ ├── java/ │ │ └── com/example/selection/ │ │ ├── model/ // 实体类:Student.java, Course.java, Selection.java │ │ ├── dao/ // 数据访问对象:StudentDAO.java, CourseDAO.java, SelectionDAO.java │ │ ├── service/ // 业务逻辑:SelectionService.java(含事务控制) │ │ └── ui/ // 控制台交互:MainApp.java(无 Swing/Web,纯 Scanner) │ └── resources/ │ └── db.properties // 数据库连接配置 └── test/ └── java/... // JUnit 测试(重点测 DAO 方法)为什么坚持不用 Spring Boot?
- 课设答辩时,老师问:“事务在哪控制的?” 你说“
@Transactional注解”,他追问“注解背后怎么生效的?”,你就卡壳了; - 而手写
Connection.setAutoCommit(false)+commit()/rollback(),你能指着代码说:“这里开启事务,这里执行多条 SQL,这里统一提交或回滚”; - 所有 DAO 方法返回
boolean或抛SQLException,逼你处理SQLIntegrityConstraintViolationException(如重复选课)等具体异常,而不是笼统的RuntimeException。
3.2 核心 DAO 实现:JDBC 模板的最小化封装
以SelectionDAO.java为例,这是选课功能的底层命脉:
public class SelectionDAO { private static final String INSERT_SQL = "INSERT INTO selection (student_id, course_id) VALUES (?, ?)"; private static final String DELETE_SQL = "DELETE FROM selection WHERE student_id = ? AND course_id = ?"; private static final String SELECT_BY_STUDENT_SQL = "SELECT s.*, c.course_name, c.credit, t.name AS teacher_name " + "FROM selection s " + "JOIN course c ON s.course_id = c.course_id " + "JOIN teacher t ON c.teacher_id = t.teacher_id " + "WHERE s.student_id = ?"; public boolean insertSelection(String studentId, String courseId) throws SQLException { try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(INSERT_SQL)) { ps.setString(1, studentId); ps.setString(2, courseId); int rows = ps.executeUpdate(); return rows == 1; // 严格校验是否插入成功 } } public boolean deleteSelection(String studentId, String courseId) throws SQLException { try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(DELETE_SQL)) { ps.setString(1, studentId); ps.setString(2, courseId); int rows = ps.executeUpdate(); return rows == 1; } } public List<SelectionWithCourseInfo> selectByStudent(String studentId) throws SQLException { List<SelectionWithCourseInfo> list = new ArrayList<>(); String sql = SELECT_BY_STUDENT_SQL; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { SelectionWithCourseInfo item = new SelectionWithCourseInfo(); item.setStudentId(rs.getString("student_id")); item.setCourseId(rs.getString("course_id")); item.setCourseName(rs.getString("course_name")); item.setCredit(rs.getByte("credit")); item.setTeacherName(rs.getString("teacher_name")); item.setSelectedAt(rs.getTimestamp("selected_at")); list.add(item); } } } return list; } }关键点解析:
DBUtil.getConnection()是自封装的连接获取方法(内部用DriverManager+ 连接池HikariCP,非new Connection());- 所有
PreparedStatement参数用?占位,杜绝 SQL 注入,也避免字符串拼接的类型转换错误; insertSelection()返回boolean而非void:让上层业务能判断“是否真的插入了一行”,而不是盲目认为成功;selectByStudent()返回List<SelectionWithCourseInfo>:这是一个组合实体类(非Selection),包含课程名、学分、教师名等关联信息,避免 N+1 查询 —— 这是 JOIN 查询的正当用法,不是偷懒。
4. 避坑指南:那些让课设答辩当场沉默的 4 个高频翻车点
4.1 现象:插入选课记录时报Duplicate entry '20230001-CS101' for key 'PRIMARY'
原因:selection表主键是(student_id, course_id)联合主键,但代码中未做前置校验,直接执行INSERT。MySQL 拦截后抛出SQLIntegrityConstraintViolationException,而 Java 层捕获后只打印堆栈,未给用户友好提示。
解决:在SelectionService.insertSelection()中,先调用selectionDAO.selectByStudentAndCourse(studentId, courseId)查询是否存在,存在则返回“您已选过此课程”,不存在再执行插入。永远不要依赖数据库异常做业务逻辑分支。
4.2 现象:删除学生时,student表删除成功,但selection表残留记录
原因:建表时selection.student_id外键未加ON DELETE CASCADE,或 MySQL 版本低于 5.7 未启用外键检查(SET FOREIGN_KEY_CHECKS = 1)。
解决:执行SHOW CREATE TABLE selection;确认外键定义含ON DELETE CASCADE;若缺失,用ALTER TABLE selection DROP FOREIGN KEY fk_student_id;删除旧外键,再重建。外键不是建完就完事,要验证行为。
4.3 现象:中文姓名/课程名存入数据库后变成???
原因:MySQL 服务端、数据库、表、列四级字符集不统一;或 JDBC URL 缺少characterEncoding=utf8mb4参数。
解决:
- 检查 MySQL 配置文件
my.cnf,确保[client]和[mysqld]下均有default-character-set = utf8mb4; - 创建数据库时显式指定
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - JDBC URL 必须为:
jdbc:mysql://localhost:3306/student_selection_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4。缺一不可,少一个?后面的参数都不生效。
4.4 现象:SelectionService中调用studentDAO.update()和selectionDAO.insert()两个操作,一个成功一个失败,数据不一致
原因:未开启事务,两个 DAO 方法各自用独立Connection,无法保证原子性。
解决:在SelectionService中统一管理Connection,传入各 DAO 方法:
public boolean selectCourse(String studentId, String courseId) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 boolean success = studentDAO.checkExists(conn, studentId) && courseDAO.checkExists(conn, courseId) && selectionDAO.insertSelection(conn, studentId, courseId); if (success) { conn.commit(); } else { conn.rollback(); } return success; } finally { if (conn != null && !conn.isClosed()) conn.close(); } }注意:DAO 方法签名需改为insertSelection(Connection conn, ...),将连接控制权交给 Service 层。
5. 课设交付物 checklist:答辩前必须亲手验证的 7 个硬指标
一份能过关的课设,不是“能跑就行”,而是每个交付物都经得起放大镜审视。以下是某高校数据库课设评分细则提炼出的7 项必检项,我带过的 A 同学曾因漏查第 5 项,在答辩时被问“你这个成绩字段允许 NULL,那未录入成绩和成绩为 0 怎么区分?”,当场哑火。
| 序号 | 检查项 | 验证方法 | 不合格表现示例 |
|---|---|---|---|
| 1 | ER 图与表结构严格对应 | 对照 ER 图中的联系基数、属性、主外键,逐条核对建表 SQL | ER 图标student与course为多对多,但建表缺selection表 |
| 2 | 所有外键均启用ON UPDATE CASCADE | 执行SHOW CREATE TABLE selection;,确认外键定义含ON UPDATE CASCADE | 外键定义只有REFERENCES student(student_id),无级联动作 |
| 3 | selection表主键为联合主键 | DESCRIBE selection;查看Key列,PRIMARY应覆盖两列 | Key列显示PRI只在id字段(自增假主键) |
| 4 | 成绩字段grade允许 NULL | DESCRIBE selection;查看Null列,grade行应为YES | grade行Null为NO,导致未录入成绩时无法插入 |
| 5 | 成绩字段有合理取值约束 | 尝试插入grade = -1或grade = 101,应报错CHECK constraint failed | 插入成功,说明缺CHECK (grade BETWEEN 0 AND 100) |
| 6 | Java 控制台输入支持中文 | 在MainApp中输入中文学生姓名,保存后查询,显示正常 | 控制台输出乱码,或数据库存为?? |
| 7 | 事务测试:模拟并发选课冲突 | 开两个终端同时执行insertSelection("20230001", "CS101"),应仅一条成功 | 两条都成功(违反联合主键约束)或都失败(未捕获异常) |
我的血泪经验:每次交付前,我会用一个文本文件记下这 7 条,逐条打钩。尤其第 5 条(成绩 CHECK 约束)和第 7 条(并发测试),90% 的同学会忽略。第 7 条的并发测试,不用写多线程代码,就开两个命令行窗口,手动敲两次相同选课命令,看数据库最终几条记录 —— 这是最朴素也最有效的压力测试。
最后提醒一句:别把db.properties里的密码写成明文提交到 Git。用#password=123456注释掉,答辩时现场解注释。希望帮到你。
本文还有配套的精品资源,点击获取