ARTICLE DETAIL

资讯详情

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

Java Swing+MySQL学生选课系统:从建表到事务并发全解析

Java Swing+MySQL学生选课系统:从建表到事务并发全解析 简介这是一份基于Java和Swing以及MySQL数据库实现的学生选课与成绩管理系统课程设计资源包重点面向需要完成毕业设计、课程设计、大作业或工程实训的初学者与进阶学习者。系统覆盖学生选课、课程管理、成绩管理、学生信息查询等核心功能并预置管理员、教师、学生三类登录账号方便快速体验与二次扩展。资源包共包含53个文件核心内容有16个Java源代码文件、1个SQL数据库脚本、6个XML配置以及MySQL驱动JAR包同时附带系统结构图、数据库E-R图和多张功能界面截图便于了解整体架构与操作流程其中数据库账号密码可在JdbcConfig中调整SQL脚本位于resources目录下部署步骤清晰。整个压缩包体积仅8.63MB轻量易用。目前已有219人学习下载尤其适合希望梳理Swing界面布局和JDBC数据库连接的开发者能够从完整示例中快速获得可复用的项目方案是初期立项与综合实训的不错参考。1. 课程设计选 Java Swing MySQL这个题目为什么值得认真做每年期末都会有人拿到类似“基于 Java Swing 的学生选课及成绩管理系统”的题目上网找 Java 课程设计案例源码下载下来一跑全是报错折腾两天最后连登录窗口都出不来。其实这个题目远没有看上去那么“老土”它的价值在于把 Java 面向对象编程、JDBC 操作、SQL 设计与 Swing 事件处理全部串在一条线上做完它你对 Java 桌面应用和 MySQL 的认知是连成片的而不是背八股文背出来的碎片。这个系统要解决的事情很具体学生登录后能选课、退课、查成绩教师登录后能开课、录成绩管理员能维护基础数据。它不涉及高并发、不涉及分布式但恰恰因为业务边界清晰才适合用来把 CRUD、事务、外键约束、表关联这些基本功练扎实。适合正在做 Java 课程设计的学生也想快速捡起 Swing JDBC 这套老组合的从业者。下面直接按我自己的实现路线讲从建表到打包每一步都是可复现的。2. 搭骨架四张表与 JDBC 连接层先把地基做厚课程设计翻车的人绝大多数不是挂在界面代码上而是挂在数据库设计和连接层上。表字段设计得随意后面写 SQL 时到处别扭连接层写死用户名密码换个机器就要改源码。这两个部分不值得追求花哨但必须一开始就做对。2.1 数据库设计student、teacher、course、sc 的字段与约束我一般会建四张表而不是常见的三张。很多人把教师信息塞进 course 表里一个 teacher_name 字段这样确实省事但课程设计答辩时老师问一句“如果要按教师查开课记录怎么写”你就得现场改表。拆成独立表是最稳的CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4; USE course_db; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, gender CHAR(2) DEFAULT 男, class_name VARCHAR(50), password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) NOT NULL, teacher_id VARCHAR(20) NOT NULL, capacity INT NOT NULL DEFAULT 60, selected_count INT NOT NULL DEFAULT 0, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sc ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DECIMAL(5,2) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_sc (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个关键选择说明一下。主键都用业务编号student_id、course_id不用自增是因为学号、课号本身唯一且业务上需要直接展示用自增反而要额外查一次。sc 表的 id 用自增是因为它只是关联记录的内部编号对外没有业务含义但 WHERE 删除时好定位。UNIQUE KEY uk_sc (student_id, course_id)是数据库层的最后一道防线防止同一学生重复选同一门课这个约束在并发场景下比 Java 代码里的判断可靠得多。selected_count字段是冗余设计目的是选课时只更新这一个数字不用每次 COUNT(*) 扫表。它的默认值 0 就是 MySQL 设置默认值为 0 的典型用法但要注意它必须与真实的选课记录保持一致否则会出现“列表显示没选满实际选不了”的怪问题。入场数据建议提前插 1 个管理员账号、2 个教师、若干学生和课程免得界面写好了没数据可看。如果你还没装 MySQL先去装一个 8.x装完顺手用 MySQL Workbench 或 Navicat 把上面这段 SQL 跑一遍这两个工具执行多行脚本都比命令行直观。2.2 JDBC 连接层一个工具类管住驱动加载、取连接、关资源课程设计阶段不需要上 MyBatis也不需要连接池但连接层的代码结构必须干净。我一般写一个 DBUtil 工具类把驱动加载、URL 拼装、连接获取、资源关闭全收进去业务代码里永远不直接出现Class.forName和DriverManager.getConnection。package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static String url; private static String username; private static String password; static { try { // MySQL 8.x 必须用 cj 驱动5.x 用的 com.mysql.jdbc.Driver 已经失效 Class.forName(com.mysql.cj.jdbc.Driver); // 注意三个参数关闭 SSL、指定字符集、指定时区 url jdbc:mysql://localhost:3306/course_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; username root; // 密码建议通过配置文件传入而不是写死在这里 password 123456; } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码里最值得讲的是 URL 上的三个参数。useSSLfalse是为了避免 MySQL 8 默认开启 SSL 后在本地连接时抛出证书相关警告characterEncodingutf8保证中文字符串从 Java 传到 MySQL 时按 UTF-8 编码少了它后面界面显示全是问号serverTimezoneAsia/Shanghai是因为 MySQL 8 驱动要求必须显式指定时区否则直接报 CST 相关的异常——这一条在 Linux 安装 MySQL 后尤其常见因为服务器默认时区可能是 UTC。关闭资源的 close 方法为什么要连 ResultSet 一起接收因为 JDBC 里 Connection 关了Statement 和 ResultSet 不一定立刻释放连接池场景下更明显。虽然课程设计多半是短连接但养成这个习惯以后接连接池时不用返工。每次用完连接就关不要想着“反正程序退出就没了”否则跑上一上午就会出现Too many connections这是 MySQL 侧最直接的惩罚。2.3 Swing 界面骨架登录窗口到主窗体的切换顺序Swing 界面的搭建顺序有固定套路先写一个登录 JFrame验证通过后 dispose 掉再 new 主窗体。有人喜欢把登录框做成模态对话框嵌在主窗体里不是不行但逻辑上绕。我更推荐独立窗口 切换的方式代码直观答辩时也讲得清楚。public class LoginFrame extends JFrame { private JTextField accountField; private JPasswordField passwordField; private JButton loginButton; public LoginFrame() { setTitle(学生选课及成绩管理系统 - 登录); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 跨平台外观规避部分系统默认外观字体发虚的问题 try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch (Exception ignored) {} JPanel panel new JPanel(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); gbc.insets new Insets(5, 5, 5, 5); gbc.gridx 0; gbc.gridy 0; panel.add(new JLabel(账号), gbc); gbc.gridx 1; gbc.gridy 0; gbc.fill GridBagConstraints.HORIZONTAL; accountField new JTextField(16); panel.add(accountField, gbc); gbc.gridx 0; gbc.gridy 1; panel.add(new JLabel(密码), gbc); gbc.gridx 1; gbc.gridy 1; passwordField new JPasswordField(16); panel.add(passwordField, gbc); gbc.gridx 0; gbc.gridy 2; gbc.gridwidth 2; loginButton new JButton(登录); panel.add(loginButton, gbc); setContentPane(panel); // 事件绑定 loginButton.addActionListener(e - doLogin()); // 回车也能触发登录 getRootPane().setDefaultButton(loginButton); } private void doLogin() { String account accountField.getText().trim(); String pwd new String(passwordField.getPassword()); User user new UserDao().login(account, pwd); if (user ! null) { JOptionPane.showMessageDialog(this, 欢迎 user.getName()); new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 账号或密码错误); } } }这段骨架代码里有三个细节决定体验。setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)是写给学生阶段用的程序只有这一个入口窗口关掉就退出不会留后台进程。setLocationRelativeTo(null)让窗口居中不要小看这一步很多课程设计跑起来窗口落在屏幕左上角第一印象就扣分。getRootPane().setDefaultButton(loginButton)把回车绑定到登录按钮演示时不用鼠标点体验差很多。窗口切换的关键在最后一个动作登录成功之后先 setVisible(true) 再 dispose顺序不能反。如果先关登录窗口再打开主窗体部分 Linux 图形环境下会出现主窗体闪现后消失的问题属于线程时序的玄学顺序调对就稳定了。3. 核心功能实现登录、选课、退课、成绩录入骨架搭好之后剩下的就是往里面填业务功能。这一章的代码量最大但每一段都有固定的写法套路。我的建议是先把 DAO 层写完再用一个简单的主方法去测确认 SQL 没问题后再写 Swing 界面不要边写界面边调 SQL那样报错时根本分不清是界面问题还是 SQL 问题。3.1 登录与权限UNION 查询区分学生和教师登录逻辑的常见做法是查询时带上角色字段一次查出用户身份。我用 UNION 把学生表和教师表合并查询好处是 Service 层只需要一个方法不用先查学生表再查教师表两次。public class UserDao { public User login(String account, String password) { String sql SELECT student_id AS id, name, student AS role FROM student WHERE student_id? AND password? UNION SELECT teacher_id AS id, name, teacher AS role FROM teacher WHERE teacher_id? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, account); ps.setString(2, password); ps.setString(3, account); ps.setString(4, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getString(id), rs.getString(name), rs.getString(role)); } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这里必须用 PreparedStatement 而不是 Statement 拼接字符串。原因不只是防 SQL 注入更实际的是字符串里如果带了单引号、中文括号这些字符拼接 SQL 时很容易因为转义问题直接抛语法错误。setString把这些都处理掉了。参数依次对应 SQL 里的四个问号前两个查学生后两个查教师同一个账号在两类表里都查一遍命中哪张表就返回哪个角色。这种做法有个边界情况如果学生的 id 恰好和教师的 id 相同UNION 会返回两行rs.next() 只取到第一行后一行被忽略。课程设计的账号体系一般不会有这种冲突但如果想做得严谨可以在数据库里约定学号以字母 S 开头、教师号以 T 开头从编号规则上杜绝重复。3.2 选课与退课事务边界与容量扣减的原子性选课是这个系统里最有技术含量的一段因为它涉及两张表的修改插入 sc 记录同时把 course 表的 selected_count 加 1。这两步必须在一个事务里否则会出现“选课记录插进去了容量没加上”或者反过来。我一般这样写public class CourseDao { public boolean selectCourse(String studentId, String courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查是否已选 String checkSql SELECT 1 FROM sc WHERE student_id? AND course_id?; try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, studentId); ps.setString(2, courseId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { conn.rollback(); return false; } } } // 2. 锁定课程行防止容量超卖 String capSql SELECT capacity, selected_count FROM course WHERE course_id? FOR UPDATE; int capacity, selectedCount; try (PreparedStatement ps conn.prepareStatement(capSql)) { ps.setString(1, courseId); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { conn.rollback(); return false; } capacity rs.getInt(capacity); selectedCount rs.getInt(selected_count); } } if (selectedCount capacity) { conn.rollback(); return false; } // 3. 插入选课记录 String insertSql INSERT INTO sc(student_id, course_id) VALUES(?,?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, studentId); ps.setString(2, courseId); ps.executeUpdate(); } // 4. 容量 1 String updateSql UPDATE course SET selected_count selected_count 1 WHERE course_id?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setString(1, courseId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } } } }事务的边界就是整个选课方法中间任何一步失败整体回滚。setAutoCommit(false)之后再取连接做所有操作最后手动 commit。这段代码里最值得讲的是SELECT ... FOR UPDATE它把 course 表这一行锁住直到事务提交或回滚才释放。两个同学同时选同一门容量只剩 1 的课数据库会让第二个事务阻塞等第一个提交后读到的 selected_count 已经是更新后的值然后判断满员、回滚。没有这个锁两个事务同时读到 selected_count59各自判断没满各自插入成功容量变成 61就超卖了——这个问题在答辩时被问到的概率极高。退课的代码是选课的反向操作事务里执行两条 SQL删掉 sc 记录再把 selected_count 减 1。注意 UPDATE 语句要写成SET selected_count selected_count - 1不要先查出来减好再写回去因为并发下读到的值可能是旧的。3.3 成绩录入与查询JOIN 语句与 JTable 刷新成绩管理的核心是两张视图教师看“谁选了我的课”学生看“我选了什么课、考了多少分”。都是多表 JOIN 查询区别只在 WHERE 条件。public ListScoreVO listStudentScores(String studentId) { String sql SELECT s.student_id, s.name, c.course_name, c.credit, sc.score FROM sc JOIN student s ON sc.student_id s.student_id JOIN course c ON sc.course_id c.course_id WHERE s.student_id ?; ListScoreVO list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, studentId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { ScoreVO vo new ScoreVO(); vo.setStudentId(rs.getString(student_id)); vo.setStudentName(rs.getString(name)); vo.setCourseName(rs.getString(course_name)); vo.setCredit(rs.getDouble(credit)); // 成绩可能是 NULL说明还没有录入 vo.setScore(rs.getObject(score) null ? null : rs.getDouble(score)); list.add(vo); } } } catch (SQLException e) { e.printStackTrace(); } return list; }成绩为 NULL 的处理必须单独判断。getDouble遇到 NULL 会返回 0显示出来就是“0 分”但实际是“未录入”。用getObject(score) null判断UI 层才能区分开这两种状态显示成“暂无成绩”而不是刺眼的 0。数据查出来后要进 JTable。JTable 的刷新是课程设计里翻车重灾区很多人把数据加载一遍就直接 setModel 一个新的 DefaultTableModel界面确实变了但排序、列宽全部重置。正确做法是复用同一个 model先清空再逐行添加public void refreshTable(JTable table, ListScoreVO data) { DefaultTableModel model (DefaultTableModel) table.getModel(); // 关键先清空所有行否则每次刷新都在尾部追加 model.setRowCount(0); for (ScoreVO vo : data) { model.addRow(new Object[]{ vo.getStudentId(), vo.getStudentName(), vo.getCourseName(), vo.getCredit(), vo.getScore() null ? 暂无成绩 : vo.getScore() }); } }setRowCount(0)这行不能省。它是把表格数据行数直接归零然后再一行行 addRow。如果不做这步第二次查询的结果会接在第一次后面越刷新越长看起来就像“数据重复”。这个问题的本质是 DefaultTableModel 内部维护着一个 Vector 数组addRow 是追加操作不是覆盖操作。教师录成绩的 SQL 更简单就是 UPDATE sc 表UPDATE sc SET score ? WHERE student_id ? AND course_id ?录入前要做合法性校验分数必须是不超过 100 的数字并且可以带一位小数。我一般用正则^\d{1,2}(\.\d)?$|^100$校验字符串同时用 Double.parseDouble 包在 try-catch 里双保险。成绩表里最好允许 NULL因为“只选了课还没考”与“考了 0 分”是两件事数据库里用 NULL 表达“未录入”最干净。4. MySQL 连接与字符集配置驱动、URL 参数与两种建库方式很多人的 Java 代码逻辑全对但程序就是连不上 MySQL或者界面上中文全部变成问号。这一段把连接层面的配置一次说透照着配能少走大半弯路。4.1 驱动与 URLMySQL 8 和 5.x 的差异MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver5.x 时代用的是com.mysql.jdbc.Driver。如果你下载的是 mysql-connector-java-8.0.x.jar却在代码里写旧类名运行时会直接抛ClassNotFoundException。驱动版本和 MySQL 版本要匹配。MySQL 5.7 用 8.x 驱动一般也能连但 MySQL 8 用老驱动是必然失败的。下载驱动时认准 jar 包名字里的版本号8.0 以上的配 MySQL 8 最稳。URL 也有对应差异:URL 参数推荐值作用useSSLfalse本地 / 课程设计场景关闭 SSL避免证书警告与握手失败characterEncodingutf8Java 与 MySQL 之间的字符传输编码serverTimezoneAsia/ShanghaiMySQL 8 驱动强制要求不设直接报时区错误allowPublicKeyRetrievaltrue使用 caching_sha2_password 认证时的兼容参数最后一个参数allowPublicKeyRetrievaltrue比较特殊MySQL 8 默认认证插件是 caching_sha2_password某些情况下 JDBC 需要它才能换取公钥完成认证。遇到Public Key Retrieval is not allowed的报错加上这个参数即可。完整 URL 就是前面 DBUtil 里的那串四个参数用连接一个都不能少。4.2 字符集三层设置库、表、连接必须一致中文乱码问题 90% 出在字符集不一致。MySQL 的字符集是分层设置的服务器级、数据库级、表级、连接级。Java 程序写入乱码通常是连接级用了 latin1界面显示乱码通常是读取时连接字符集不对。最省心的做法是从建库开始就统一 utf8mb4。utf8mb4 是 utf8 的超集能存 emoji 和生僻字课程设计阶段直接用这个不会错CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;在 MySQL Workbench 里执行这条语句右键数据库选 Alter Schema也能看到同样的选项。如果已经建好了库可以用ALTER DATABASE course_db CHARACTER SET utf8mb4;补救表同理。但连接层的 characterEncodingutf8 必须和库表一致否则 Java 端把字符串转成 UTF-8 字节流MySQL 却按 latin1 理解存进去的汉字再查出来就全是问号。判断问题出在哪一层的办法很简单在 MySQL Workbench 里直接执行 INSERT 一条中文记录然后执行查询。如果 Workbench 显示正常说明库表没问题问题出在 Java 连接层或 Swing 显示层。Swing 的 JLabel 和 JTable 显示中文乱码通常不是字体问题而是数据源就错了先把数据源查干净。4.3 Navicat 与 Workbench 导入脚本时的编码陷阱用可视化工具建表和导数据比命令行方便但有一个坑默认连接字符集可能不是 utf8mb4。Navicat 新建连接时高级选项卡里可以设置编码为 utf8mb4MySQL Workbench 的默认连接字符集一般没问题但如果从外部导入 .sql 文件右下角要确认编码是 UTF-8而不是 ANSI 或被系统默认编码误判。我自己遇到过一种典型情况Windows 上用记事本另存的 SQL 脚本是 ANSI 编码其中包含中文导入 MySQL 后所有中文变成乱码。解决方法是保存 SQL 文件时强制选 UTF-8 编码。如果已经乱码了把表删掉重建重新导一次不要试图 UPDATE 修数据修复成本远高于重建。这一章的配置做完后建议写一个最小的测试类只执行一条SELECT 1和一条带中文的 INSERT确认连接、字符集、事务全部正常后再往界面层走。这种“最小可运行验证”的习惯能帮你把环境问题和代码问题隔离开。5. 避坑 / 常见问题这五个问题几乎每个课程设计都会遇到下面这五条是我看别人代码和自己写代码时攒下的高频翻车点每一条都按“现象 → 原因 → 解决”来讲答辩前对照排查一遍能少挨不少骂。5.1 现象点击登录按钮后界面卡住几秒后才弹出结果原因是 JDBC 操作放在了 Swing 事件线程里。Swing 是单线程模型所有界面刷新都在 EDT 线程上执行在这个线程里做数据库查询连接耗时和 SQL 执行耗时都会阻塞界面刷新表现就是按钮按下去没反应。解决课程设计阶段不需要上并发工具最简单的做法是把耗时操作放到新线程里执行。用new Thread(() - { User user dao.login(...); SwingUtilities.invokeLater(...); }).start()包一层查询完成后再切回 EDT 更新界面。更高的做法是用 SwingWorker但课程设计讲清楚线程分离就够了能引入 SwingWorker 是加分项。5.2 现象关闭登录窗口后 Java 进程还在后台跑原因是setDefaultCloseOperation设为 HIDE_ON_CLOSE 或者根本没设置窗口关闭并不终止 JVM。前面登录窗口代码里我特意用了 EXIT_ON_CLOSE是因为登录窗口是系统唯一入口关掉就退出这是学生阶段最安全的设置。但在多窗口系统里如果登录窗口用了 DISPOSE_ON_CLOSE主窗体打开后用户关掉主窗体进程也可能不死因为登录窗口已经释放了。解决办法是在主窗体的 WindowListener 里写System.exit(0)或者把主窗体的关闭操作设为 EXIT_ON_CLOSE。更稳妥一点在关闭事件里先调用 DBUtil 里定义的静态关闭方法确保连接释放后再退出。5.3 现象退课后再选课同一门课的容量显示不准原因是容量字段的 UPDATE 逻辑没有包事务或者更新 SQL 写成了先 SELECT 再 UPDATE。我见过有人这样写先查 selected_countJava 里减 1再 UPDATE 回数据库。这在单用户测试时没问题但两个人同时退课就会丢更新。解决退课 SQL 直接写UPDATE course SET selected_count selected_count - 1 WHERE course_id ?让数据库自己完成减一操作不要先读后写。同时把“删除 sc 记录”和“容量减一”放进同一个事务方法和选课时一样。容量字段的准确性和事务边界强相关这也是数据一致性在课程设计里最典型的体现。5.4 现象JTable 数据第一次正常第二次查询后旧数据还在原因就是前面说的setRowCount(0)和 addRow 的关系。很多人直接把查询结果放进一个新的 DefaultTableModel再 setModel 给 JTable但旧 model 里如果绑定了 TableModelListener旧的监听器还在数据会重叠或刷新错乱。解决复用同一个 model。JTable 初始化时就 setModel 一个空 model后续每次刷新拿这个 model 出来 setRowCount(0) 再 addRow。这个方案简单、可预测也方便在表格里嵌入 JComboBox 之类的编辑组件。如果你希望在刷新后保持列排序把 TableRowSorter 重新 set 一遍即可注意设置后要调用排序器的setSortKeys(null)清掉旧排序状态。5.5 现象录成绩时输入“98.5”报错输入“abc”也报错原因分两种输入“98.5”报错是 SQL 里 score 字段类型是 DECIMAL(5,2)但 UPDATE 语句把值当字符串拼进去了某些驱动会按严格模式拒绝隐式转换输入“abc”报错是你的 Java 层没做校验直接把字符串塞给了数据库数据库抛DataTruncation或者报不识别。解决Java 层先做两件事第一用正则校验格式第二用Double.parseDouble转型并捕获 NumberFormatException。注意 DECIMAL 字段如果定义为 DECIMAL(4,1)最大只能存 999.9若输 100.0 会被 MySQL 严格模式拒绝。这个边界要在代码里也拦一层界面上提示“分数不能超过满分”而不是等数据库报错。另外一个隐藏细节是 UPDATE 时score ?的浮点数精度Java 的 double 转 DECIMAL 可能产生 98.499999 这类值建议 setDouble 前先用 BigDecimal 保留两位小数。6. 让课程设计超出及格线并发验证与可执行交付功能能跑只是及格能证明自己考虑过边界才有机会拿高分。做完基础功能后我建议花半天时间做三件事并发验证、单元验证、打包。并发验证最简单的方法开着两个登录窗口用两个不同学生账号同时选同一门只剩 1 个名额的课程两个窗口几乎同时点选课按钮。如果数据库的SELECT ... FOR UPDATE和事务生效只有一个会成功另一个会提示“已满”。如果两个都成功说明事务锁没起作用回去检查setAutoCommit(false)是否在SELECT ... FOR UPDATE之前执行。单元验证用 JUnit 或者干脆写一个带 main 的测试类都行重点覆盖三个场景重复选课被拒绝、退课后容量回退、成绩录入 NULL 与合法值转换。这些测试代码本身就是答辩时展示专业度的素材。打包这一步特别提一下。在 IDEA 里通过 Artifacts 打 jar 时记得把 mysql-connector-java 的 jar 包加到依赖里否则生成的 jar 在别人机器上跑会报ClassNotFoundException。打完包后到命令行用java -jar course_system.jar跑一遍这一步能提前暴露一堆 IDE 里看不到的路径问题。也可以在 lib 目录里放驱动包启动脚本用java -cp course_system.jar:lib/mysql-connector-java-8.0.x.jar MainClass指定 classpath。如果还想再往前走一点把 DBUtil 里的直连换成 HikariCP 连接池改动量很小代码风格却会明显上一个档次MySQL 的数据库连接池场景也覆盖到了。连接池不是课程设计要求的但能答出“为什么 DriverManager 直连不适合生产”这个问题本身就是加分项。最后说一个我自己踩过的习惯做这个系统时我一度沉迷于把界面做得花哨反复调字体和颜色结果花的时间远超写 SQL 的时间。后来答辩时发现老师更在意的是事务边界、SQL 写法、异常处理这些“看不见”的地方。界面干净、交互合理就够了把精力留给上面这些真正有区分度的细节才是课程设计最划算的时间分配。希望这篇笔记能让你少走几步弯路把自己的系统稳稳跑起来。本文还有配套的精品资源点击获取
返回列表