多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java+SQL Server+GUI学籍系统实战:从表设计到JDBC事务与Swing避坑

Java+SQL Server+GUI学籍系统实战:从表设计到JDBC事务与Swing避坑 简介基于Java与Sql Server实现的GUI学籍管理系统完整资源面向高校Java课程设计、数据库实训及信息系统开发入门者解决学生信息、成绩、班级与教学计划的一体化管理需求。压缩包共52个文件以24个Java源码、10个TXT数据、5个SQL脚本为主附带可直接运行的JAR包、PPT展示、docx使用说明书及png/jpg运行截图整体仅2.55MB方便本地环境快速部署调试。功能层面覆盖txt批量导入学生/成绩/课程信息、手动录入与批量修改、班级增删改查、按学号或课程号修改成绩、多字段组合查询教师和成绩详情以及基于选修/必修学分阈值的自定义开除学生筛选登录登出依赖Sql Server数据库权限控制可限制不同用户可用功能。已有233人学习读者可借助完整工程目录、建库/视图/存储过程/函数/用户授权等SQL脚本快速理解JavaSql Server分层开发思路并复用到自己的课程设计与毕业设计中。1. 为什么“Java SQL Server GUI”的学籍系统值得自己动手写一个管理几百个学生的学籍系统很多人第一反应是“不就是增删改查吗”但这个组合恰恰是理解桌面应用与关系型数据库配合的最小完整样本Java 处理界面和业务规则SQL Server 负责数据持久化GUI 形态让问题落到桌面上不依赖 Web 容器。标题里的系统解决的是学生信息登记、班级归档、成绩录入、离校状态统一更新这类实操需求适合正在补 JDBC 和 Swing/JavaFX 实战的人也适合小机构需要一个能离线录入、不依赖公网的工具。别把它当玩具真正把连接串、事务、编码这些细节处理清楚后你在同等复杂度下踩过的坑放到大型项目里一样是那些坑。2. 先拆业务再选技术这套组合适合解决什么问题2.1 学籍系统的对象模型先别急着写代码把数据关系画出来我一般会先梳理一张关系图而不是直接开 IDE 建项目。学籍系统里跑不掉的有五类对象学生、班级、课程、成绩、操作员。学生属于某个班级班级可以按入学年份、专业划分学生与课程之间通过成绩发生多对多关系操作员是登录后执行录入、修改的那类人。这五张表的关系画出来整个系统的骨架就定了。这里有一个容易犯的毛病把学生表设计成“万能表”一个学生所有属性都往同一行塞。实践里我看到过一张学生表有 40 多个字段里面混杂着宿舍号、缴费状态甚至家庭成员联系方式这是典型的“没想清楚边界”。学籍管理关心的是在校状态、班级归属、选课与成绩这类教学环节数据缴费和宿舍通常属于另一套系统管的塞进来要么留空要么维护不起来。我的设计习惯是凡是多值属性、可能独立变化的状态都拆成单独的表或代码表。学生主表只保存天然一对一的信息比如学号、姓名、性别、出生日期、入学日期、班级、状态扩展资料、家庭联系人这类低频字段放到扩展表用学号关联。这样做主表查询时行宽小索引效率也不会被大字段拖累。2.2 为什么选 SQL Server 而不是 MySQL事务和约束更适合桌面端有些读者会问MySQL 免费又常见为什么这个方案用 SQL Server答案要分两层看。第一层是产品特性SQL Server 的事务隔离、外键级联、身份列自增这些能力非常成熟在 Windows 桌面环境里安装和运维都不难Express 版本对学籍这种数据量几千上万的系统完全够用。第二层是 JDBC 驱动的稳定性微软官方驱动对连接串、批量更新、日期时间类型支持都比较规范出问题时日志定位路径清晰很少遇到诡异行为。SQL Server 在中文场景下的一个实用优势是排序规则。数据库可以设置成中文排序规则配合 nvarchar/nchar 类型中文存取基本不用操额外的心。MySQL 在这块也不算差但如果你是在 Windows 上跑 GUI 程序让客户端直连 SQL Server从安装 SQL Server 实例、开 TCP/IP 协议到用 SSMS 图形化建表整个过程比在 MySQL 里配字符集更顺手。另外要明确边界这个方案适合单机或小规模局域网使用通常在几十到几千学生规模内。如果做成大并发的云端服务SQL Server 当然也可以但那就不应该用 Swing 客户端直连数据库的架构了。这里强调一点方案没有“绝对最好”只有“合适”。标题这个组合的优势在于部署简单、数据资产本地可控、二次开发门槛低。2.3 为什么界面选 GUI 而不是 Web数据录入型场景的务实选择GUI 在这个标题里不是情怀而是实际场景需要。学籍录入的操作者通常是教务人员操作集中在几个窗口里学生信息维护、成绩录入、查询统计。这类数据密集型录入Swing 的 JTable 内联编辑、Tab 键焦点跳转、本地弹窗校验比浏览器里来回刷新页面顺手得多。而且 GUI 程序不需要部署 Web 容器装好 JDK、配好 JDBC 驱动就能跑。当然也要承认 GUI 的代价客户端升级要重新分发多台机器都得装环境。所以这种系统的常见部署形态是一台机器装 SQL Server 作为数据服务端客户端机器只装 JDK 和程序包连接数据库数据量更小的时候甚至可以数据库和客户端装在同一台机器上。标题对应的大概率是后者。Swing 和 JavaFX 的选择上我默认用 Swing。原因不是 JavaFX 不好而是 Swing 在 JDK 8 之后仍然内置支持零额外依赖表格模型和事件监听机制足够清晰资料也多。JavaFX 的样式更现代但要在模块路径、控件皮肤上多花时间这个量级的项目Swing 能把复杂度压到最低。3. 把表设计成能长期用的样子建库脚本与字段选型3.1 表结构设计先定字段类型再写建表 SQL设计字段时我遵循几条习惯。学号用 char(10) 而不是 int因为学号往往有前导零、带年级或专业代码int 会丢掉格式姓名和中文描述用 nvarchar不要用 varchar中文场景下 nvarchar 的 Unicode 存储更稳妥性别用 char(1) 存“男”“女”存代码值 0/1 也能用但直接维护显示值在报表里少一步转换出生日期用 date不要用 datetime学籍系统不需要精确到时分秒状态字段用 tinyint 代码值配合一张状态代码表在读、休学、毕业、退学都对应数字。关于主键我建议学号用业务主键而不是自增代理键。因为学号是系统天然唯一、不重复、可读性强的业务键直接用它能避免“查学生时要先知道自增 ID”这种别扭场景。唯一需要考虑的是万一学校重新制定学号规则业务主键的修改成本比代理键高但这种情况在学籍系统里极少发生业务键带来的直观性更值得。成绩表这里要区分“考试”维度。一次考试由课程、考试批次决定所以成绩表至少在逻辑上要能区分“张三 2024 年春季的数据库原理成绩”和“张三 2023 年秋季的数据库原理成绩”。我习惯给成绩表加一个 TestBatch 字段或单独建考试批次表否则后期做成绩分析时会在同一课程下分不清学期。3.2 建库建表脚本一份能直接跑通的 SQL下面这份脚本是我在这个场景下常用的初始版本建了四张核心表班级表、学生表、课程表、成绩表。你在 SSMS 里对实例执行即可。-- 创建学籍管理数据库 CREATE DATABASE StudentManage; GO USE StudentManage; GO -- 班级表存储班级基本信息和所属年级 CREATE TABLE ClassInfo ( ClassId int IDENTITY(1,1) PRIMARY KEY, ClassName nvarchar(50) NOT NULL, GradeYear char(4) NOT NULL, -- 入学年份如 2024 CONSTRAINT UK_Class UNIQUE (ClassName, GradeYear) ); GO -- 学生表学号作为业务主键 CREATE TABLE StudentInfo ( StudentNo char(10) PRIMARY KEY, -- 学号建议统一长度 StudentName nvarchar(50) NOT NULL, Gender char(1) NOT NULL CHECK (Gender IN (N男, N女)), BirthDate date NULL, Phone nvarchar(20) NULL, ClassId int NOT NULL, StatusCode tinyint NOT NULL DEFAULT 1, -- 1在读 2休学 3毕业 4退学 CreateTime datetime NOT NULL DEFAULT GETDATE(), CONSTRAINT FK_Student_Class FOREIGN KEY (ClassId) REFERENCES ClassInfo (ClassId) ); GO -- 课程表 CREATE TABLE CourseInfo ( CourseNo char(6) PRIMARY KEY, CourseName nvarchar(50) NOT NULL, Credit decimal(3,1) NOT NULL ); GO -- 成绩表一个学生在某门课程某个批次的成绩 CREATE TABLE ScoreInfo ( ScoreId int IDENTITY(1,1) PRIMARY KEY, StudentNo char(10) NOT NULL, CourseNo char(6) NOT NULL, TestBatch nvarchar(30) NOT NULL, -- 如 2024-2025-1 期末 Score decimal(5,2) NULL, -- 百分制成绩允许空 CONSTRAINT FK_Score_Student FOREIGN KEY (StudentNo) REFERENCES StudentInfo (StudentNo), CONSTRAINT FK_Score_Course FOREIGN KEY (CourseNo) REFERENCES CourseInfo (CourseNo) ); GO这个脚本的关键点有四个。一是 ClassId 用 identity 自增因为班级本身没有有意义的业务编号数字主键够用二是 StudentNo 用 char(10) 定长如果学校学号不到 10 位程序写入时补齐或直接按实际位数修改类型长度三是成绩表保留 TestBatch 字段这是学籍系统和简单成绩记录最大的分水岭——没有它成绩会被覆盖或分不清学期四是状态字段有默认值和注释业务逻辑里看到 StatusCode 就能定位学生的在校状态。执行完脚本后建议立刻插入验证数据比如几个班级、一个学生、两门课程、一条成绩确保外键约束都生效。这一步能提前发现脚本里类型长度不匹配等低级问题。3.3 索引与外键不是所有字段都要建索引初学的人容易犯的错是给每个字段都加索引觉得“加了就快”。实际上索引会拖慢插入和更新学籍系统写操作频率不低滥用索引得不偿失。我在这里只加两种索引主键自带的聚集索引以及在成绩表的 StudentNo 和 CourseNo 上建联合非聚集索引。CREATE INDEX IX_Score_StudentCourse ON ScoreInfo (StudentNo, CourseNo);这个联合索引直接服务“查某学生的全部成绩”和“查某课程的所有学生成绩”两类高频查询。学生表的 ClassId 也可以建索引因为按班级筛选学生是常用操作但如果你确定班容量很小全表扫描也不慢那这步可以省。外键约束要保持“有动作但不过度”的默认策略。学籍系统的成绩删除不推荐级联防止误删学生导致成绩被连带清空成绩记录应作为独立历史数据保留。因此这里的外键我只约束引用完整性不设置 ON DELETE CASCADE。删除学籍数据走专门的回收逻辑比如把状态改成退学而不是物理 DELETE。4. 用 JDBC 把 Java 和 SQL Server 接通连接、分层与 CRUD4.1 驱动选择与连接串先解决“能不能连上”Java 连 SQL Server 的官方驱动叫 mssql-jdbc从 Maven 仓库获取对应版本后可以用 Class.forName 加载驱动也可以直接让 DriverManager 按 URL 自动识别。如果不想引 Maven把驱动 jar 文件放到项目 lib 目录并加到构建路径即可。连接串的写法是固定的import java.sql.Connection; import java.sql.DriverManager; public class DbUtil { private static final String URL jdbc:sqlserver://localhost:1433; databaseNameStudentManage; encryptfalse; trustServerCertificatefalse; private static final String USER sa; private static final String PASSWORD YourStrongPassword; public static Connection getConnection() throws Exception { // 较新驱动可以不写 Class.forNameDriverManager 会自动发现 return DriverManager.getConnection(URL, USER, PASSWORD); } }这块参数有几个特别值得注意。localhost:1433 是默认的 SQL Server 端口如果你的实例配置了命名实例连接串要改成 host\实例名端口可能不是 1433databaseName 指向第三章创建的库encryptfalse 是给本地开发准备的新版 JDBC 驱动默认把 encrypt 置为 true如果本地 SQL Server 没有配置受信任的证书连接会报加密相关的错误开发环境直接关掉省去证书折腾。我用 sa 账号连接示例是因为本地学习环境往往没有配置 Windows 集成认证。生产环境最好单独创建一个拥有 db_datareader 和 db_datawriter 权限的专用账号不要把 sa 口令暴露在客户端程序里否则逆向 Java 包里的配置就能拿到高权限数据库账号。4.2 三层结构界面、业务、数据访问分开别写面条代码常见的失败做法是把 SQL 语句写在按钮的事件监听里界面一多代码就成了一团。我的组织方式是三个包dao 包放数据访问类只负责 SQL 和 ResultSet 映射service 包放业务方法处理校验和组合逻辑ui 包放 Swing 界面只调用 service 方法。这样一个简单系统的类文件结构大致是com.example.studentmanage ├── dao │ ├── StudentDao.java │ └── ScoreDao.java ├── service │ └── StudentService.java └── ui ├── LoginFrame.java └── StudentManageFrame.javastudentmanage 模块不引入 MyBatis 或 Hibernate 这类框架的理由是学籍系统的 SQL 都是可预期、可枚举的JDBC 手写完全可控还能让你在调试时直接看到每一条 SQL 的执行情况。数据量小、逻辑直接引入 ORM 反而多一层映射排查成本。4.3 从查询到保存一个学生信息的完整 CRUD 代码下面这段 DAO 方法覆盖了查询列表和插入学生两个最典型操作。查询走的是 JDBC 最常规的写法插入则体现了 PreparedStatement 的两个关键点防 SQL 注入以及日期类型绑定。public ListStudent findStudentsByClass(int classId) { ListStudent list new ArrayList(); String sql SELECT StudentNo, StudentName, Gender, BirthDate FROM StudentInfo WHERE ClassId ? ORDER BY StudentNo; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, classId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setStudentNo(rs.getString(StudentNo)); stu.setStudentName(rs.getString(StudentName)); stu.setGender(rs.getString(Gender)); // getObject 能兼容数据库中的 NULL 值 stu.setBirthDate(rs.getObject(BirthDate, LocalDate.class)); list.add(stu); } } } catch (Exception e) { e.printStackTrace(); } return list; } public boolean insertStudent(Student stu) { String sql INSERT INTO StudentInfo (StudentNo, StudentName, Gender, BirthDate, ClassId, StatusCode) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stu.getStudentNo()); ps.setString(2, stu.getStudentName()); ps.setString(3, stu.getGender()); if (stu.getBirthDate() null) { ps.setNull(4, Types.DATE); } else { ps.setObject(4, stu.getBirthDate()); } ps.setInt(5, stu.getClassId()); ps.setInt(6, stu.getStatusCode()); return ps.executeUpdate() 1; } catch (Exception e) { e.printStackTrace(); return false; } }这里说几个值得留意的细节。try-with-resources 会自动关闭 Connection、PreparedStatement 和 ResultSet少写一堆 finally 且不容易漏关连接日期字段用 setObject 传 LocalDate驱动会正确转成数据库的 date 类型比手动拼字符串安全查出生日期时用 getObject 加 LocalDate.class 而不是 getDate这样能避免 java.sql.Date 转 LocalDate 的繁琐步骤也顺带处理了 NULL。修改和删除的写法与插入几乎一样只是 SQL 换成 UPDATE 和 DELETE。删除建议用“假删除”也就是把 StatusCode 改成 4 退学状态而不是真正 DELETE 行这样历史成绩还能保留以后做恢复也有余地。这是我的习惯学籍数据的审计价值高于存储成本。4.4 Swing 与数据交互刷新表格和防止界面卡死JTable 的数据填充通常用一个 TableModel 承载。最常见的问题是“点查询后表格没更新”原因是只修改了 List 数据而没有触发 TableModel 的事件刷新。我的做法是每次查询后重新构建一个 TableModel再调用 setModel 给表格String[] columns {学号, 姓名, 性别, 出生日期}; DefaultTableModel model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; // 列表只读编辑放到单独窗口 } }; for (Student stu : studentList) { model.addRow(new Object[]{ stu.getStudentNo(), stu.getStudentName(), stu.getGender(), stu.getBirthDate() null ? : stu.getBirthDate().toString() }); } studentTable.setModel(model);Model 做成只读是刻意为之避免用户在表格里直接乱改然后收到底层异常。学号、姓名这些字段的增改放到独立表单窗口校验起来更清晰。这个设计不算花哨但能把“查询展示”和“编辑维护”两条路径分开代码也好维护。5. 常见问题与避坑SQL Server 连接和 GUI 里的那些坑5.1 本地连接超时TCP/IP 协议没有启用现象JDBC 连接抛异常提示 Connection refused 或连接超时但 SQL Server Management Studio 却能正常连上。原因SSMS 走的是共享内存或命名管道而 JDBC 默认走 TCP/IP。SQL Server 安装后 TCP/IP 协议可能默认是关闭状态或者只监听了 localhost 而不是实际 IP。解决打开 SQL Server 配置管理器找到“SQL Server 网络配置”下的实例启用 TCP/IP 协议重启 SQL Server 服务。再用命令确认端口在监听netstat -ano | findstr :1433如果没有任何输出说明服务没监听端口可以看到监听记录后再把连接串里的 localhost 改成机器的实际 IP 或实例名。这个坑在我经手的项目里出现频率排第一排查时先看协议再看防火墙。5.2 中文写入变问号类型和排序规则惹的祸现象程序界面显示正常但插入库里的“张三”变成了“???”。原因要么表的字段用了 varchar 而不是 nvarchar导致字符集转换时丢失中文要么数据库排序规则不是中文相关规则还有一种情况是连接串里没有指定字符集。解决建表时所有中文字段都用 nvarchar脚本里我会同时给数据库设置排序规则CREATE DATABASE StudentManage COLLATE Chinese_PRC_CI_AS;如果数据库已经建好可以单独改列的类型。连接串上加 charset 参数对 SQL Server 驱动并不总是生效最稳定的还是从字段类型和数据库排序规则层面解决。改完类型后之前已写入的乱码数据需要清理重录这个没有后悔药所以建表时一步到位最关键。5.3 新版驱动报 SSL 加密错误现象驱动升级到较新版本之后原来能跑的代码突然报错提示无法建立安全连接。原因新版 JDBC 驱动默认启用了 SSL 加密要求服务器端配置可以被信任的证书。本地开发环境的 SQL Server 通常用的是自签名证书客户端默认不信任连接就被拦下来。解决开发环境在连接串里显式加上 encryptfalse或者加 trustServerCertificatetrue 让客户端信任自签名证书。我建议本地开发用前者简单直接如果将来程序要连生产环境的 SQL Server 并且对方已配置正规证书则应该保留 encrypttrue 并去掉 trustServerCertificate。处理这个坑的关键是理解这不是驱动坏了是安全策略变了。5.4 点击按钮后界面卡死耗时操作占住了 UI 线程现象单击“查询”按钮后窗口标题显示“未响应”过几秒或几十秒才恢复成绩导入时整个界面直接白屏。原因Swing 的界面事件在事件分发线程EDT上执行。我在事件里直接调用了 JDBC 查询网络慢或者数据量大时UI 线程被阻塞界面自然卡死。解决把耗时操作放到后台线程执行。轻量做法是用 SwingWorkerSwingWorkerListStudent, Void worker new SwingWorkerListStudent, Void() { Override protected ListStudent doInBackground() throws Exception { return studentService.findStudentsByClass(classId); } Override protected void done() { try { ListStudent students get(); refreshTable(students); } catch (Exception e) { JOptionPane.showMessageDialog(frame, 查询失败 e.getMessage()); } } }; worker.execute();这样查询期间界面能保持响应进度条和取消按钮也能正常使用。这个改动对用户体验的提升非常直接是 GUI 程序从“能用”到“好用”的重要分水岭。5.5 插入重复学号导致程序弹异常主键冲突没有转成业务提示现象用户输入的学号在库里已存在点击保存后程序直接弹出英文异常信息或者静默失败没有任何提示。原因主键冲突被数据库以 SQLException 抛出来但程序里没有区分这个异常类型也没有做预校验。解决两种策略配合。保存前先用一条 SELECT 判断学号是否存在给出中文提示同时在插入代码里捕获 SQLException通过异常状态码或消息内容识别主键冲突再统一转成“该学号已存在”的提示。预校验在并发不高的时候够用异常捕获是兜底两者都做才稳。6. 从能跑到好维护批量导入、口令存储与验证技巧学籍系统真正让人头疼的往往不是单条增删改查而是学期初的成绩导入和账号安全问题。批量导入成绩时逐条调用 insert 方法既慢又不安全——如果导入到一半出错前面的数据已经落库还得手工清理。更好的做法是使用 JDBC 的批量更新加事务控制让导入过程要么全部成功、要么全部回滚public void batchImportScores(ListScore scores) { String sql INSERT INTO ScoreInfo (StudentNo, CourseNo, TestBatch, Score) VALUES (?, ?, ?, ?); try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Score sc : scores) { ps.setString(1, sc.getStudentNo()); ps.setString(2, sc.getCourseNo()); ps.setString(3, sc.getTestBatch()); ps.setObject(4, sc.getScore()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } } catch (Exception e) { e.printStackTrace(); } }这个方法的收益在几百条以上就非常明显不需要任何中间件JDBC 原生机制就能做到。事务的边界是整批导入凡是跨越多条数据、必须维持一致性的写入都应当放在同一个事务里这是我认为这个项目里最值得养成的一个习惯。操作员口令不要明文存到库里。常见做法是对密码做加盐哈希比如用 SHA-256 或 BCrypt 生成摘要登录时对比摘要而不是原文。虽然学籍系统一般规模不大但口令泄露的风险不值得赌。最后说一个调试技巧如果怀疑某条 SQL 性能有问题我一般先把 DAO 里拼好的 SQL 打印到控制台复制到 SSMS 手工执行并开启“显示估计的执行计划”。通过执行计划能直接看到是走了索引还是全表扫描指引你调整索引设计而不是凭感觉加索引。这些习惯我都是在维护这类系统的过程中一点点沉淀下来的第一次做时也因为 SSL 参数折腾过大半天后来学会了先看驱动版本行为再看代码逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表