ARTICLE DETAIL

资讯详情

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

Java Swing选课系统全解析:ER图、MySQL与DAO分层实现

Java Swing选课系统全解析:ER图、MySQL与DAO分层实现 简介这套基于Java Swing的学生选课成绩信息管理系统是一个从用户界面、业务逻辑到数据库层均可运行的完整项目主要面向Java初学者、在校学生以及准备毕业设计或课程设计的人群。项目使用Swing组件构建图形界面覆盖学生登录、注册、课程选择、成绩录入与查看等典型业务同时配合SQL脚本与实体关系ER图展示底层表结构及关联关系完整覆盖从需求分析、数据库设计到功能编码的实现链路可直接在IntelliJ IDEA或Eclipse中导入工程进行调试与二次开发。压缩包整体约131MB共142个文件内含36个Java源文件、75个class编译文件、SQL数据库脚本、ER图与说明文档、操作演示视频以及多个gif截图源码、文档、演示素材一应俱全目录分类清晰便于按需查阅。目前已有747人学习/下载。学习时可对照源码、视频与ER图逐层分析既能掌握Swing各组件的界面布局与事件处理也能理解多表关联查询、选课容量校验等业务逻辑配套资料也可作为课程报告、答辩演示和二次开发的起点。1. 基于Java Swing的学生选课成绩信息管理系统课程设计包里的ER图、源码和视频应该怎么用很多同学拿到这份基于Java Swing的学生选课成绩信息管理系统压缩包第一反应是解压、打开IDEA、点运行。但我建议你先别碰代码先打开里面的ER图文档课设答辩时老师最常问的一句话就是“你的系统有几个实体、几张表、外键怎么关联”而这些答案全在ER图里。这个项目是典型的Java课程设计案例源码技术上没有太高门槛核心价值在于把学生选课、成绩查询、教师录成绩这一条业务链完整走通适合Java基础学完、正在做课程设计或者想练手二次开发的读者。你不需要追求界面多炫只要ER图与表结构严谨、Swing界面与功能对得上、SQL不裸奔就已经是能拿得出手的作业。2. 先把ER图画明白从学生选课成绩的实体关系到MySQL建表的完整落地2.1 画ER图之前先定业务规则三个实体加一张关联表选课成绩系统看起来功能很多落到数据层面其实就两件事学生选了什么课这门课考了多少分。最常用的ER图建模方式是建立三个核心实体和一个关联关系学生、教师、课程是基础实体“选课”是学生与课程之间的多对多关系而“成绩”不是学生或课程的普通属性它是选课这个行为产生的结果属性。常见做法是把成绩挂在一张独立的选课关系表上表名叫sc结构至少包含学号、课程号、成绩、学期。这里有个容易忽略的业务规则同一个学生同一门课只能选一次也就是说“学号课程号”需要做成唯一约束同一位教师可以上多门课但一门课在同一个学期原则上只有一个教师课程表里放教师号作为外键即可。设计ER图时的实体属性可以这样划分实体关键属性主键策略关系说明学生学号、姓名、性别、专业学号与选课关系为一对多教师教师工号、姓名、职称教师工号与课程为一对多课程课程号、课程名、学分、容量自增主键与选课关系为一对多选课关系学期、成绩自增主键 唯一约束联结学生与课程两个外键如果你在整理需求时对“一对多还是多对多”拿不准就走一遍业务场景假设跳出一个学生张三他选了“数据结构”和“操作系统”两门课再跳出一门“数据结构”班上有50个学生选它。这个双向事实说明学生与课程确实是多对多不做关联表你就会在某一张表里存重复数据后面成绩录入必然翻车。2.2 把ER图转关系模式一对多放外键多对多拆中间表ER图画完之后下一步是把ER图转关系模式这一步几乎是所有课设的硬伤点。绝大多数教材都会给一套标准规则每个实体转成一张表一对多关系在“多”的一侧加外键多对多关系新建一张中间表中间表的主键通常是自增ID同时把两端的主键作为外键带进来如果关系本身携带属性比如成绩、选课时间就把这些属性放在中间表而不是实体表里。拿这个项目来说教师和课程是一对多所以course表里要出现teacher_id如果偷懒把教师信息连同课程一起做成冗余字段之后教师职称变了、姓名改了历史选课数据里的教师名也会跟着乱这是典型的冗余字段问题。学生和课程是多对多中间表sc把学生、课程、成绩、学期都收纳进来主键不用自增ID也没问题可以直接用联合主键“学号课程号”但很多同学后期要按选课记录逐条操作自增主键在定位某一条记录时更方便。PowerDesigner、MySQL Workbench、Draw.io都可以画这类ER图。如果你用的是MySQL Workbench还可以在表结构全部建好后用逆向工程反向生成ER图这个功能对应经常搜索的“mysql的表导出er关系图”过程是Database菜单下选择Reverse Engineer选中刚才建的库Workbench会把外键关系自动画出来。用它和原始ER图文档对着看能第一时间发现漏掉外键或者关系标反的地方。2.3 MySQL建表语句把ER图落成可执行的DDLER图只是纸面设计落地到MySQL需要写DDL。我在做这一类系统时建表顺序一般从“不依赖别人”的表开始先建student和teacher再建course最后建sc否则外键会报找不到父表的错误。下面是适合这个项目的建表语句-- 学生表 CREATE TABLE student ( stu_id VARCHAR(20) NOT NULL COMMENT 学号, stu_name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, major VARCHAR(100) DEFAULT NULL COMMENT 专业, enroll_year INT DEFAULT NULL COMMENT 入学年份, PRIMARY KEY (stu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 教师表 CREATE TABLE teacher ( teacher_id VARCHAR(20) NOT NULL COMMENT 教师工号, teacher_name VARCHAR(50) NOT NULL COMMENT 教师姓名, title VARCHAR(30) DEFAULT NULL COMMENT 职称, PRIMARY KEY (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师表; -- 课程表 CREATE TABLE course ( course_id INT NOT NULL AUTO_INCREMENT COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, teacher_id VARCHAR(20) DEFAULT NULL COMMENT 任课教师工号, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, capacity INT DEFAULT 60 COMMENT 选课容量, PRIMARY KEY (course_id), KEY idx_teacher (teacher_id), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课成绩关系表 CREATE TABLE sc ( id INT NOT NULL AUTO_INCREMENT COMMENT 选课记录ID, stu_id VARCHAR(20) NOT NULL COMMENT 学号, course_id INT NOT NULL COMMENT 课程编号, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩NULL表示未录入, semester VARCHAR(20) NOT NULL COMMENT 开课学期如2024-2025-1, PRIMARY KEY (id), UNIQUE KEY uk_stu_course (stu_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课成绩关联表;这段DDL里值得注意的有三处第一所有表都用utf8mb4而不是utf8否则遇到生僻字或特殊符号在Java里显示正常、入库后变问号的情况会很难受第二sc表加了唯一约束uk_stu_course这是防止同一学生重复选同一门课的兜底手段程序写得再烂数据库这层也能拦住第三score字段允许NULLNULL语义是“已选课但未录入成绩”不要用0或-1代替否则教师端还没录分学生查询时看到的就是0分答辩时非常尴尬。导入这段脚本可以在命令行执行mysql -u root -p init.sql也可以在Navicat里直接运行。导入后最好立刻用SHOW CREATE TABLE sc;看一眼外键是否建立成功因为有些图形工具在批量执行脚本时会把外键约束悄悄吞掉。3. Swing界面骨架到具体功能登录窗体、主界面与选课成绩模块的写法3.1 用JFrame搭建登录窗体和主窗体骨架Swing界面这部分我的经验是从登录窗口开始但不要在一个类里写完所有组件。一个常见的做法是LoginFrame负责身份校验校验成功后把当前用户ID和角色传给MainFrameMainFrame用JTabbedPane按角色动态展示不同标签页。这样既符合面向对象编程Java的组织习惯后续给系统加权限控制时也不需要重写界面。登录窗体的核心代码只有一小块重点看按钮事件和角色判断public class LoginFrame extends JFrame { private JTextField userField new JTextField(12); private JPasswordField pwdField new JPasswordField(12); private JComboBoxString roleBox new JComboBox(new String[]{学生, 教师}); public LoginFrame() { super(学生选课成绩信息管理系统); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); setLayout(new GridBagLayout()); add(new JLabel(学号/工号), ...); add(userField, ...); add(new JLabel(密码), ...); add(pwdField, ...); add(roleBox, ...); JButton loginBtn new JButton(登录); loginBtn.addActionListener(e - doLogin()); add(loginBtn, ...); pack(); setLocationRelativeTo(null); } private void doLogin() { String role (String) roleBox.getSelectedItem(); String userId userField.getText().trim(); // 这里先做简单的非空判断再走UserDAO校验 if (userId.isEmpty() || new String(pwdField.getPassword()).isEmpty()) { JOptionPane.showMessageDialog(this, 账号和密码不能为空); return; } // 校验通过后跳转 new MainFrame(userId, student.equals(role) ? student : teacher); dispose(); } }这段代码里的GridBagLayout是Swing里常用的弹性布局它能控制组件居中而不是默认顶到左上角。如果你觉得自己写布局约束太啰嗦也可以改用null布局配合setBounds但那样窗口一变大小组件就会错位我一般不推荐。登录校验不要写在按钮事件里大段堆SQL放在后面第4章讲到的DAO层里界面类只负责拿到结果并提示用户。MainFrame的骨架更为直观public class MainFrame extends JFrame { public MainFrame(String userId, String role) { setTitle(学生选课成绩信息管理系统 - 用户 userId); setSize(1000, 700); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JTabbedPane tabs new JTabbedPane(); if (student.equals(role)) { tabs.addTab(在线选课, new SelectCoursePanel(userId)); tabs.addTab(成绩查询, new StudentScorePanel(userId)); } else { tabs.addTab(成绩录入, new ScoreInputPanel(userId)); tabs.addTab(课程管理, new CourseManagePanel(userId)); } setContentPane(tabs); setVisible(true); } }这样做的第一个好处是每个业务面板是独立类谁出问题改谁不会出现一个类1500行的情况第二个好处是之后想把某个面板替换成Spring Boot加MyBatis的后端调用只需要换掉面板内部的数据获取来源。算是一种低成本的“可演进”结构。3.2 学生端在线选课JTable加载课程数据与选课事件处理在线选课核心是两张表左边课程列表右边已选课程。课程列表用JTable加DefaultTableModel数据从DAO查询结果填充。这个模块我见过最普遍的翻车点是直接在循环里调用model.addRow这样做没问题但前提是查回来的数据必须提前在DAO层组装成对象而不是把ResultSet直接交给界面。public class SelectCoursePanel extends JPanel { private DefaultTableModel model; private JTable courseTable; private String stuId; public SelectCoursePanel(String stuId) { this.stuId stuId; model new DefaultTableModel( new Object[]{课程编号, 课程名称, 学分, 任课教师, 选课容量}, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; courseTable new JTable(model); courseTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); setLayout(new BorderLayout()); add(new JScrollPane(courseTable), BorderLayout.CENTER); JButton selectBtn new JButton(选课); selectBtn.addActionListener(e - doSelect()); add(selectBtn, BorderLayout.SOUTH); loadCourses(); } private void loadCourses() { model.setRowCount(0); ListCourse courses new CourseDAO().findSelectable(stuId); for (Course c : courses) { model.addRow(new Object[]{ c.getCourseId(), c.getCourseName(), c.getCredit(), c.getTeacherName(), c.getCapacity() }); } } private void doSelect() { int row courseTable.getSelectedRow(); if (row 0) { JOptionPane.showMessageDialog(this, 请先选择一门课程); return; } int courseId (int) model.getValueAt(row, 0); try { new SelectCourseDAO().insert(stuId, courseId, 2024-2025-1); JOptionPane.showMessageDialog(this, 选课成功); loadCourses(); } catch (SQLException ex) { if (ex.getErrorCode() 1062) { JOptionPane.showMessageDialog(this, 你已经选过这门课了); } else { JOptionPane.showMessageDialog(this, 选课失败 ex.getMessage()); } } } }代码里isCellEditable这个方法值得留意不重写它表格里的单元格是允许双击编辑的用户乱改显示数据虽然不影响数据库但会让界面看起来像有Bug。选课按钮里通过捕获MySQL错误码1062来识别唯一约束冲突这比先查一遍再插再判断要稳因为两个用户同时选同一门课的场景下先查后插仍然可能撞车。课程容量字段目前只做了展示真正严格的控制需要在DAO层update时带上余额判断。3.3 教师端成绩录入下拉框联动与表格数据回显教师端成绩录入面板的业务逻辑是先选课程再列出这门课所有已选学生然后录入成绩。这里面有个很实际的体验问题不能在JTable里直接输入分数也不建议给每个学生放一个文本框最方便的做法是单击选中一行在下方单独的JTextField里输入成绩点“保存成绩”后更新当前行。public class ScoreInputPanel extends JPanel { private JComboBoxString courseBox new JComboBox(); private DefaultTableModel model; private JTable scoreTable; private JTextField scoreField new JTextField(10); private String teacherId; public ScoreInputPanel(String teacherId) { this.teacherId teacherId; setLayout(new BorderLayout()); loadCourses(); JPanel topBar new JPanel(); topBar.add(new JLabel(选择课程)); topBar.add(courseBox); courseBox.addActionListener(e - loadStudents()); add(topBar, BorderLayout.NORTH); model new DefaultTableModel( new Object[]{学号, 姓名, 课程名, 成绩}, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; scoreTable new JTable(model); scoreTable.getSelectionModel().addListSelectionListener(e - { int row scoreTable.getSelectedRow(); if (row 0 model.getValueAt(row, 3) ! null) { scoreField.setText(String.valueOf(model.getValueAt(row, 3))); } }); add(new JScrollPane(scoreTable), BorderLayout.CENTER); JPanel bottomBar new JPanel(); bottomBar.add(new JLabel(成绩)); bottomBar.add(scoreField); JButton saveBtn new JButton(保存成绩); saveBtn.addActionListener(e - saveScore()); bottomBar.add(saveBtn); add(bottomBar, BorderLayout.SOUTH); } }课程下拉框里显示的是课程名但真正提交时需要一个课程ID。常见做法是下拉框的item保存一个自定义的CourseItem对象重写toString返回课程名这样界面上看到的是文字选中后取到的是完整对象。如果嫌麻烦也可以维护一个MapString,Integerkey是“课程名-学期”value是courseId。这个模块和后面DAO层配合时成绩更新SQL要同时带course_id和stu_id两个条件避免选错了课程把另一个班的成绩覆盖了。4. 用DAO和JDBC把业务串起来数据层代码分离与SQL参数动态绑定4.1 为什么Swing课设也要做分层DAO接口把数据访问与界面隔开很多人以为分层是Spring Boot才需要的Swing项目一个类一把梭也能跑。但实际课设做下来界面类里写满ResultSet和getString的代码后面想改一个字段名得把所有界面类翻一遍这比改数据库本身还痛苦。更合理的最小分层是界面类只负责展示数据和收集输入业务逻辑放Service或直接放DAO数据库访问集中在DAO。在这个系统里DAO按实体划分成StudentDAO、CourseDAO、TeacherDAO、SelectCourseDAO四个类每个类只写它对应的增删改查。这算面向对象编程Java里“单一职责”最直觉的体现。如果嫌类太多至少也要抽一个DBUtil工具类负责连接管理一个CourseDAO负责课程与选课查询一个ScoreDAO负责成绩更新。界面里不许出现executeQuery字样这是我给这个项目划的红线。4.2 PreparedStatement动态SQL查询、插入、更新三种场景的写法JDBC里最基础也是最容易被扣分的知识点就是用PreparedStatement做参数绑定。拼接字符串的方式注入风险大还有中文和引号问题现在的课设答辩老师基本都会问。下面分别给出三个高频场景的写法。查询可选课程public ListCourse findSelectable(String stuId) { String sql SELECT c.course_id, c.course_name, c.credit, t.teacher_name, c.capacity FROM course c JOIN teacher t ON c.teacher_id t.teacher_id WHERE c.course_id NOT IN ( SELECT course_id FROM sc WHERE stu_id ? ) ORDER BY c.course_id ; ListCourse result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Course c new Course(); c.setCourseId(rs.getInt(course_id)); c.setCourseName(rs.getString(course_name)); c.setCredit(rs.getBigDecimal(credit)); c.setTeacherName(rs.getString(teacher_name)); c.setCapacity(rs.getInt(capacity)); result.add(c); } } } catch (SQLException e) { throw new RuntimeException(查询可选课程失败, e); } }这里SQL里的?是占位符ps.setString(1, stuId)把第一个占位符替换成学号PreparedStatement会安全处理特殊字符。注意外层用了try-with-resources语法ps和conn不用手动close资源管理更保险。JDK9开始还支持在同一个try块里管理多个资源上面代码里查询结果rs也是自动关闭的。插入选课记录public void insert(String stuId, int courseId, String semester) throws SQLException { String sql INSERT INTO sc(stu_id, course_id, semester) VALUES(?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuId); ps.setInt(2, courseId); ps.setString(3, semester); ps.executeUpdate(); } }如果课程表里选课容量是60人这里还应单独做一个“先查已选人数再插入”的原子操作否则多人同时抢最后一门课会超卖。事务部分放在第6章再说这里至少把insert和查询分开保证DAO方法语义单一。更新成绩public void updateScore(String stuId, int courseId, BigDecimal score) throws SQLException { String sql UPDATE sc SET score ? WHERE stu_id ? AND course_id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setBigDecimal(1, score); ps.setString(2, stuId); ps.setInt(3, courseId); int rows ps.executeUpdate(); if (rows ! 1) { throw new SQLException(更新了 rows 行不是预期的一行); } } }执行后检查rows的值是个好习惯。如果update语句没写where条件或者where写错了更新0行或全表更新是不会报错的唯一能发现问题的就是rows。这也是很多同学成绩录进去之后学生端查不到的原因界面提示保存成功其实affectedRows是0。4.3 DBUtil连接管理URL参数和资源释放顺序连接管理是JDBC最容易引发“血泪经验”的地方。初始化连接时URL上的参数直接影响中文是否乱码和连接是否成功public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_course ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD root123; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL里的参数一条都不能省characterEncodingutf8管住中文serverTimezoneAsia/Shanghai避免MySQL 8.0版本报时区错误allowPublicKeyRetrievaltrue解决MySQL 8连接时的公钥检索提示。Class.forName这一行在新版MySQL驱动里可不写但写了能兼容旧驱动也方便你在静态块里排查依赖缺失。关闭资源的原则是后开先关ResultSet先关再Statement最后Connection。如果用try-with-resources顺序是自动从内到外关闭的不需要手动操作。还有一个初学者常踩的坑是关闭Connection之后还想用ResultSet这必然抛异常数据要及时转成Java对象保存下来。5. 避坑实战Swing选课成绩系统里最容易翻车的五个场景5.1 中文乱码控制台、Swing界面、MySQL三处一起排查现象JTable里中文显示正常但写入MySQL后变成了问号或者反过来数据库里有中文Swing里读出来是乱码。最迷的是有时候开发环境里正常换一台电脑打包运行就又乱了这类问题确实很“玄学”。原因常见是三层中某一层编码不一致。MySQL连接参数里没带characterEncodingutf8MySQL表默认字符集是latin1IDE的编译编码不是UTF-8。这三个问题常常叠在一起。解决优先级最高的是把URL参数加上characterEncodingutf8再确认建表语句是utf8mb4。做完这两步后如果界面还乱就检查IDE的File Encoding设置把项目编码、全局编码都改成UTF-8如果是控制台乱把IDEA的Console编码改成UTF-8。不要在代码里用new String(bytes, utf-8)去强行转码那是治标不治本还会把本来正确的数据转坏。5.2 Swing界面假死点查询后窗口拖不动CPU飙满现象点击“查询课程”或“查询成绩”按钮后整个窗口卡住鼠标拖不动窗口标题栏出现“未响应”过几秒才恢复。这在数据量小的时候几乎不出现一旦sc表里数据上千行就频繁发生。原因Swing的界面事件处理是在EDT线程上执行的按钮的actionPerformed方法里直接执行了JDBC查询数据库操作阻塞了EDT线程导致界面无法重绘。这不是死锁却比死锁更让人抓狂。解决用SwingWorker把耗时操作放到后台线程完成后再回到EDT更新界面。SwingWorkerListCourse, Void worker new SwingWorker() { Override protected ListCourse doInBackground() { return new CourseDAO().findSelectable(stuId); } Override protected void done() { try { ListCourse list get(); model.setRowCount(0); for (Course c : list) { model.addRow(new Object[]{...}); } } catch (Exception e) { JOptionPane.showMessageDialog(SelectCoursePanel.this, 查询失败); } } }; worker.execute();技巧在于doInBackground里只做数据库查询done里只做界面渲染。及格线是窗口不再卡死进阶级做法是查询期间给一个“正在加载”的提示这个用SwingWorker的setProgress配合JProgressBar实现即可。5.3 外键约束失败删除课程时弹出Cannot delete or update现象在课程管理界面里删除一门有学生选过的课程系统报错Cannot delete or update a parent row但删除那些没人选的课程却又正常。原因sc表里存在引用该course_id的选课记录外键约束阻止删除。这其实是数据库在保护数据完整性但课设里很多同学没处理这个异常用户看到报错会觉得是Bug。解决做法分两种。第一种是在删除课程前先删除选课记录用事务包裹保证两步要么都成功要么都失败第二种是建表时给外键加ON DELETE CASCADE父课程删除时子选课记录自动清理。我在这个项目里倾向于后者因为选课记录离开课程没有任何保留价值。但要注意成绩单历史记录如果要做毕业存档就不能级联删除要改成逻辑删除字段。5.4 JDK版本差异换成JDK17后项目编译不过现象在原本JDK8环境跑得好好的工程换到JDK17的电脑上编译报错报错信息指向com.sun.image.codec.jpeg或提示模块访问受限。项目本身没有改动任何代码。原因从JDK9开始模块化很多com.sun.*内部类不再开放尤其是sun.misc.BASE64Encoder这类类名非常显眼另外一些老代码里用到的javax.xml.bind包也被移出了JDK需要额外引入依赖。解决如果项目没有特殊需求最简单的是把项目SDK和语言级别退回JDK8如果必须用高版本JDK就把代码里的内部类替换成标准API例如Base64用java.util.Base64编码解码日期操作改用java.time包。界面上Swing本身不受影响javax.swing在JDK里是长期存在的标准库。5.5 同一学生重复选课数据库唯一约束兜底现象学生在界面上连点了两次选课按钮结果sc表出现两条一模一样的记录教师录入成绩时分不清该给哪条录。原因按钮事件没有加防重复提交同时代码逻辑里也没有先查重再插入两个请求并发时都能通过。解决数据库层加UNIQUE KEY uk_stu_course(stu_id, course_id)程序层在insert时捕获MySQL的1062错误码并给出友好提示。改完之后无论用户在界面点得多快数据库都能兜住。另外选课按钮在事件处理开始时调用setEnabled(false)处理完后重新setEnabled(true)也能在体验上减少误触。6. 进阶收尾用ER图反查代码一致性顺手补上事务与日志6.1 用ER图反查代码字段名和表关系对不对得上当我完成一个版本的Swing选课系统最常用的验证方法是把建表SQL重新导入MySQL Workbench的逆向工程生成一张当前数据库的ER关系图再和项目自带的ER图文档逐字段对照。重点看三处外键数量是否一致sc表是否仍然保留着学生与课程之间的关联字段类型是否和Java实体类的属性类型匹配。曾有一次我检查发现course表有teacher_id但我的Course实体类没有teacherName字段导致课程列表显示不出任课教师这类问题只靠肉眼读代码很难发现用ER图反查则一眼看到。如果你以后想从手写JDBC切换到MyBatis Plus甚至利用MyBatis Plus根据Java实体类生成创建表的SQL语句那么现在保持Java实体类和表结构字段一一对应的习惯会让迁移成本大幅降低。6.2 给系统补上事务与日志一个看起来更职业化的收尾成绩录入这个场景特别适合加事务。一次成绩保存通常不只有UPDATE sc表可能还要在score_log表里写一条操作记录。两者必须同时成功或同时失败否则日志有了成绩没更新或者成绩更新了日志丢了。常见做法是同一连接上关闭自动提交后手动提交public void saveScoreWithLog(String stuId, int courseId, BigDecimal score, String operator) throws Exception { try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try { String updateScore UPDATE sc SET score? WHERE stu_id? AND course_id?; try (PreparedStatement ps conn.prepareStatement(updateScore)) { ps.setBigDecimal(1, score); ps.setString(2, stuId); ps.setInt(3, courseId); ps.executeUpdate(); } String insertLog INSERT INTO score_log(stu_id, course_id, operator, op_time) VALUES(?,?,?,NOW()); try (PreparedStatement ps conn.prepareStatement(insertLog)) { ps.setString(1, stuId); ps.setInt(2, courseId); ps.setString(3, operator); ps.executeUpdate(); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } }日志方面不必引入Log4j用java.util.logging.Logger打一行操作日志足够Logger logger Logger.getLogger(ScoreService); logger.info(成绩已保存: stuId stuId , courseId courseId , score score);我自己的习惯是每完成一个模块就对照ER图检查一遍不是等项目全部写完再统一验证。经历了太多次“代码能跑但逻辑对不上”的翻车之后我现在的第一反应永远是先打开ER图文档而不是先点那个运行按钮。一个课设项目能不能让人愿意投入精力完善标准其实不高关系设计合理、界面能完整跑通业务流程、数据库层有约束、异常能被用户看懂。做到这四点你已经超过了大多数拿压缩包原样交差的同学。希望帮到你。本文还有配套的精品资源点击获取
返回列表