简介:这是一份面向Java课程设计与初学者的学生信息管理系统配套说明文档,核心基于Java Swing界面与MySQL数据库,通过JDBC完成对学生信息的增删改查操作,适合需要快速搭建管理类课设项目的人群参考。文档中给出了完整的数据库建表语句,涵盖管理员表与学生表的字段设计、初始测试数据,并展示了实体类的代码结构与项目分层思路(model-dao-view),对理解Java连接数据库、封装业务对象及基础CRUD实现有直接帮助。同时说明了项目开发环境(jdk7+MySQL5+win7),便于读者复现运行环境。资源包仅含1个PDF文件,体积约135KB,内容精简集中,便于离线查阅。目前已有5010人学习/下载,是同类学生管理系统中较受关注的一份资料,尤其适合用于复习JDBC操作、Swing界面设计与MySQL基础配置。
1. 学生信息管理系统:Java+MySQL为什么到现在还是课堂、实习和外包项目的标配
你在搜索栏里敲下“Java+MySQL实现学生信息管理系统”,大概率是要交课设、给单位做内部工具,或者接了个练手的小外包。这个组合看起来很“老”,但恰恰因为老,它的技术边界、失败模式、验收标准都已经被前人踩得透透的。用Java写好业务逻辑,用MySQL存好结构化数据,两者通过JDBC或ORM框架对接,这是一条从Java SE到Web开发都能覆盖的练习路径,也是面试里问“你做过什么项目”时最容易被追问细节的项目。
它能解决的问题很具体:学生的增删改查、按条件筛选、成绩录入与统计、登录鉴权。说白了,就是一个带数据库交互的CRUD系统,但在CRUD之外,你还要应付连接管理、SQL注入、中文乱码、事务一致性这些真实生产环境才会露头的毛病。这篇文章的目标不是给你贴一大段可抄的完整代码,而是把从建库到跑通、再到排错和优化的一整条路线拆开讲:每一步为什么这样做、参数怎么调、翻车点在哪。适合正在做课设的学生,也适合刚入门想搞懂“Java怎么碰MySQL”的开发者——我尽量用做过类似方案的口吻,把黑匣子打开给你看。
2. 技术选型与库表设计:先决定主键和字符集,再写第一行Java代码
2.1 JDBC还是MyBatis:从项目规模倒推选择,而不是追热点
做学生信息管理系统,最常见的路线有两条:纯JDBC和MyBatis。JDBC是Java访问数据库的原生接口,所有ORM框架底层都是它。MyBatis则是半自动的持久层框架,SQL你自己写,映射帮你做。对于这个标题下的系统,如果目标是练底层原理、应付“手写JDBC”的考试或面试,那就用原生JDBC;如果目标是一个带界面的Web项目,且后续要加Spring Boot,那我一般建议直接上MyBatis,但本文核心讲JDBC方式,因为MyBatis的坑有一大半来自JDBC的坑。
选型的另一个维度是并发量。学生信息管理系统通常是几十人同时使用的内部系统,连接数最多几十个,JDBC连池足够。如果用MyBatis,也离不开连接池。所以先别急着引入Spring Boot全家桶,把JDBC这套跑通,后面加任何框架都只是换壳。
库表设计是决定后面少改代码的关键。学生信息管理系统最少需要三张表:学生表、班级表、成绩表。学生表存基本信息,班级表独立出来是为了避免“班级名称”字段在数百行学生记录里重复存储,成绩表则关联学生ID。如果还要做登录,再建一张用户表或直接给学生表加账号字段。下面给出建表SQL,按MySQL 8.0语法写。
CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; CREATE TABLE tb_class ( class_id INT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(50) NOT NULL UNIQUE ) ENGINE=InnoDB; CREATE TABLE tb_student ( student_id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender ENUM('M','F') DEFAULT 'M', age TINYINT UNSIGNED, class_id INT, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES tb_class(class_id) ) ENGINE=InnoDB; CREATE TABLE tb_score ( score_id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_name VARCHAR(50) NOT NULL, score DECIMAL(5,2) CHECK (score BETWEEN 0 AND 100), exam_time DATE, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES tb_student(student_id) ) ENGINE=InnoDB;这段SQL里有几个参数值得单独说明。
DEFAULT CHARACTER SET utf8mb4不是可有可无的装饰,MySQL 5.7 之前很多系统默认用utf8,它只能存基本多语言字符,存不了emoji和某些生僻字;utf8mb4是真正的四字节UTF-8。如果建库时漏掉这个,后面Java写入中文时一旦字段长度按字符数计算,可能直接报“Data truncation”或者出现乱码。COLLATE utf8mb4_unicode_ci是排序规则,ci表示大小写不敏感,查询where name = 'Zhang'能匹配到zhang,这符合学生姓名检索的直觉。
主键统一用INT AUTO_INCREMENT。如果以前用过UUID做主键,在这个系统里要慎重:UUID作为字符串主键占用空间大,且随机无序导致B+树索引页分裂频繁,写入性能明显下降。学号student_no虽然业务上唯一,但可能包含格式变更风险,比如“2023级”改成“2024级”时学号规则调整,所以不把它当主键,而是用额外的唯一索引约束。ENGINE=InnoDB是MySQL 8.0默认存储引擎,支持事务和外键,MyISAM在崩溃恢复上的脆弱性在课程设计这种多轮增删改场景下容易丢数据。
外键设计上,tb_score引用tb_student,这样删除学生时如果成绩表里有引用,MySQL会拒绝删除或级联删除,由你决定。我一般用ON DELETE RESTRICT(默认),防止误删学生留下孤儿成绩记录。别在Java里手工维护“先删成绩再删学生”的逻辑,数据库外键是最后一道保险。
2.2 性别字段用ENUM还是TINYINT:一个影响取值的判断
性别这个字段,很多教材用CHAR(2)存‘男’/‘女’,也有用BIT存0/1。两种都有代价。ENUM('M','F')在MySQL内部是整数枚举,读取出来是字符串,Java里用String接就行,避免你在代码里写if (gender == 0)这种魔法数字。但ENUM有个坑:如果你后续要加“未知”这个值,必须ALTER TABLE修改枚举列表,否则插入失败。TINYINT更灵活,但Java端就要维护“0=女,1=男”的映射表,一旦有人写错就产生脏数据。
我的建议是:如果系统只有一个入口(Java代码写死),用ENUM足够,直观且不易出错;如果未来可能有数据迁移或报表工具直接读库,用TINYINT加注释更稳妥。本文前面SQL用了ENUM,你在自己的项目里可以按这个权衡调整。重点是把约束放在数据库层面,而不是依赖应用层判断。
2.3 连接参数里必须手写的三个配置
任何时代写Java连MySQL,java.sql.DriverManager或连接池都需要一个JDBC URL。常见写法是:
String url = "jdbc:mysql://localhost:3306/student_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; String user = "root"; String password = "your_password";useUnicode=true&characterEncoding=utf8是让驱动以UTF-8字节流与MySQL通信。注意这里characterEncoding=utf8是给驱动用的字符集名,不是数据库的字符集,MySQL驱动能识别utf8和utf8mb4,写utf8不影响与utf8mb4库通信。useSSL=false是因为本地开发环境MySQL默认没配SSL证书,驱动8.x默认尝试SSL连接,如果不关掉会报SSL connection error或者握手警告;生产环境如果真有网络加密需求,再改成useSSL=true并配证书。
serverTimezone=Asia/Shanghai是MySQL 8.0驱动的强制要求项。MySQL服务器端time_zone如果设置成SYSTEM,而运行Java的机器时区不是UTC,驱动读取日期时间时会报The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,乱码一样的时区名。直接指定Asia/Shanghai最省事。allowPublicKeyRetrieval=true是MySQL 8.0使用caching_sha2_password插件时,客户端首次连接需要从服务器获取公钥,如果不允许,会报Public Key Retrieval is not allowed。本地开发直接放开,生产环境建议改用mysql_native_password或配置SSL后再去掉这个参数。
3. 用JDBC手写一套CRUD:从Connection到PreparedStatement的完整路径
3.1 写一个连接工具类:把连接参数收敛到一个方法里
有了库表结构,开始写Java。最忌讳的做法是在每个业务方法里重复写Class.forName("com.mysql.cj.jdbc.Driver")和DriverManager.getConnection(...),因为一旦连接参数要改,你得全局替换。我一般先写一个DBUtil,用静态代码块加载驱动,把连接参数放在常量里。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,检查依赖"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }Class.forName在MySQL 8.x驱动里其实可以省略,因为JDBC 4.0以后驱动通过META-INF/services自动注册。但保留它有一个好处:如果依赖没导对,能明确抛出ClassNotFoundException,而不是在getConnection时才报“No suitable driver found”。注意驱动类名,MySQL 5.x 是com.mysql.jdbc.Driver,8.x 改成com.mysql.cj.jdbc.Driver,写错一个字母就翻车。
这个工具类的另一个重点是静态代码块抛ExceptionInInitializerError。因为ClassNotFoundException是受检异常,静态块里不能直接抛给调用方,包一层运行时异常是常见做法。调用方只需要DBUtil.getConnection(),然后在finally里关连接。
3.2 新增学生:用PreparedStatement而不是Statement,三个原因
插入一条学生记录是最常见的操作。新手容易写成Statement拼接SQL,比如:
Statement stmt = conn.createStatement(); stmt.executeUpdate("INSERT INTO tb_student(student_no, name, gender, age, class_id) VALUES('" + no + "', '" + name + "', '" + gender + "', " + age + ", " + classId + ")");这段代码在课设里能跑,但存在SQL注入、引号转义、类型拼接错误三个隐患。如果name是O'Brien这样的值,SQL直接语法错误;如果name是' OR '1'='1,后果不必多说。正确做法是用PreparedStatement:
public int addStudent(Student s) { String sql = "INSERT INTO tb_student(student_no, name, gender, age, class_id, phone) " + "VALUES(?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setInt(5, s.getClassId()); ps.setString(6, s.getPhone()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } }try-with-resources语法(Java 7+)会自动关闭Connection和PreparedStatement,不需要手写finally。注意关闭顺序:驱动文档要求先关ResultSet、再关Statement、最后关Connection,try-with-resources按声明逆序关闭,正好满足。如果把Connection放在最前面声明,PreparedStatement在后面,关闭时会先关ps再关conn,没问题;反过来如果把Connection放最后,就会先关连接再关语句,可能抛异常。
参数索引从1开始,不是0,写错下标会报Parameter index out of range。setString和setInt的调用顺序不必和SQL里的问号数字完全一致,只要?的位置能对上,代码顺序可以灵活,但为了可读性,保持一致。
3.3 查询学生列表:ResultSet的 next() 和 getXxx() 边界
查询是比插入更容易出错的环节。很多初学者用while (rs.next())读取数据,但不是所有场景都应该用while。如果查询结果是按学号唯一取一条,应该用if (rs.next()),否则误用while时会多读一个空行。更关键的是getXxx的列索引或列名。
public List<Student> findStudentsByClass(int classId) { String sql = "SELECT student_id, student_no, name, gender, age, phone " + "FROM tb_student WHERE class_id = ? ORDER BY student_id"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, classId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setStudentId(rs.getInt("student_id")); s.setStudentNo(rs.getString("student_no")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setPhone(rs.getString("phone")); list.add(s); } } } catch (SQLException e) { e.printStackTrace(); } return list; }这段代码里,ResultSet也放进try-with-resources,避免忘记关闭。getString("gender")能直接返回ENUM的字符串值,这是MySQL驱动帮我们做的映射,不用getObject再转型。另外一个潜在坑:age字段在数据库是TINYINT UNSIGNED,范围0~255,Java端用getInt接没有问题,但如果用getByte接,超过127会溢出,这在自己的库里不容易触发,一旦有留学生或教职工录入,就可能变成负数。所以能用int就用int,别为了省内存用byte。
还有一个容易忽略的点:查询语句里的ORDER BY student_id。如果不写排序,MySQL返回结果的顺序不保证稳定,同样数据在不同时间查出来可能不同,分页时就会出现“上一页最后一条跑到下一页”的怪象。加一个显式排序,是分页功能能稳定的前提。
3.4 更新与删除:executeUpdate的返回值就是校验依据
更新和删除的写法与插入类似,但返回值int代表受影响行数。这是判断操作是否成功的官方依据,而不要用“有没有抛异常”来判断。
public boolean updateStudentPhone(int studentId, String newPhone) { String sql = "UPDATE tb_student SET phone = ? WHERE student_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, newPhone); ps.setInt(2, studentId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public boolean deleteStudent(int studentId) { String sql = "DELETE FROM tb_student WHERE student_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }删除学生时,如果成绩表里有外键引用,DELETE会抛Cannot delete or update a parent row: a foreign key constraint fails。这是数据库保护你的数据,不是程序bug。处理方式有两种:业务上确认该学生没有成绩记录再删,或者把删除改成逻辑删除——加一个is_deleted TINYINT字段,查询时过滤is_deleted = 0。课程设计阶段用物理删除问题不大,但如果在真实系统里,我强烈建议用逻辑删除,因为学生可能选过课、交过费、有奖惩记录,物理删除会让历史数据全部断裂。
4. 常见问题排查:驱动、时区、中文乱码与连接泄漏的5个踩坑记录
4.1 现象:java.sql.SQLException: No suitable driver found,原因:驱动依赖没进运行环境
新手最常见的问题,代码写了Class.forName("com.mysql.cj.jdbc.Driver")却没报错,但一跑到getConnection就报 “No suitable driver found”。排查顺序是:先看Maven/Gradle依赖里是否真有mysql-connector-j,注意8.x的artifact名不再是mysql-connector-java,而是com.mysql:mysql-connector-j;再看运行配置的classpath是否包含jar包。用IDEA跑时,Maven依赖是自动引入,但如果你用java -jar打包,没有把依赖打进Fat Jar,就会缺。
解决:不要自己下载jar包放进lib目录,直接用构建工具管理。如果是Maven,加上:
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.4.0</version> </dependency>版本号以你实际用的为准,但注意MySQL Connector/J 8.x对应MySQL 5.7和8.x都能连。连MySQL 5.7时,URL里也要加serverTimezone,一样的原因,驱动时区解析逻辑是统一的。
4.2 现象:插入的中文变成问号,原因:客户端连接字符集和服务器不一致
明明数据库建表用了utf8mb4,Java字符串也是中文,存进去却变成???。这是连接层面的字符集没对齐。在MySQL命令行里查SHOW VARIABLES LIKE 'character_set_client',如果看到latin1,说明服务器默认客户端字符集是拉丁,而你的JDBC URL里characterEncoding=utf8只在连接建立时设了客户端字符集,但可能被后端参数覆盖。
解决:在my.cnf或my.ini的[mysqld]段下加配置:
character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci同时保证JDBC URL里characterEncoding=utf8(utf8mb4也接受)。改完重启MySQL,再确认character_set_client变成utf8mb4。另外,如果SQL文件本身不是UTF-8编码,用命令行导入时加--default-character-set=utf8mb4,否则表结构和数据在源头就乱。
4.3 现象:Public Key Retrieval is not allowed,原因:MySQL 8.0默认认证插件机制
我们已经在前文URL里加了allowPublicKeyRetrieval=true。如果去掉这个参数,MySQL 8.0的用户以caching_sha2_password方式认证,客户端第一次连时没有缓存公钥,又没启用SSL,服务端不能明文传公钥,就抛这个错。
解决:本地开发直接加allowPublicKeyRetrieval=true;生产环境优先配置SSL证书,或者把用户的认证插件改成mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';但注意,mysql_native_password在MySQL 8.4里已被标记为废弃,未来版本可能移除,所以治本方案是配SSL而不是换插件。课设阶段不用纠结,本地加参数就好。
4.4 现象:程序跑一段时间后变慢或报Too many connections,原因:连接泄漏
这个坑我在接手一个老课设时踩过。学生管理系统打开页面越来越慢,最后报Data source rejected establishment of connection, message from server: "Too many connections"。一看代码,Connection是在方法里getConnection()后没有关闭,或者只有ps.close()忘了关conn。MySQL默认最大连接数151,每个页面打开一次连不上就积累。
解决:把所有JDBC操作都改成try-with-resources或用finally关连接。这是一个习惯问题,不是技术难点。另外,如果是Web应用,别再每次请求都新建连接,引入连接池,比如HikariCP,它能把连接复用起来,并配置最大连接数上限:
dataSource.maximumPoolSize=20 dataSource.connectionTimeout=300004.5 现象:查询结果时好时坏,报The database returned no natively generated identity value,原因:主键生成策略与数据库自增不匹配
如果用了JPA/MyBatis Plus这类框架,并在Java实体类里配了@GeneratedValue(strategy = GenerationType.IDENTITY),但实际表的主键不是AUTO_INCREMENT,插入后想获取自增主键就会报这个错。在纯JDBC里,我们如果想拿到自增主键,需要额外指定。
解决:在prepareStatement时传入Statement.RETURN_GENERATED_KEYS:
String sql = "INSERT INTO tb_student(...) VALUES(...)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, ...); ps.executeUpdate(); try (ResultSet rs = ps.getGeneratedKeys()) { if (rs.next()) { int newId = rs.getInt(1); } } }这个写法适合“插入学生后立即使用他的ID去插成绩”的场景。如果不传这个参数,getGeneratedKeys()返回空结果集,ID就拿不到。
5. 登录校验与分页查询:从控制台到Web层都绕不开的四个实现细节
5.1 登录会话管理:用HttpSession还是自己写Token?先看你有没有Web容器
如果这个学生信息管理系统是带Servlet的Web项目,登录状态用HttpSession是最常见的做法。用户登录成功后,把学生ID放进Session,后续请求通过过滤器检查session是否存在。如果是不带Servlet的纯控制台程序,则要自己用Map<String, UserSession>管理。我先说Web方式。
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 这里应该调用UserDAO校验,而不是在Servlet里写SQL User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect("index.jsp"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }密码校验有个绝对底线:不要把明文密码直接比对。至少用MD5加盐或BCrypt。MD5本身已被破解碰撞,但课设里用MessageDigest做个加盐MD5比明文强很多。生产级建议用BCryptPasswordEncoder(Spring Security里的类)。另外一个容易忽略的问题是Session超时配置。
<session-config> <session-timeout>30</session-timeout> </session-config>这里单位是分钟,30分钟内用户无操作,Session失效。太短影响体验,太长增加服务端内存压力,按内部系统的开放时间调。
5.2 分页查询:LIMIT ? OFFSET ? 的边界计算与总页数
学生列表几十条是小事,但一旦上千条,全量查询不仅慢,页面显示也难。分页查询的核心参数是page和pageSize,SQL用LIMIT加OFFSET。
public List<Student> findStudentsByPage(int page, int pageSize) { int offset = (page - 1) * pageSize; String sql = "SELECT student_id, student_no, name, gender, age, phone " + "FROM tb_student ORDER BY student_id LIMIT ? OFFSET ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, offset); try (ResultSet rs = ps.executeQuery()) { // 封装成List } } }注意LIMIT ? OFFSET ?在MySQL支持用占位符,但如果数据库是SQL Server,语法不同,那种情况得用OFFSET ... ROWS FETCH NEXT ... ROWS ONLY。这里锁定MySQL,所以写法如上。
分页查询要同时返回总条数,才能计算总页数。常见做法是另外查一次SELECT COUNT(*)。如果数据量超过几万条,COUNT也会慢,可以先走缓存,但学生系统没这个必要。
int total = countStudents(); // SELECT COUNT(*) FROM tb_student int totalPages = (total + pageSize - 1) / pageSize;(total + pageSize - 1) / pageSize是向上取整的经典写法。例如total=10,pageSize=3,(10+2)/3=4,正好3页。不要用Math.ceil(total / pageSize),因为整数除法会先取整再ceil,产生错误结果。
同时要注意page越界的问题。用户手输page=999,查询结果为空,页面应跳转到最后一页。后端要把page限制在[1, totalPages]范围内:
if (page < 1) page = 1; if (page > totalPages) page = totalPages;这里最容易出的是totalPages=0的情况,没有学生时totalPages为0,page > 0永远成立,所以至少要保证if (totalPages == 0) totalPages = 1;或者前端不显示分页条。
5.3 按姓名模糊搜索:LIKE的占位符拼接是注入重灾区
搜索学生的功能,SQL大致是:
SELECT * FROM tb_student WHERE name LIKE ? ORDER BY student_idJava里设置参数时需要拼接通配符:
ps.setString(1, "%" + keyword + "%");很多教程会把通配符直接写进SQL:
String sql = "SELECT * FROM tb_student WHERE name LIKE '%" + keyword + "%'";这种写法是SQL注入训练的典型教材。PreparedStatement的占位符只保护参数值本身,LIKE的%符号在参数值里是安全的,因为%和_在MySQL LIKE中是通配符,但不会被当成SQL语法解析。keyword里如果包含%,那么"%" + keyword + "%"会把用户输入的%也当作通配符,导致查出意外数据。更稳妥的做法是转义:
String escaped = keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"); String pattern = "%" + escaped + "%";这个细节课设里不要求,但如果你的系统面向真实用户,搜索框里输入%会让人困惑——搜“李%”把“李小明”搜出来。加上转义,行为才符合预期。
另外,模糊搜索字段如果是中文姓名,数据库排序规则是utf8mb4_unicode_ci,它不区分大小写,但中文是按拼音排序吗?不一定,MySQL默认的utf8mb4排序并不完全按拼音,可能按Unicode编码。如果业务上要求按拼音排序,就得用CONVERT(name USING gbk)或单独的拼音列。学生信息管理系统一般不要求,但你要知道这个边界。
6. 收尾优化:连接池参数、预编译批量写和一行时间统计的实战习惯
系统功能跑通只是开始,真正能让它在课设答辩或实习评审里加分的,往往是几个不起眼的工程习惯。
第一个习惯是别再用DriverManager.getConnection,换成HikariCP连接池。HikariCP是目前Java生态里性能最优的连接池之一,Spring Boot 2.x默认就是它。即使不引入Spring,单独用也行。核心配置就几个参数:
spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000maximum-pool-size不是越大越好。每个连接都会占用MySQL服务端线程和内存,20个连接配50人的内部系统绰绰有余。如果设成200,反而可能因MySQLmax_connections限制而连接失败。connection-timeout是客户端等待连接的秒数,30秒内如果池中没有空闲连接且创建不了新连接,就抛超时;不要设成几小时,那会让故障发现变得延迟。
第二个习惯是批量插入。如果管理员导入一个包含500名新生的Excel,用单条INSERT循环500次,每次都提交一次事务,那会慢到让人怀疑人生。改成批量提交:
String sql = "INSERT INTO tb_student(student_no, name, gender, age, class_id) VALUES(?, ?, ?, ?, ?)"; Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement ps = conn.prepareStatement(sql); for (Student s : studentList) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setInt(5, s.getClassId()); ps.addBatch(); } ps.executeBatch(); conn.commit();addBatch把SQL参数累积在客户端,executeBatch一次发给服务端执行。这里有两个参数学问:rewriteBatchedStatements=true要加在JDBC URL里,否则MySQL驱动默认一条条发送,性能提升有限;setAutoCommit(false)保证所有插入要么全部成功,要么回滚,避免导入一半时某条数据违反唯一约束导致前面数据已提交的惨状。事务开启后,记得在catch里conn.rollback(),否则连接池里的连接带着未提交事务返回池中,下一次复用可能出现脏读或锁等待。
第三个习惯是给每个DAO方法加上耗时统计,在答辩时展示性能数据比喊“优化过”有力得多。别用System.currentTimeMillis()手动打点,用StopWatch(Spring的或Apache Commons的)或者最土的方式:
long start = System.currentTimeMillis(); // 执行查询 long cost = System.currentTimeMillis() - start; System.out.println("findStudentsByPage cost " + cost + " ms");这种输出在日志里是噪音,但课设阶段足够定位哪条SQL慢。更专业的是用MySQL慢查询日志定位:
slow_query_log=ON long_query_time=1设置后超过1秒的SQL会记录到日志里,回头看哪个表缺索引。学生系统通常卡在模糊搜索和分页排序,试试给name建普通索引:
ALTER TABLE tb_student ADD INDEX idx_name(name);如果搜索WHERE name LIKE '张%'(前缀匹配),索引生效;如果LIKE '%张%'(中间匹配),索引基本失效,全表扫描是常态。这是MySQL B+树索引的边界,不是优化能解决的。
实践中最怕的不是功能缺失,而是你拿着一个不可复现的“玄学bug”到处问人。上面这些习惯——统一连接入口、强制关闭资源、显式排序、限制分页边界、批量提交——能帮你把90%的运行时异常消灭在代码评审阶段。我当年做这个系统时,就因为忘了关Connection,在演示前一晚被Too many connections折腾到凌晨。后来把每个方法都检查了一遍try-with-resources,从此再没遇过这种故障。希望帮到你——照这条路径走,踩过的坑我替你填了。
本文还有配套的精品资源,点击获取