ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

学生选课信息管理系统Java实现:从数据库设计到事务与并发控制

学生选课信息管理系统Java实现:从数据库设计到事务与并发控制 简介面向数据库课程设计的学生与开发者这份学生选课信息管理系统源代码及设计报告基于Java与MySQL实现采用C/S架构完整覆盖学生、教师、管理员三类核心角色。学生端支持修改个人信息、查询课程、选课退课、成绩查询与打印、奖惩信息浏览教师端可维护个人资料、查看所授课程与学生选课情况、录入课程成绩管理员端提供学生管理、教师管理、课程管理、选课管理、成绩管理、院系设置及奖惩信息管理等模块业务链路完整适合作为课程设计、毕业设计或项目实训的参考蓝本。压缩包共112个文件约1.99MB主要包含21个Java源码、66个class编译文件、1个SQL建表脚本、2个Word设计报告以及jar依赖与工程配置可直接导入IDE运行。SQL脚本建表规范合理Word报告涵盖需求分析、概念结构设计、逻辑结构设计等内容能帮助快速理解选课系统的数据表关系与设计思路。目前已有16470人学习下载对正在完成数据库课程设计、需要现成可行方案的同学具有较高的参考价值。1. 学生选课信息管理系统为什么它成了数据库课程设计的“默认题目”临近期末课程设计选题名单上十个有八个是学生选课信息管理系统。原因很简单选课的业务边界足够清晰三张表就能说清学生、课程和选课关系它又能把外键约束、事务、并发冲突这些考点全带出来。对Java学习者来说从JDBC连接到DAO分层难度恰好卡在“能学会”和“有挑战”之间正适合做“Java课程设计案例源码”级别的练手项目。这篇文章从建库SQL写到Java的DAO层再到事务与冲突检测最后把常见问题按“现象—原因—解决”列出来。适合正需要一套能跑通、也能在答辩时讲明白方案的同学。2. 需求梳理与数据库建模从课表到三张表的核心设计2.1 选课场景的三个硬约束时间冲突、学分上限、名额控制很多同学拿到题目第一反应是建表但这正是后面翻车的起点。选课系统的核心不在表在约束。一个真正能交差的系统至少要处理三类硬约束同一个学生不能在同一时间选两门课、一个学期选的课不能超过学分上限、每门课有容量上限不能被无限选爆。这三条约束直接影响表结构的字段设计和索引设计不是写代码时才临时加的。实体关系也很简单一个学生可选多门课一门课可被多个学生选是典型的多对多关系。按数据库设计的常规做法需要拆成三张表学生表、课程表、选课关系表。选课关系表用来存“谁选了哪门课、选课时间、最终成绩”它同时携带两个外键是系统的核心表。这里有个设计取舍值得提前说清课程表里的“上课时间”不要直接用“周一3-4节”这种字符串。虽然建表时省事但查冲突时你不得不在Java里做字符串解析既慢又容易出错。更常见也更好用的做法是把上课时间拆成“星期几”和“第几节”两个数值字段比如week_day1表示周一、start_slot3表示第3节查询直接用数值比较索引也能用上。2.2 建库建表MySQL 5.7/8.0 都能跑的脚本先建库再建三张表。下面是完整的建库建表SQL在MySQL 5.7和8.0里都能直接执行。如果你在本地还没装好环境先照着mysql安装配置教程把服务跑起来再用客户端执行即可。-- 创建选课系统数据库 CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE course_select; -- 学生表 CREATE TABLE student ( sno VARCHAR(20) NOT NULL COMMENT 学号, sname VARCHAR(50) NOT NULL COMMENT 姓名, ssex CHAR(1) DEFAULT 男 COMMENT 性别, sage INT DEFAULT NULL COMMENT 年龄, sdept VARCHAR(50) DEFAULT NULL COMMENT 院系, PRIMARY KEY (sno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; -- 课程表 CREATE TABLE course ( cno VARCHAR(20) NOT NULL COMMENT 课程编号, cname VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT NULL COMMENT 学分, teacher VARCHAR(50) DEFAULT NULL COMMENT 授课教师, week_day INT DEFAULT NULL COMMENT 星期几1-7, start_slot INT DEFAULT NULL COMMENT 开始节次1-12, capacity INT DEFAULT 0 COMMENT 课程容量, selected_count INT DEFAULT 0 COMMENT 已选人数, PRIMARY KEY (cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课关系表 CREATE TABLE student_course ( sno VARCHAR(20) NOT NULL COMMENT 学号, cno VARCHAR(20) NOT NULL COMMENT 课程编号, select_time DATETIME DEFAULT NULL COMMENT 选课时间, score DECIMAL(5,1) DEFAULT NULL COMMENT 成绩, PRIMARY KEY (sno, cno), CONSTRAINT fk_sc_student FOREIGN KEY (sno) REFERENCES student (sno), CONSTRAINT fk_sc_course FOREIGN KEY (cno) REFERENCES course (cno) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课记录表;这段脚本里有几个参数值得单独说。student_course表的主键用了(sno, cno)联合主键这在数据库层面直接保证“同一学生不能重复选同一门课”是最硬的一道防线。课程表里的capacity和selected_count分别存容量与已选人数每次选课成功时更新selected_count判断“课程是否已满”就变成了一个数值比较不需要额外统计查询。外键的命名也建议写清楚比如fk_sc_student一看就知道是选课表到学生表的外键。出问题时看错误信息能少走弯路。如果你用的是MySQL Workbench这个脚本也可以通过“正向工程”直接生成ER图和表结构写进报告里时更省事。2.3 初始化数据课程样例与三个必要表设置有了表结构没有数据系统跑不起来也没法演示。课程表需要至少填上十几门课覆盖不同星期和节次这样演示时间冲突时才有素材。学生表也放几个示例学生。-- 初始化学生 INSERT INTO student (sno, sname, ssex, sage, sdept) VALUES (2023001, 张明, 男, 20, 计算机), (2023002, 李静, 女, 21, 软件工程), (2023003, 王磊, 男, 20, 大数据); -- 初始化课程注意week_day和start_slot的搭配 INSERT INTO course (cno, cname, credit, teacher, week_day, start_slot, capacity, selected_count) VALUES (C001, 数据库原理, 3.0, 刘老师, 1, 3, 50, 0), (C002, Java程序设计, 4.0, 陈老师, 1, 3, 40, 0), (C003, 操作系统, 3.0, 赵老师, 2, 1, 60, 0), (C004, 计算机网络, 3.5, 周老师, 3, 5, 45, 0);初始化数据时最容易忽略的是ENGINEInnoDB。如果建表时用了MyISAM事务和外键约束都不生效到了写选课事务那一步你会非常被动。DEFAULT CHARSETutf8mb4也是必选项只写utf8在某些MySQL版本中实际是utf8mb3中文没问题但遇到特殊符号会报“Incorrect string value”的错属于典型的“看着没问题实际有隐患”。外键的删除行为这里用默认的RESTRICT也就是有选课记录的学生不允许直接删除。这个行为对课程设计反而是好事因为它在数据库层面保护了业务数据不被误删。如果你希望删除课程时级联删除选课记录可以把外键改成ON DELETE CASCADE但演示时反而容易在删除时突然没了一堆数据多数场景下不建议这么做。3. Java侧实现JDBC连接与DAO层的可复用写法3.1 JDBC连接参数URL里必须带的四个配置数据库建好之后下一步是用Java连上它。很多同学从网上下载了“Java课程设计案例源码”改完数据库密码还是抛异常问题多半出在JDBC连接串上。连接MySQL的URL写法远比想象中讲究差一个参数就是一道坎。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_select ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingUTF-8 allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 你的密码; public static Connection getConnection() throws SQLException { // MySQL 8.x 驱动类名5.x 用 com.mysql.jdbc.Driver // 新版本驱动会自动注册这一行可写可不写 // Class.forName(com.mysql.cj.jdbc.Driver); return DriverManager.getConnection(URL, USER, PASSWORD); } }代码里的四个参数是每次连接必须确认的。useSSLfalse在本地开发时关掉SSL握手避免多余告警serverTimezoneAsia/Shanghai解决MySQL 8.0时区导致的“Server returns invalid timezone”报错characterEncodingUTF-8和数据库的utf8mb4对上了中文数据读写才不会乱码allowPublicKeyRetrievaltrue专门解决mysql 8.0的caching_sha2_password认证时报“Public Key Retrieval is not allowed”的问题。JDBC驱动版本和MySQL版本也要匹配。MySQL 8.x配mysql-connector-java-8.0.xMySQL 5.x配5.1.47及以后版本。驱动的jar包用手动导入的方式放到项目lib目录下比用Maven本地仓库更好理解出问题时排查路径少一截。3.2 用DAO把你的SQL和业务逻辑隔开连接有了下一步别急着往main方法里堆SQL。课程设计评审老师大概率会问“代码分层是什么”如果看到所有JDBC代码都写在界面类里解释成本会很高。按最常见的做法至少分出两层DAO层负责SQL和结果集映射Service层负责业务判断。import java.sql.*; import java.util.ArrayList; import java.util.List; public class CourseDao { // 查询所有可选课程 public ListCourse findAvailableCourses(Connection conn, String sno) throws SQLException { String sql SELECT cno, cname, credit, teacher, week_day, start_slot, capacity, selected_count FROM course WHERE selected_count capacity AND cno NOT IN (SELECT cno FROM student_course WHERE sno ?); ListCourse list new ArrayList(); // PreparedStatement 预编译参数用 ? 占位避免拼接SQL try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Course c new Course(); c.setCno(rs.getString(cno)); c.setCname(rs.getString(cname)); c.setCredit(rs.getDouble(credit)); c.setTeacher(rs.getString(teacher)); c.setWeekDay(rs.getInt(week_day)); c.setStartSlot(rs.getInt(start_slot)); c.setCapacity(rs.getInt(capacity)); c.setSelectedCount(rs.getInt(selected_count)); list.add(c); } } } return list; } }这里值得说明的是try-with-resources写法。PreparedStatement和ResultSet都在 try 的小括号里声明方法结束时自动关闭不用手写finally。很多初版代码只关闭了Connection而漏掉Statement短时间没问题跑久了连接池就爆满属于典型的黑匣子故障。这条查询SQL同时做了两件事selected_count capacity过滤掉已满课程NOT IN过滤掉已经选过的课程。把过滤逻辑放在SQL里而不是Java里是为了让数据库在返回结果前就削减数据量对后面的分页、报表都有好处。Course是个最简单的POJO类只包含对应字段的getter/setter这里的setCredit(rs.getDouble())要注意课程表里credit是DECIMAL(3,1)用getDouble或getBigDecimal都行但别用getInt否则小数学分直接被截断数据库原理课的3.5学分就变成3了。3.3 Service层怎么用把SQL和业务规则分开DAO只负责“怎么查”Service负责“能不能选”。选课的入口一般放在CourseService里它拿到学生要选的课程号后先判空、再查重、查冲突、最后调DAO写入。public class CourseService { private final CourseDao courseDao new CourseDao(); private final StudentCourseDao scDao new StudentCourseDao(); public boolean selectCourse(Connection conn, String sno, String cno) { // 1. 课程必须存在且未满 boolean available courseDao.checkAvailable(conn, cno); if (!available) { throw new RuntimeException(课程不存在或已经选满); } // 2. 不能重复选 boolean exists scDao.exists(conn, sno, cno); if (exists) { throw new RuntimeException(你已经选过这门课); } // 3. 时间冲突检查 boolean conflict scDao.checkTimeConflict(conn, sno, cno); if (conflict) { throw new RuntimeException(该时间与已选课程冲突); } // 4. 写入选课记录 return scDao.insert(conn, sno, cno) 0; } }这样分层之后每个方法的职责单一出错了看异常信息就能定位到具体步骤。更重要的是Service层可以方便地加事务——如果选课是“先扣名额再插记录”两步操作任何一步失败都不能让另一边落库这个逻辑放在DAO层是管不住的必须放到Service层统一控制。第4章会专门展开这一点。4. 核心业务逻辑选课事务与冲突检测怎么一次写对4.1 选课业务不是一条INSERT事务边界怎么定新手写选课往往就是一条INSERT INTO student_course然后以为完事了。但结合程序设计逻辑选课时课程表的selected_count要加1选课表要插入一条记录有的系统还要记录选课时间。两步操作之间如果程序崩溃就可能出现“选课记录插上了、名额没扣”或者反过来数据对不上。把这两步包在一个事务里是数据库课程设计的标准考点。public boolean selectWithTransaction(String sno, String cno) { Connection conn null; try { conn DBUtil.getConnection(); // 关闭自动提交事务开始 conn.setAutoCommit(false); // 第一步名额未满则更新已选人数 String sql1 UPDATE course SET selected_count selected_count 1 WHERE cno ? AND selected_count capacity; PreparedStatement ps1 conn.prepareStatement(sql1); ps1.setString(1, cno); int rows ps1.executeUpdate(); if (rows 0) { throw new SQLException(课程已满选课失败); } // 第二步插入选课记录 String sql2 INSERT INTO student_course (sno, cno, select_time) VALUES (?, ?, NOW()); PreparedStatement ps2 conn.prepareStatement(sql2); ps2.setString(1, sno); ps2.setString(2, cno); ps2.executeUpdate(); // 两步都成功统一提交 conn.commit(); return true; } catch (SQLException e) { // 任何一步失败整体回滚 if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException(选课失败 e.getMessage()); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码里有三个关键点。第一UPDATE语句把“校验名额”和“扣减名额”合并成了一条SQL通过WHERE selected_count capacity让数据库在行锁层面判断满没满而不是先SELECT查到结果再UPDATE后者在高并发下会出现“都查到还剩1个名额、结果都扣成负数”的竞态问题。第二rows 0表示更新影响行数为零本质是条件不满足——课程不存在或已满这时候必须抛异常阻止后续插入。第三事务的提交和回滚只在catch/finally里各出现一次别在两步之间随手commit否则事务就成了摆设。4.2 冲突检测的边界同时间、超学分、重复选时间冲突是最容易漏判的逻辑。用第2章设计的week_day和start_slot字段冲突检测的SQL可以写成一条子查询找出该学生已选课程中与目标课程同一星期且上课时间段有交集的记录。-- 检测是否选了同一时间段的课 SELECT COUNT(*) FROM course c WHERE c.cno IN ( SELECT cno FROM student_course WHERE sno ? ) AND c.week_day ? AND c.start_slot ?public boolean checkTimeConflict(Connection conn, String sno, String cno) throws SQLException { String sql SELECT COUNT(*) FROM course c WHERE c.cno IN (SELECT sc.cno FROM student_course sc WHERE sc.sno ?) AND c.week_day (SELECT week_day FROM course WHERE cno ?) AND c.start_slot (SELECT start_slot FROM course WHERE cno ?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, cno); ps.setString(3, cno); try (ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1) 0; } } }这版冲突判断只做了“同一星期同一开始节次”的精确匹配够课程设计用但严格来说不够完整。比如一门课在3-4节、另一门在4-5节start_slot不同但实际第4节课冲突了。要处理这种交叉重叠常见做法是给课程表再加一个end_slot字段SQL改成AND c.start_slot ? AND c.end_slot ?这种区间重叠判断。报告里能主动写出这个局限和升级方案往往是加分项。至于学分上限可以在学生表加一个max_credit字段选课前统计该学生已选课程学分总和和目标课程的学分相加后做比较。这个判断逻辑放在SQL里会写得很绕放在Service层更清晰。代码就不单独贴了核心就是SELECT SUM(credit) FROM course WHERE cno IN (SELECT cno FROM student_course WHERE sno ?)然后和阈值比大小。4.3 名额扣减的两种策略悲观锁与乐观锁选课系统在课程设计里是单机演示但在“计算机系统结构”层面这道题考的是并发。名额还剩最后一个两个学生同时点选课数据库层面必须保证只有一个成功。两种常见策略各有取舍。悲观锁的写法是在事务里对课程行加锁其他事务只能等待SELECT capacity, selected_count FROM course WHERE cno ? FOR UPDATE;// Java侧配合事务使用 // 在获取Connection并setAutoCommit(false)之后 String selectSql SELECT capacity, selected_count FROM course WHERE cno ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(selectSql)) { ps.setString(1, cno); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { int selected rs.getInt(selected_count); int capacity rs.getInt(capacity); if (selected capacity) { throw new RuntimeException(课程已满); } } } }FOR UPDATE会对命中的行加排他锁直到事务提交或回滚才释放。好处是判断和更新期间没有人插得进来坏处是并发量大时锁等待会变长。单机演示场景完全够用而且写进报告里显得你懂锁机制。乐观锁则不在查询时加锁而是在更新时带上版本条件。最常见的是在课程表加一个version字段更新时同时判断版本号。对比而言悲观锁实现简单、理解成本低更适合课程设计乐观锁的优点在于读多写少时不阻塞读但代码量更大。我一般建议优先用悲观锁如果你想让报告更有看点再补一句“生产环境高并发下可切换为乐观锁”说明你已经思考过这个问题。5. 常见问题与避坑排查从连不上库到中文乱码的5条记录5.1 连不上数据库三个最常见的连接报错选课系统跑不起来八成的锅在JDBC连接。第一个是“ClassNotFoundException: com.mysql.cj.jdbc.Driver”现象是执行到Class.forName就抛异常原因通常是驱动jar包没有正确放进lib目录或没被项目引用。解决方式是重新添加依赖并确认构建路径里能看到mysql-connector的jar。第二个是“Communications link failure”现象是连本机数据库也报链路错误原因多为MySQL没启动或连接URL里端口填成了3307之类在“mysql安装教程”里启动服务后再试。第三个是“Access denied for user rootlocalhost”密码不对或者你用了MySQL 8.0的caching_sha2_password认证方式而驱动版本太老换新驱动即可。5.2 中文乱码与字符集不一致一处没配上处处是问号现象是Java控制台输出的课程名称全是???或者插入中文后查出来是乱码。原因通常是三层字符集不统一数据库表是utf8mb4、JDBC连接串没带characterEncodingUTF-8、操作系统控制台默认是GBK。解决办法是同时做两件事在连接URL里加上characterEncodingUTF-8参数建库建表时统一使用utf8mb4。如果控制台在Windows上还是乱码那基本不是程序问题而是IDE的Console编码不是UTF-8。顺手在报告里写一句“本系统统一使用UTF-8编码”避坑理由就非常充分了。5.3 外键删除导致的连锁反应有选课记录时删不掉课程现象是删除一门课程时报“Cannot delete or update a parent row: a foreign key constraint fails”。原因就是student_course表有外键引用course表默认的RESTRICT行为禁止有子记录时删父记录。解决思路不是去改外键为CASCADE而是应该理解为业务本身不允许删已有人选的课。但课程设计里老师很可能想演示删除功能我的习惯是在Service层先执行DELETE FROM student_course WHERE cno ?再执行DELETE FROM course WHERE cno ?两步也放进事务里。删选课记录、删课程本身都是业务动作这种“先子后父”的顺序才是正确解法。5.4 并发选课漏判断单机Demo也会被主键兜底现象是名额还剩一个时多线程模拟两个学生同时选课日志里出现两次“选课成功”或数据库里出现两条相同的选课记录。原因是在Service层先判断“是否已选”再执行INSERT两个事务读到同样的空结果于是都走完了插入流程。解决有三个层次最低限度student_course的联合主键已经能拦住重复选课让第二个插入直接抛主键冲突异常更进一步在DAO里把这个异常捕获并转成友好提示“不能重复选课”最严格结合第4章的事务和锁让名额判断和扣减成为原子操作。课程设计阶段能写到第二层就已经很稳了第三层是加分项。5.5 本地MySQL改了密码后项目全部失效现象是换了一台机器或重装MySQL后项目连不上数据库但密码明明改过了。原因很可能是你改了my.ini或安装时指定了skip-grant-tables来重置密码服务重启后认证方式变回旧密码里不存在的账号权限。解决方式是重新用root登录执行ALTER USER rootlocalhost IDENTIFIED BY 新密码;再确认FLUSH PRIVILEGES。另外提醒一句密码里的特殊字符如果出现在URL里比如或#必须URL编码否则连接串会被截断这是最容易被忽略的玄学问题。6. 交差前最后一步三种快速验证方案写完了代码、跑通了界面离答辩还有一步。别等到答辩现场才靠鼠标点来点去演示更不要只截一张运行截图放报告里。结合我个人的血泪经验交差前建议用下面三种快速验证把系统核心逻辑“自己给自己体检”一遍。第一种是写一个纯Java的测试类不依赖界面直接调Service层跑三条用例正常选课成功、重复选课被拦截、选满的课被拦截。每次跑完看输出是否符合预期这就是最简单回归测试。界面再怎么改业务层不会出大岔子。public class CoreTest { public static void main(String[] args) { Connection conn null; try { conn DBUtil.getConnection(); CourseService service new CourseService(); boolean result service.selectWithTransaction(2023001, C001); System.out.println(第一次选课结果: result); } catch (RuntimeException e) { System.out.println(预期内失败: e.getMessage()); } finally { // 关闭连接 } } }第二种是数据一致性SQL检查事务是否真的可靠。执行SELECT * FROM student_course WHERE sno 2023001看有没有预期记录再执行SELECT cno, capacity, selected_count FROM course看名额是否同步增减。事务做没做对这两条SQL一跑就知道。第三种是报告素材整理把数据库ER图、核心表的结构说明、一张含时间冲突的演示截图放进去。报告中能写清楚“本系统使用InnoDB引擎保证事务外键约束保证参照完整性采用悲观锁应对并发选课”比贴十几页代码有价值得多。这套验证做完项目才算真正闭环。以后再做带数据库的课程设计我建议你先从约束和数据一致性出发先把表结构想清楚再写代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表