1. 先用一句话说清:Java连MySQL到底在连什么
不少刚接触Java后端的朋友,第一次看到"连接数据库"这几个字的时候,脑子里冒出来的画面往往是:Java代码里写个什么神秘的地址,然后"叮"一声,数据就自动流过来了。真做起来却完全不是这么回事——写完了跑起来,报错信息五花八门,有的说驱动找不到,有的说连接超时,有的说密码错误,折腾半天一个数据都查不出来,那种挫败感我太熟悉了。
实际上,Java连接MySQL并实现数据交互,本质就是三件事:加载驱动、建立连接、通过语句对象交换数据。驱动是MySQL官方提供的一层"翻译器",负责把Java的数据库操作请求转换成MySQL能理解的协议;连接是Java程序和MySQL进程之间的一条网络通道,通信双方约好地址、端口、账号密码;数据交互则是通过JDBC里的Statement或PreparedStatement把SQL发过去,再把结果集ResultSet取回来。
拆开看,这套流程并不复杂,核心调用链正常情况下一只手数得过来。但为什么那么多人在这一步卡壳?因为中间涉及的环境细节太多了:驱动版本和MySQL服务端版本是否兼容、JDBC URL参数有没有写对、时区参数配置是否正确、数据库账号权限够不够、甚至驱动的jar包到底放没放进classpath,都会导致完全不同的报错。这篇文章我就按照自己实际一步步跑通的经验,把Java连接MySQL并实现数据交互的全过程拆开讲一遍,包括那些我在初学阶段踩过的、至今印象深刻的坑。
适合看这篇内容的读者有两类:一类是刚学完Java语法、正准备接触数据库交互的初学者,另一类是工作中要临时接一个MySQL数据交互模块、急需快速找到正确姿势的开发者。文章会从环境准备讲到完整代码,再讲到常见的坑和排查思路,最后给一些项目级的优化建议。沿用我自己的学习路线——先跑通最基本的一版,再回头理解每个环节为什么这么设计,你会发现数据库交互真不是玄学,每一环都有迹可循。
2. 动手前的准备:驱动下载、URL参数和账号权限,一个都不能错
2.1 驱动的选择:版本匹配比想象中更重要
Java连接MySQL,第一步不是写代码,是拿到一个叫"驱动"的jar包。这个包通常就是这个文件:mysql-connector-j,早期的版本叫mysql-connector-java,现在官方把它更名为com.mysql:mysql-connector-j。如果你是Maven项目,在pom.xml里加依赖就行;如果是普通Java项目,手动下载jar包放进lib目录并添加到classpath。
版本匹配这件事,我一直跟身边人强调:驱动版本最好不低于MySQL服务端的大版本。什么意思?如果你的MySQL是8.0,就尽量用8.x的驱动;如果你的MySQL是5.7,用8.x的驱动通常也没问题(它会兼容),但反过来——MySQL已经是8.0了,你还在用5.1的老驱动,那大概率会报Communications link failure或者干脆连驱动类都加载不起来。
为什么会有这种坑?因为MySQL 8.0之后默认的认证插件变成了caching_sha2_password,而老版本的驱动(5.1.x及更早)只认识mysql_native_password。两边语言不通,连接自然失败。所以选驱动最稳的策略就是:驱动版本跟MySQL大版本对齐,或者干脆用当前最新的8.x驱动,向后兼容性做得很好。
2.2 JDBC URL的连接参数
驱动准备好之后,就要拼JDBC URL。这是连接信息里最核心的一串字符串,格式大概是这样的:
jdbc:mysql://主机地址:端口号/数据库名?参数1=值1&参数2=值2比如:
jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true这一段看起来像是在背课文,其实每个参数背后都有实际原因:
useUnicode=true&characterEncoding=utf8:告诉驱动,我要用UTF-8编码跟数据库通信,防止中文乱码。serverTimezone=Asia/Shanghai:指定服务器时区。如果MySQL服务端没有明确配置时区,你在Java里拿到的Timestamp类型可能会有8小时的偏差,这个后面单独说。useSSL=false:本地开发或者内网环境通常不需要SSL加密连接,加上这个参数避免连接时因为证书问题报错。allowPublicKeyRetrieval=true:这个参数特别坑。MySQL 8.0在使用caching_sha2_password认证方式时,如果连接没有走SSL,客户端需要从服务端获取公钥做密码加密传输,而驱动出于安全考虑默认不允许自动获取公钥,于是就会报Public Key Retrieval is not allowed。加了这个参数,才能正常连上。
2.3 数据库账号权限的坑
还有一种情况,代码、驱动、URL全都没问题,数据库用户却连不上——不是密码错,是权限不够。
我记得刚学的时候直接用root账号连接,查到哪写到哪,一切顺利。后来为了规范,创建一个专门的应用账号,结果所有操作都报Access denied for user。排查了一圈才发现,MySQL里创建用户和授权是两步:
-- 创建用户 CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'your_password'; -- 授权:只给需要的权限,不要一上来就 GRANT ALL GRANT SELECT, INSERT, UPDATE, DELETE ON test_db.* TO 'app_user'@'localhost';'localhost'是指这个账号只能从本机连接。如果你要远程连接,得换成对应IP或者用'%'表示允许所有来源,但生产环境强烈不建议这么干,安全隐患太大。
还要注意一个细节:改完权限记得FLUSH PRIVILEGES。在非交互式命令行或者某些特殊场景下,不刷新授权表,新权限不会立刻生效。实际项目里我见过好几次因为这个操作漏掉,导致应用一直报权限错误的案例。
3. 完整跑通一次数据交互:从建表到增删改查
3.1 六步走:一次查询背后的完整调用链
环境准备就绪后,就可以动手写代码了。JDBC操作数据库的完整调用链路,我习惯把它归纳成六步:
- 加载驱动类:
Class.forName("com.mysql.cj.jdbc.Driver")。 - 获取连接:
DriverManager.getConnection(url, username, password)。 - 创建语句对象:
conn.prepareStatement(sql)。 - 执行SQL:
executeQuery()用于查询,executeUpdate()用于增删改。 - 处理结果:查询得到
ResultSet,遍历读取;增删改得到受影响行数。 - 关闭资源:按"后开先关"的顺序,依次关闭
ResultSet、Statement、Connection。
很多教程会告诉你"第一步是注册驱动",但如果你用的驱动是4.0及以上版本,这步其实可以省略——因为驱动jar包里的META-INF/services/java.sql.Driver文件已经注册好了,DriverManager会自动加载。不过写上也没坏处,主要作用是让代码的意图更明确,遇到一些老旧的类加载环境时也更稳妥。
连接对象Connection是整个环节里最"重"的资源。建立一条MySQL连接,底层要做TCP握手、认证、初始化会话,整套流程下来通常要几十甚至上百毫秒。所以代码里一定不要频繁创建和销毁连接,这个在后面的连接池部分我会展开。
3.2 PreparedStatement为什么是首选
这是我想单独拎出来讲的一点,因为它对初学者来说最容易忽略,但影响又最大。
JDBC里执行SQL有两种常用语句对象:Statement和PreparedStatement。我用一个最简单的例子对比一下:
// Statement:字符串拼接SQL String name = "zhangsan"; Statement stmt = conn.createStatement(); String sql = "SELECT * FROM user WHERE username = '" + name + "'"; ResultSet rs = stmt.executeQuery(sql); // PreparedStatement:占位符+参数绑定 PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE username = ?"); ps.setString(1, name); ResultSet rs2 = ps.executeQuery();表面看,差别只是拼接方式不同。但实际用起来,PreparedStatement有两个压倒性优势:
第一,防SQL注入。字符串拼接SQL的话,如果传入的name是' OR '1'='1这种内容,拼接出来的SQL就变成了SELECT * FROM user WHERE username = '' OR '1'='1',直接把整张表都查出来。而PreparedStatement用占位符绑定参数,驱动会把参数内容当作纯数据来处理,而不是当作SQL语法的一部分,从根本上堵住了注入的口子。
第二,预编译带来的性能收益。同一个SQL模板只要发送一次,后续再执行只是替换参数值,数据库端不需要重新解析和生成执行计划。执行频率越高,性能优势越明显。
所以我的建议是:不管项目大小,一律用PreparedStatement。写多了你会发现,那些需要动态拼接的表名、排序字段等场景才需要特殊处理,而普通的增删改查用PreparedStatement完全够用,还更安全。
3.3 动手写一个增删改查的Demo
纸上谈兵不如跑一遍,我直接给一个完整的增删改查Demo,覆盖最常见的操作。假设有一张用户表:
CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `email` varchar(100) DEFAULT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;完整的Java代码如下:
import java.sql.*; public class JdbcDemo { private static final String URL = "jdbc:mysql://localhost:3306/test_db" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false&allowPublicKeyRetrieval=true"; private static final String USER = "app_user"; private static final String PASSWORD = "your_password"; public static void main(String[] args) { // 1. 插入一条记录 insertUser("zhangsan", "zhangsan@example.com"); // 2. 查询所有用户 queryAllUsers(); // 3. 更新邮箱 updateUserEmail("zhangsan", "new_email@example.com"); // 4. 删除用户 deleteUserByUsername("zhangsan"); } public static void insertUser(String username, String email) { String sql = "INSERT INTO user (username, email) VALUES (?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, email); int rows = ps.executeUpdate(); System.out.println("插入影响行数: " + rows); } catch (SQLException e) { e.printStackTrace(); } } public static void queryAllUsers() { String sql = "SELECT id, username, email, created_at FROM user"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { long id = rs.getLong("id"); String username = rs.getString("username"); String email = rs.getString("email"); Timestamp createdAt = rs.getTimestamp("created_at"); System.out.printf("id=%d, username=%s, email=%s, created_at=%s%n", id, username, email, createdAt); } } catch (SQLException e) { e.printStackTrace(); } } public static void updateUserEmail(String username, String newEmail) { String sql = "UPDATE user SET email = ? WHERE username = ?"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, newEmail); ps.setString(2, username); int rows = ps.executeUpdate(); System.out.println("更新影响行数: " + rows); } catch (SQLException e) { e.printStackTrace(); } } public static void deleteUserByUsername(String username) { String sql = "DELETE FROM user WHERE username = ?"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); int rows = ps.executeUpdate(); System.out.println("删除影响行数: " + rows); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里用到了try-with-resources语法,Java 7以后推荐这么写,因为它会自动帮你关闭实现AutoCloseable接口的资源,省去手动在finally里繁琐的判断和关闭。
3.4 关闭资源的小细节
说到关闭资源,这是初学阶段最容易偷懒、也最容易出问题的地方。我见过很多同学只关Connection,不关Statement和ResultSet,理由是"连接都关了,其他的不也跟着释放了吗"。
理论上,关闭Connection确实会释放关联的Statement和ResultSet。但问题在于,如果你在同一个连接上执行多次SQL,用的是同一个Statement,上一次的ResultSet没关闭,下一次执行时旧结果集可能被自动关闭,也可能发生资源泄漏,这取决于驱动的具体实现。更稳妥的做法是谁打开谁关闭,按反方向逐个关闭:先ResultSet,再PreparedStatement,最后Connection。
try-with-resources之所以好用,就是因为它帮我们把这种别扭的资源管理逻辑封装成了固定的语法结构。多个资源的关闭顺序也是自动按照声明顺序的反向执行,正好符合我们说的"后开先关"原则。
4. 初学者最容易踩的五个坑,以及我应该怎么排查
4.1 驱动类找不到 vs 连接被拒绝:两个错误,两种解法
初学阶段最常见的两类报错,我用两个真实的错误信息来区分:
第一类:
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver这个错只说明一件事:JVM在classpath里找不到驱动类。通常的解法就两个:检查jar包是否真的下载完整了;检查IDE里是否把jar包添加到了项目的库中。如果是Maven项目,检查依赖坐标有没有写对,写完之后有没有刷新让依赖真正下载下来。
第二类:
java.sql.SQLException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.这个报错看起来像"连接被拒绝",但其实它只是一个笼统的网络层错误。可能的原因包括:MySQL服务没启动、端口写错了、防火墙拦了、或者驱动和服务端版本不匹配。排查的时候我建议按这个顺序来:
- 先确认MySQL服务在跑:命令行执行
mysqladmin -u root -p status,能返回信息就说明服务正常。 - 再确认端口:默认是3306,如果你启动MySQL时改了端口,URL里必须同步修改。
- 然后确认账号权限:见第二节提到的授权方式。
- 最后确认驱动版本:MySQL 8.0环境用旧驱动,也可能报这个错。
4.2 时区参数引发的八小时误差
这个坑我踩得记忆犹新。有一次业务那边反馈,数据库里存的created_at时间是对的,但Java程序查出来打印,发现比数据库里多了8个小时。研究了一圈,问题就出在时区参数的缺失上。
MySQL JDBC驱动在连接时,如果URL里没有指定serverTimezone,而MySQL服务端的time_zone变量是SYSTEM,驱动就会拿Java虚拟机所在的系统时区和数据库时区做换算。如果两边时区不一致,就会出现时间偏差。国内一般是Asia/Shanghai,也就是UTC+8,如果服务端默认用了UTC,查询结果自然就差8小时。
解决办法很简单,URL里明确指定:
serverTimezone=Asia/Shanghai如果数据库存储本身就是UTC时间,想避免时区换算干扰,用serverTimezone=UTC。总之,这面的时区最好显式指定,别把决定权交给驱动去猜。
4.3 ResultSet的指针问题
这个坑不算报错,但会让结果"看起来很奇怪"。
ResultSet内部有一个游标(指针),初始位置在第一行数据之前,而不是第一行。你必须调用一次next(),游标才会移动到第一行;再次调用next(),才走到第二行;当next()返回false,说明已经遍历完了。
所以正确写法是:
while (rs.next()) { // 在这里读取当前行的数据 String username = rs.getString("username"); }常见的错误是把取值写在while循环外面,或者忘了先调用next()就getString,结果直接报java.sql.SQLException: No data found。我见过不少人在这一步迷茫,其实记住一句话就行:拿到ResultSet之后,第一件事永远是判断并移动游标,然后才能读数据。
4.4 连接数耗尽:最隐蔽的坑
前面提到连接是很"重"的资源,这里就有一个隐藏的连锁反应:如果代码里每一次数据库操作都新建连接、用完不关,在高并发场景下,数据库的连接数会迅速被打满,之后所有新请求都会阻塞甚至报Too many connections。
我自己早期的一个项目就出过这问题:测试环境一切正常,一到线上流量稍微上来,应用就频繁报连接超时。查了半天,发现是某段代码在循环里反复getConnection,但连接没有正确释放。虽然单次泄漏看不出影响,但累积到一定量,就是雪崩式的故障。
这种问题的修复思路分两步:短期是排查代码,确保所有连接都在finally或try-with-resources里关闭;长期是引入连接池,统一管理连接的生命周期,避免频繁创建销毁。连接池这块我留在第5章展开。
4.5 中文乱码:字符集要两端对齐
中文乱码的本质是编码和解码用了不同的字符集。一条数据从Java字符串变成SQL语句发送给MySQL,再存进表里,中间经过好几个环节,任何一个环节的字符集不对齐,都会出现乱码。
常见的排查点有四个:
- Java代码文件的编码:IDE里统一设置为UTF-8。
- JDBC URL里的
characterEncoding:指定为utf8(MySQL里的utf8mb4才是完整的UTF-8,比utf8多支持emoji等四字节字符)。 - MySQL服务端的默认字符集:查看
SHOW VARIABLES LIKE 'character_set_server';,强烈建议配置成utf8mb4。 - 表的字符集:建表时显式指定
CHARSET=utf8mb4。
一个项目里这四个环节只要有一个是latin1或者默认的gbk,就可能出问题。排查的时候,先用SHOW CREATE TABLE user;确认表字符集,再检查URL参数,最后看有没有可能报文层面被转码。
5. 从跑通到像样:连接池、事务和简单封装
5.1 再也不要手动关连接:连接池的作用
Demo跑通之后,如果直接把这个方式用到线上,很快就会发现两个问题:一是频繁创建连接导致响应慢,二是并发上来后连接数失控。
连接池就是来解决这两个问题的。一个典型的连接池会提前创建一批连接放在池子里,应用程序要连接时,直接从池子里"借"一个,用完再"还"回去,而不是真正关闭。这样连接的创建成本被摊薄了,连接数量也被上限控制住了。
Java生态里常用的连接池有HikariCP、Druid、C3P0等。我个人的建议是:新项目首选HikariCP,它的性能在同类里表现非常突出,而且配置简单。国内很多项目用Druid,因为它自带监控页面,可以直观看到SQL执行情况和连接池状态,适合需要监控的场景。
用HikariCP整合我们前面的Demo,其实改动很小——只需要把DriverManager.getConnection(url, user, password)替换成从DataSource里拿连接:
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DataSourceHolder { private static final HikariDataSource DATA_SOURCE; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/test_db" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false&allowPublicKeyRetrieval=true"); config.setUsername("app_user"); config.setPassword("your_password"); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); DATA_SOURCE = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DATA_SOURCE.getConnection(); } }之后业务代码里只需要Connection conn = DataSourceHolder.getConnection();,用完还是按老规矩调用close()——这时的close()实际上是把连接归还给池子,而不是物理关闭。
几个池子参数给新手一点参考:
maximumPoolSize:池中最大连接数,不是越大越好。每一条连接都是数据库端的一个线程/进程,开太多反而拖慢整体性能。一般按CPU核心数 * 2 + 磁盘数的经验公式起步,再根据压测调整。minimumIdle:池中保持的最小空闲连接数。设置太小,请求一上来要临时创建新连接,会有短时延迟。connectionTimeout:从池子里借连接的最大等待时间,超过就报超时。这个时间设得太短容易误伤慢查询,设得太长会让请求长时间挂起。
5.2 事务的基本使用方式
多个SQL操作要"要么全成功,要么全失败"时,就需要事务。JDBC里的事务控制非常直白:
Connection conn = null; try { conn = DataSourceHolder.getConnection(); // 关闭自动提交 conn.setAutoCommit(false); // 执行业务操作 updateA(conn); // 某个更新操作 updateB(conn); // 另一个更新操作 // 全部成功才提交 conn.commit(); } catch (SQLException e) { if (conn != null) { // 出错回滚 conn.rollback(); } e.printStackTrace(); } finally { if (conn != null) { conn.setAutoCommit(true); // 恢复自动提交,方便连接复用 conn.close(); } }事务里最容易忽略的一个点是:连接从连接池借出来的时候,状态可能不是"全新"的。如果上一个使用方把autoCommit改成了false却没有恢复,下一个用这条连接的人就会莫名其妙地不自动提交了。所以归还连接前,把setAutoCommit(true)恢复成默认值,是个非常好的习惯。
另一个常见问题是:事务中某一步查询只是为了读取数据,但为了"保证数据一致",把整条链路都包在事务里,还用了SELECT ... FOR UPDATE行锁,结果并发一上来就死锁。事务不是随便开的,应该遵循"短事务、快提交"的原则,锁的范围越小越好。
5.3 简单封装:把重复代码收进工具类
写到这里你肯定也发现了,每次数据库操作都有大量重复代码:拿连接、建PreparedStatement、执行、关资源。如果每个业务方法都这么写,代码冗余会非常严重。
我早期的做法是抽一个简单的工具类,把"执行增删改"和"执行查询"这两种操作统一封装起来。比如:
public class JdbcUtils { public static int executeUpdate(String sql, Object... params) { try (Connection conn = DataSourceHolder.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { bindParams(ps, params); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("执行更新失败", e); } } public static <T> List<T> executeQuery(String sql, RowMapper<T> mapper, Object... params) { List<T> result = new ArrayList<>(); try (Connection conn = DataSourceHolder.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { bindParams(ps, params); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { result.add(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException("执行查询失败", e); } return result; } private static void bindParams(PreparedStatement ps, Object... params) throws SQLException { if (params != null) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } } } @FunctionalInterface public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; } }这样业务代码就清爽多了:
List<User> users = JdbcUtils.executeQuery( "SELECT id, username, email FROM user WHERE username LIKE ?", rs -> new User(rs.getLong("id"), rs.getString("username"), rs.getString("email")), "%" + keyword + "%" );要注意,我这里用了ps.setObject(i + 1, params[i])做通用参数绑定。虽然大多数情况下能工作,但遇到需要精确指定JDBC类型的场景(比如传入null、日期类型、BigDecimal),setObject的自动推断不一定准确,需要时再结合具体的setXxx方法做重载处理。真正复杂的企业项目,通常直接用MyBatis、Hibernate这类ORM框架,但理解JDBC这一层封装,对理解ORM的底层原理帮助极大。
5.4 分页与批量插入的初步优化
最后再补两个在实际开发中很容易遇到的操作场景。
分页查询。MySQL的分页用LIMIT ? OFFSET ?实现,配合PreparedStatement:
String sql = "SELECT id, username, email FROM user ORDER BY id LIMIT ? OFFSET ?"; ps.setInt(1, pageSize); ps.setInt(2, (pageNum - 1) * pageSize);分页最需要注意的坑是:OFFSET越大,查询越慢。因为数据库需要扫描并跳过前面那些行才能拿到目标数据。数据量小无所谓,数据量大了之后,一般的姿势是用"游标"或者"书签"代替深分页——即根据上次查询返回的最后一条记录的id往下翻:
SELECT id, username, email FROM user WHERE id > ? ORDER BY id LIMIT ?这比OFFSET高效得多,前提是你有稳定的排序字段(比如自增主键)。
批量插入。逐条插入一万条数据,如果走普通循环,每次都是独立事务、独立网络往返,慢得离谱。JDBC里可以用批量提交优化:
String sql = "INSERT INTO user (username, email) VALUES (?, ?)"; try (Connection conn = DataSourceHolder.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (int i = 0; i < 10000; i++) { ps.setString(1, "user" + i); ps.setString(2, "user" + i + "@example.com"); ps.addBatch(); if (i % 500 == 0) { ps.executeBatch(); } } ps.executeBatch(); conn.commit(); }addBatch()把多条SQL攒在一起,executeBatch()统一发送给数据库执行,配合手动设置autoCommit=false(让所有插入在同一个事务里完成),插入性能通常能提升一个数量级以上。当然也有人把SQL直接拼成一条超长的INSERT INTO ... VALUES (…),(…)…,这种方式插入效果也很好,但受SQL语句长度上限和内存限制,addBatch的通用性更好。
在这套链路里,我从最基础的驱动加载讲到连接池、事务和批量操作,覆盖了Java连接MySQL并实现数据交互的完整路径。最后在收尾处说一个我自己坚持了很久的习惯:在任何数据库相关改动之后,第一步先验证"最小可用链路"——建一条测试连接,执行一条最简单的SELECT 1,确认通畅后再去改业务代码。排查数据库问题时,从这条最简查询开始逐层加条件,往往能最快定位到问题到底出在环境、URL、SQL还是自己的代码逻辑上。这种从简到繁的调试方式,帮我省下的时间远比写这篇文章花的时间多。