ARTICLE DETAIL

资讯详情

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

Java课程设计:学生成绩管理系统从MySQL建表到JDBC避坑指南

Java课程设计:学生成绩管理系统从MySQL建表到JDBC避坑指南 简介这是一份面向高校Java课程设计与期末项目的学生成绩管理系统完整源码包适合正在学习Swing/GUI编程、数据库连接与文件导入导出操作的读者。系统覆盖登录验证、学生与教师注册、成绩增删改查、文件导入、学生账号管理及密码修改等模块功能链路完整可直接作为课设参考或二次开发基础。压缩包共80个文件约5.74MB主要包含28个Java源文件、对应class编译文件、两个可用jar包、实验报告docx文档、数据库驱动相关文件及少量图片资源导入Eclipse即可运行。目前已有9122人浏览学习该资源热度较高。配套实验报告对系统设计、模块实现和运行流程有清晰说明有助于快速理解项目结构并完成答辩准备。1. 学生成绩管理系统Java 课设最常见的题为什么多数人栽在数据库和报告上每逢期末Java 课程设计的选题列表里十有八九会躺着“学生成绩管理系统”。这个题目听起来很基础无非是给成绩单做增删改查。可真动手时你会发现自己同时面对 Java 语法、JDBC、SQL 设计、代码分层的考卷最后还要写一份足以让老师点头的实验报告。我见过太多人卡在同一个地方数据库驱动的 ClassNotFoundException 翻来覆去中文乱码一身冷汗系统跑通了又不知道报告里该写什么。我会把从需求、建表、Java 实现到报告答辩的完整路线过一遍包含可以直接复制的 SQL 和 Java 代码也把最常见的五个坑按现象、原因、解决列清楚。适合正在做课设的新手也适合想快速搭一个项目骨架的开发者。2. 先把需求钉死功能拆分、流程与四张核心数据库表2.1 成绩管理系统的功能边界要做什么、不要做什么先明确边界这是后面不翻车的前提。课程设计语境下的“学生成绩管理系统”核心就是围绕成绩做数据库增删改查再叠加一点统计。不要把题目理解成一个产品老师考察的是你有没有把 Java 和数据库串起来。常见功能清单如下模块功能点涉及数据表学生管理新增、查询、修改、删除学生student课程管理新增、查询、修改、删除课程course成绩管理录入、修改、删除、查询成绩score统计课程平均分、学生总分、成绩排名score / course登录可选控制台或界面入口校验sys_user“不要做什么”往往比“要做什么”更能决定你能否按期交。别给系统加角色权限、排课、考勤、可视化大屏不要因为熟悉就引入 Spring Boot 和 MyBatis除非老师明确要求。原因很简单这门课考察的是 Java SE 和 JDBC用框架确实方便可一旦被问到“你的 PreparedStatement 哪里用了”你很容易答不上来。控制项目范围把写报告的时间留出来才是课程设计的最优解。整体流程也不复杂用户进入菜单选择功能输入数据UI 层把请求交给 ServiceService 调用 DAODAO 通过 JDBC 执行 SQL结果一层层返回到界面。如果做控制台程序UI 层就是菜单打印和 Scanner如果做 SwingUI 层是 JFrame 和 JButton 的事件监听。建议先做控制台能跑通后再加图形界面顺序不要反。2.2 MySQL 建表脚本学生、课程、成绩、用户四张表的结构与字段说明我一般用 MySQL 8.0字符集固定 utf8mb4。四张表的关系很清晰学生和课程是多对多成绩表 score 是多对多的中间表同时它也是核心业务表sys_user 独立用来做登录。直接复制下面这段 SQL先建库建表再插测试数据后面代码才能跑。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE student ( sno CHAR(10) PRIMARY KEY COMMENT 学号, sname VARCHAR(50) NOT NULL COMMENT 姓名, sex ENUM(男,女) DEFAULT 男 COMMENT 性别, age INT COMMENT 年龄, class_name VARCHAR(50) COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( cno CHAR(6) PRIMARY KEY COMMENT 课程编号, cname VARCHAR(50) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) NOT NULL DEFAULT 0.0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE score ( sno CHAR(10) NOT NULL COMMENT 学生学号, cno CHAR(6) NOT NULL COMMENT 课程编号, score DECIMAL(5,2) NULL COMMENT 成绩NULL表示缺考, PRIMARY KEY (sno, cno), CONSTRAINT fk_score_student FOREIGN KEY (sno) REFERENCES student (sno) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_score_course FOREIGN KEY (cno) REFERENCES course (cno) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user ( username VARCHAR(30) PRIMARY KEY, password VARCHAR(64) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO sys_user (username, password) VALUES (admin, admin123); INSERT INTO student (sno, sname, sex, age, class_name) VALUES (202408001, 张三, 男, 20, 计科2401), (202408002, 李四, 女, 19, 计科2401), (202408003, 王五, 男, 21, 计科2402); INSERT INTO course (cno, cname, credit) VALUES (C101, Java程序设计, 3.0), (C102, 数据库原理, 2.5); INSERT INTO score (sno, cno, score) VALUES (202408001, C101, 85.5), (202408001, C102, 90.0), (202408002, C101, 78.0);这段 SQL 的设计理由值得展开说。学号 sno 用 CHAR(10) 而不是 INT因为学号只是标识符不做算术运算而且开头为零的学号用 INT 会丢用字符串还能统一扩展成 12 位。性别字段用 ENUM 简单直接MySQL 支持如果换 SQLite 就改成 CHAR(2)。成绩字段用 DECIMAL(5,2) 表示最大三位整数加两位小数避免 float/double 在求和时产生 0.0000001 之类的误差平均分和总分才能对得上。score 表联合主键 (sno, cno) 的价值是同一学生同一课程只能存在一条成绩记录想让数据错都错不进去。外键用 ON DELETE CASCADE删除学生时相关成绩自动清空这正好回答了答辩时“删学生成绩怎么办”的问题。插入的测试数据覆盖了三个学生、两门课程、三条成绩。注意我特意让张三有两门课李四只有一门王五还没有成绩这样后面测“平均分统计”时能看出 AVG 对没有成绩的学生是怎么处理的——COUNT 和 AVG 只统计非 NULL 行所以不会把缺考算成 0。如果你在实验报告里需要更多数据可以在 INSERT 语句里多复制几行不影响结构。2.3 从需求到 SQL增删改查对应哪些语句成绩统计怎么算有了表下一步是把“增删改查”翻译成 SQL。很多人到这一步就卡住其实逻辑很简单新增对应 INSERT删除对应 DELETE修改对应 UPDATE查询对应 SELECT。成绩管理模块里最常用的几条 SQL 我直接列出来-- 插入成绩成绩表中还没有该学生该课程时执行 INSERT INTO score (sno, cno, score) VALUES (202408003, C101, 88.0); -- 修改成绩根据学号和课程号定位到唯一一行 UPDATE score SET score 92.5 WHERE sno 202408001 AND cno C101; -- 删除成绩 DELETE FROM score WHERE sno 202408001 AND cno C102; -- 查询某个学生的全部成绩多表连接把课程名带出来 SELECT s.sno, s.sname, c.cname, sc.score FROM student s JOIN score sc ON s.sno sc.sno JOIN course c ON sc.cno c.cno WHERE s.sno 202408001; -- 按课程统计平均分和选课人数左连接保证没成绩的课程也列出 SELECT c.cno, c.cname, AVG(sc.score) AS avg_score, COUNT(sc.sno) AS cnt FROM course c LEFT JOIN score sc ON c.cno sc.cno GROUP BY c.cno, c.cname; -- 统计某个学生的总分和平均分 SELECT sno, SUM(score) AS total, AVG(score) AS avg FROM score WHERE sno 202408001 GROUP BY sno;这里特别注意 JOIN 和 LEFT JOIN 的差别。内连接只返回能匹配上的行如果某门课程没有任何成绩课程就不会出现在统计结果里而成绩统计通常要求所有课程都显示哪怕人数为 0所以用 LEFT JOIN。AVG 和 COUNT 会自动跳过 NULL 成绩这符合“缺考不计入平均分”的业务预期。如果你想排名可以加 ORDER BY avg_score DESC或者在 Java 里排序。如果老师指定要用 Java 实现排序算法那就在 Service 层排序SQL 层只取原始数据。这也是我在下一章要把统计逻辑放进 Service 的原因。3. Java JDBC 实现分层结构、工具类与核心 CRUD 代码3.1 项目分层实体类、DAO、Service、UI 怎么划分用纯 Java SE 做这个题最怕的是把所有逻辑塞进 main 方法。200 行内还能靠人脑硬扛到了 500 行以上连自己都分不清哪段是输入、哪段是 SQL、哪段是算平均分。所以我建议一开始就按四层拆包这也是实验报告里好画图的结构层包名职责示例类UI 层com.example.score.ui菜单、打印、Scanner、Swing 窗口Main.javaService 层com.example.score.service业务判断、统计、排序、事务协调ScoreService.javaDAO 层com.example.score.dao只写 JDBC 和 SQLScoreDao.javaEntity 层com.example.score.entity对应数据表的实体对象Score.javaUtil 层com.example.score.util数据库连接工具DBUtil.java实体类是和表字段一一对应的普通 JavaBean。以 Score 为例package com.example.score.entity; public class Score { private String sno; // 学号 private String cno; // 课程编号 private Double score; // 成绩用 Double 包装类型允许为 null public Score() {} public Score(String sno, String cno, Double score) { this.sno sno; this.cno cno; this.score score; } public String getSno() { return sno; } public void setSno(String sno) { this.sno sno; } public String getCno() { return cno; } public void setCno(String cno) { this.cno cno; } public Double getScore() { return score; } public void setScore(Double score) { this.score score; } }这里为什么用 Double 而不是 double因为成绩表里允许 NULL表示缺考。如果用基本类型 double从 ResultSet 里读出来的 NULL 会被转成 0统计时就会把缺考误当作 0 分。用包装类型 Doublers.getDouble 返回 0.0 后你可以配合 rs.wasNull() 把它置为 null这是新手最容易忽略的细节。3.2 数据库连接与配置文件DriverManager 和 Properties 的坑时区、编码数据库连接信息不要写成代码里的字符串常量。换机器、换密码就要改源码答辩时还容易在屏幕上露密码。常见做法是放到 src 下的 db.properties 文件里jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456DBUtil 类用来读取这个配置并返回 Connectionpackage com.example.score.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.util.Properties; public class DBUtil { private static Properties props new Properties(); static { try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { props.load(in); Class.forName(props.getProperty(jdbc.driver)); } catch (Exception e) { e.printStackTrace(); throw new ExceptionInInitializerError(数据库配置加载失败); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection( props.getProperty(jdbc.url), props.getProperty(jdbc.username), props.getProperty(jdbc.password)); } }关键参数逐个说明。jdbc.url 里的 serverTimezoneAsia/Shanghai 是 MySQL 8 驱动的强制要求不写就抛 InvalidConnectionAttributeExceptioncharacterEncodingutf8 保证中文写入和读取不错乱useSSLfalse 是本地开发时免去 SSL 证书告警。Class.forName 这句在 MySQL 8 下其实可以省略驱动会通过 SPI 自动注册但写上它能让新手理解“加载驱动”这回事也可以兼容旧版 JDBC。注意 db.properties 的值末尾不要留空格properties 解析会把空格认成值的一部分密码就会不对。3.3 成绩增删改查落地PreparedStatement 写法与参数说明现在写 ScoreDao。先写插入、修改、删除三个方法它们套路完全一样package com.example.score.dao; import com.example.score.entity.Score; import com.example.score.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; public class ScoreDao { public int insert(Score score) { String sql INSERT INTO score (sno, cno, score) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, score.getSno()); ps.setString(2, score.getCno()); if (score.getScore() null) { ps.setNull(3, java.sql.Types.DECIMAL); } else { ps.setDouble(3, score.getScore()); } return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } public int update(String sno, String cno, double newScore) { String sql UPDATE score SET score ? WHERE sno ? AND cno ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setDouble(1, newScore); ps.setString(2, sno); ps.setString(3, cno); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } public int delete(String sno, String cno) { String sql DELETE FROM score WHERE sno ? AND cno ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, cno); return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); return 0; } } }这里必须说清楚 PreparedStatement 的几个“脾气”。第一参数下标从 1 开始不是 0setString(1, ...) 对应第一个问号写错不会报语法错而是在执行时报参数越界或类型转换错。第二executeUpdate 返回的是受影响的行数插入一条返回 1WHERE 条件没匹配到返回 0这个返回值可以用来判断操作是否真的生效。第三成绩可能为 null所以插入代码里用 setNull(3, Types.DECIMAL) 而不是 setDouble(3, 0)这样才能把缺考语义保留下来。try-with-resources 会自动关闭 Connection 和 PreparedStatement省去一堆 finally。3.4 成绩统计与排序按课程平均分、总分排名顺带提冒泡排序的课堂要求查询是 CRUD 里最值得展开的部分。按课程查成绩列表并支持按成绩排序public ListScore findByCourse(String cno) { String sql SELECT sno, cno, score FROM score WHERE cno ?; ListScore list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, cno); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Score s new Score(); s.setSno(rs.getString(sno)); s.setCno(rs.getString(cno)); double v rs.getDouble(score); s.setScore(rs.wasNull() ? null : v); list.add(s); } } } catch (Exception e) { e.printStackTrace(); } return list; }这段代码的细节在 ResultSet 的游标上。rs.next() 每次移动到下一行返回 false 表示没有更多数据列可以用列名取值比用列序号 1、2、3 可读性好得多。rs.wasNull() 必须在调用 getXxx 之后立刻判断否则下次再取其他列就会失效。得到 List 后平均分可以直接在 SQL 里用 AVG也可以在这个 Java 列表里算。我的建议是如果已经要把成绩显示在屏幕上就在 Service 里一并算了省得多查一次数据库。如果课程设计明确要求写排序算法比如“使用冒泡排序”可以这样实现public static void bubbleSortByScore(ListScore list) { for (int i 0; i list.size() - 1; i) { for (int j 0; j list.size() - 1 - i; j) { if (list.get(j).getScore() list.get(j 1).getScore()) { Score tmp list.get(j); list.set(j, list.get(j 1)); list.set(j 1, tmp); } } } }冒泡排序的交换条件是当前成绩比下一个低这样从大到小排。时间复杂度 O(n²)对课程设计里的十几条数据完全够用但你要在报告里诚实写出它的缺点再补一句“实际开发中应使用 SQL ORDER BY 或 Collections.sort”。这么一写老师会觉得你不只完成了功能还真理解了算法边界。排序之外总分排名的常见做法是按学号分组查出每个学生的总分得到一个 List 再用同样的比较器排序。SQL 部分用“SELECT sno, SUM(score) AS total FROM score GROUP BY sno”Java 部分再处理。4. 实验报告怎么写结构、截图与答辩准备的五个关键点4.1 实验报告的标准结构从实验目的到结果分析系统跑通只是拿分的一半另一半在实验报告。老师没时间逐行看你的代码他先翻目录再截图最后看问题和总结。所以报告结构要按课程设计模板走但每部分的重点不能写偏。我整理了一份可以直接套的章节表报告章节写什么建议篇幅实验目的写清“通过本次实验掌握 Java JDBC 数据库编程、SQL 增删改查、分层设计”半页实验环境JDK 版本、MySQL 版本、IDE、操作系统列表需求分析系统功能结构图、使用流程图、数据流描述1 页数据库设计ER 图、表结构说明、建表 SQL1-2 页核心代码设计包结构图、类图、关键方法说明2-3 页测试结果测试用例表 运行截图2 页问题与解决写 3 个真实踩坑现象、原因、处理过程1 页总结与心得收获、不足、改进方向半页注意“核心代码设计”最忌讳整段贴代码。你应该挑三个片段DBUtil 的连接获取、ScoreDao 的一条 PreparedStatement 操作、排序方法的逻辑。每个片段下面用三句话说明“这段在干什么、边界条件怎么处理、为什么这样写”。报告里出现 100 行以上的代码块只会让老师觉得你在凑页数答辩也容易露馅。实验环境部分别只用一句话带过。比如“JDK 1.8、MySQL 8.0.33、IntelliJ IDEA 2023.1、Windows 11”最好精确到小版本因为时区报错和驱动版本有关这本身就能成为“问题与解决”的素材。数据库设计部分要画 ER 图时不用专业工具用 draw.io 或 Word 自带图形就够实体框里写清主键关系上标好 1:N。4.2 测试用例与运行截图怎样让老师一眼看出系统能跑测试用例表是报告里最直观的“证据”。每个功能至少一条用例每条必须包含输入、预期结果、实际结果。以成绩管理为例用例编号功能输入数据预期结果实际结果T01插入成绩sno202408003, cnoC101, score88.0插入成功查询能看到插入成功T02修改成绩sno202408001, cnoC101, 改为 92.5原成绩被替换平均分变化修改成功T03删除成绩sno202408001, cnoC102删除后查询无记录删除成功T04查询学生成绩学号 202408001显示该生两门课成绩及课程名与预期一致T05统计课程平均分课程 C101平均分85.578.0/2程序输出 81.75这里有一个很多人忽略的细节预期结果要写业务规则不能只写“能运行”。比如 T01 的预期结果里应该包含“如果该生已存在 C101 成绩则插入失败并给出提示”这就把联合主键约束写进去了。老师看到这种用例会认为你做过边界思考。截图方面控制台程序把输出直接用 table 格式对齐我习惯用 System.out.printf(|%10s|%10s|%6.2f|%n, ...) 打印表格式文本截图比一坨 println 好看得多。截图最好三张一张是程序菜单界面一张是查询成绩结果一张是 MySQL 命令行里的同一条数据三张能相互印证说明程序真的操作了数据库。4.3 答辩常问问题数据库设计依据、事务、排序算法答辩环节经常五分钟决定分数。老师的问题往往围绕你写在报告里的每句话展开所以不要写自己讲不清楚的东西。五个高频问题提前备好答案为什么成绩表用联合主键 (sno, cno)答这组值唯一标识一条成绩记录同时能防止同一个人同一门课存在多条成绩它还分别外键关联学生和课程保证引用完整性。PreparedStatement 和 Statement 的区别答PreparedStatement 预编译 SQL防止 SQL 注入执行多次时性能更好Statement 拼接字符串容易注入也不安全。删除学生时成绩表怎么办答我在建表时用了 FOREIGN KEY ON DELETE CASCADE所以删学生时数据库自动删掉关联成绩如果面试或笔试问到事务我会说更稳妥的做法是在事务里先删成绩再删学生出错回滚。平均分是 SQL 算的还是 Java 算的答我两种都做了。列表先在 SQL 里用 AVG 聚合课堂要求手写排序总分排名用 Java 算这样两端都能验证结果。为什么数据库连接不放到静态代码块外面答DBUtil 用静态块只加载一次 properties 和驱动避免每次连接都读文件每个方法内部独立建连接的缺点是高频场景开销大但课程设计可接受。把这些答案写成自己的话练两遍。回答时先给结论再补一句依据不要长篇背诵。遇到不会的问题就诚实说“这个点我没有做但我在报告里提到了后续可以改进的方向”比硬编一个答案要好。5. 避坑/常见问题排查JDBC 连不上、中文乱码、成绩统计不对下面的五条是我在课设答疑里遇到最高频的问题。每一条都按“现象 - 原因 - 解决”展开顺序也建议你按从环境到代码排查不要一上来就怀疑算法。5.1 现象java.sql.SQLException: Access denied for user rootlocalhost原因很直白用户名、密码不匹配或者 root 账户的登录 host 不是 localhost。很多时候不是 MySQL 密码错了而是 db.properties 文件被操作系统里的文本编辑器加上了看不见的空格比如 password123456 后面多了个空格就会认证失败。解决步骤先用 MySQL 命令行验证密码在终端执行 mysql -u root -p输入同样的密码能进去说明问题不在 MySQL 而在配置读取然后检查 properties 文件里每个 keyvalue 的值首尾是否有多余空白。如果密码里有类似 # 或 ! 的特殊字符properties 需要转义否则会截断或读错。最后确认驱动版本MySQL 8 用 com.mysql.cj.jdbc.Driver老驱动访问新版数据库会认证失败。如果服务没启动还会看到 Communications link failure而不是 Access denied注意区分。5.2 现象程序输出中文和数据库里全是问号这个坑基本人人都踩。原因是三层字符集没对齐建库字符集不是 utf8mb4、JDBC url 没指定 characterEncoding、控制台编译器用的是系统默认编码。解决顺序是先按第 2 章 SQL 里的 CREATE DATABASE ... CHARACTER SET utf8mb4 重新建库再检查 db.properties 的 url 末尾有没有 characterEncodingutf8最后在 IDE 的 VM options 加 -Dfile.encodingUTF-8。如果已经插入了乱码数据光改配置不会把存量乱码变回来得清理后重新插入。这里有个血泪经验不要把 UTF-8 编码的中文直接写进 Java 源文件除非 IDE 的 file encoding 是无争议的 UTF-8否则类文件里的字符串常量在编译期就可能变成乱码源头。5.3 现象连接时抛 InvalidConnectionAttributeException提示设置 serverTimezone这是 MySQL 8 驱动和 MySQL 8 服务器的时区协商问题。驱动拿不到连接时区拒绝继续。解决是在 jdbc.url 里加 serverTimezoneAsia/Shanghai。要注意这个参数对连接池同样有效如果是 XML 配置文件里的数据源 要写成 否则 XML 解析会报错。如果你在服务器上部署不要写成 serverTimezoneUTC否则时间差 8 小时成绩录入的时间字段会偏移。时区参数和字符集参数要一起写在 URL 里顺序无所谓。验证方式很简单把 jdbc.url 复制到命令行测试连接或者查 MySQL 的全局时区变量 SELECT global.time_zone两边一致才安心。5.4 现象修改成绩后再次查询程序里看到的还是旧值如果你在 DAO 里手动设置了 conn.setAutoCommit(false)并且没有在修改后 commit那当前连接的事务里查询到的仍然是旧快照重开程序连接才能看到。解决方法是明确事务边界要么不关闭自动提交单条语句执行完自动生效要么在 finally 中判断成功就 commit异常就 rollback。另一个常见原因是程序连的库和你命令行查的库不是同一个。比如 db.properties 里 jdbc.url 的路径是 student_db但 MySQL 里有两个同名库表结构一个在主库一个在测试库数据自然对不上。出现这种情况就把 url 复制到命令行 mysql -u root -p student_db -e select count(*) from score 里验证一遍。5.5 现象用冒泡排序给全班成绩排名数据量一大就卡我给一个同学排过 500 条成绩控制台等了七八秒。原因有两层冒泡排序本身 O(n²)500 条就要做大约 12 万次比较和交换更糟的是循环里反复调用 list.get(j).getScore()每次调用都有方法栈开销还在比较后立即 System.out.println 打印中间过程把 IO 也拖进去了。解决方法是把打印移出排序循环排序结束后统一输出能用 ORDER BY 就用 ORDER BY我们要向老师说明“手写排序是为了演示算法实际查询走 SQL 索引排序才是工程做法”。如果老师硬性要求冒泡那就只在单课程范围内排比如一个班一门课最多几十条肉眼看不到卡顿。还可以在循环里用一个局部变量保存 list.size()避免每次内层循环都调用 size()虽然性能提升有限但代码习惯是好的。6. 把项目做厚一点参数化配置、批量导入与结果校验6.1 批量录入成绩把事务和一“批”数据结合当你把单个增删改查写完可以试试批量导入成绩。很多同学的课设做到这里就停了但批量导入和事务校验才是和“数据库增删改查”里最容易被问到的“事务、连接管理”挂钩的点。给 ScoreDao 加一个方法public int[] batchInsert(ListScore list) { String sql INSERT INTO score (sno, cno, score) VALUES (?, ?, ?); int[] rows new int[0]; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (Score s : list) { ps.setString(1, s.getSno()); ps.setString(2, s.getCno()); if (s.getScore() null) { ps.setNull(3, java.sql.Types.DECIMAL); } else { ps.setDouble(3, s.getScore()); } ps.addBatch(); } rows ps.executeBatch(); conn.commit(); } catch (Exception e) { e.printStackTrace(); } return rows; }这里的核心变化是先 setAutoCommit(false)把多个插入放到同一个事务里最后统一 commit。只要中间有一条失败连接关闭时未提交的事务会自动回滚避免半批数据写进库里。addBatch 只是把 SQL 攒在驱动里executeBatch 才真正发到数据库。要注意如果有一条触发了联合主键冲突整个批处理抛异常需要在 catch 块里决定是跳过冲突还是整体回滚。我在课设里经常把主键冲突预判在校验阶段而不是等数据库报错。6.2 插入前的数据校验别把脏数据送进数据库批量导入前先做一次业务校验成绩范围是否在 0-100、学号在 student 表里是否存在、课程号在 course 表里是否存在、这条成绩是否已经存在。前两个用简单 if后两个用 PreparedStatement 查询。比如校验成绩是否存在public boolean exists(String sno, String cno) { String sql SELECT 1 FROM score WHERE sno ? AND cno ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, cno); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } catch (Exception e) { e.printStackTrace(); return false; } }这样批量导入时每条先查 exists存在就跳过或改成更新而不是让数据库报主键冲突。整个过程分两步走先校验再批量写库。这个习惯放到真实项目里就是“幂等导入”的雏形答辩时提一嘴含金量很高。最后说点我自己的教训。我当年第一次做这个题目直接打开 IDE 就开始写 main 方法写完才发现数据库连不上更别提写报告。后来我养成一个习惯先把 SQL 建表脚本在 MySQL 里跑一遍再用 DBUtil 写一个最简查询确认能拿到数据最后才补菜单和界面。界面永远是最后一步数据库跑通才是唯一的路基。希望这些经验能帮你少踩几个坑把课设稳稳做完。本文还有配套的精品资源点击获取
返回列表