ARTICLE DETAIL

资讯详情

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

JDBC连接MySQL增删改查全解析:驱动、PreparedStatement、事务与批处理

JDBC连接MySQL增删改查全解析:驱动、PreparedStatement、事务与批处理 简介面向Java初学者的JDBC数据库操作指南围绕如何连接MySQL并完成增删改查展开适合正在学习JavaWeb或希望深入理解数据库编程原理的广大开发者。资源以单个PDF文档打包体积约326KB内容涵盖数据库建表准备、DBUtil连接类的静态初始化、实体类字段映射以及DAO层增删改查的完整代码示例。教程默认读者已装好JDK、Eclipse、MySQL与Navicat基础工具并针对imooc数据库中的Goddess表演示代码特别展示了PreparedStatement参数化SQL防止注入还对比Hibernate、MyBatis等ORM框架的底层封装逻辑。目前已有5619人学习下载说明这份实战教程对入门者有较强的参考价值。读者通过这份资源能快速搭建JDBC开发环境掌握数据库操作的标准分层思路不仅能够完成基本的增删改查编码还能为后续学习企业级开发框架筑牢基础。1. 为什么 JDBC 连接 MySQL 的增删改查值得手写一遍很多后端开发一上来就用 MyBatis 或 Spring Data JPASQL 写在注解里或 XML 里连接池和事务由框架托管日子过得很舒服。但面试题里仍然高频出现“jdbc 连接 mysql 数据库实现增删改查操作”线上排查CommunicationsException、sql injection violation、连接被关闭、大查询撑爆内存时绕来绕去最后还是落到 JDBC 的底层机制上。手写一遍不是为了造轮子而是为了看清驱动加载、连接 URL 参数、PreparedStatement 预编译、事务边界和资源释放这些框架替你省掉的步骤。这篇文章给出一条完整的路径从 MySQL 驱动和连接参数开始写 CRUD、加事务和批处理最后给出一个不依赖 Spring 的模板类。适合正在准备 Java 面试的人也适合在非 Spring 项目里需要手写数据访问层的人。2. JDBC 连接 MySQL 的驱动引入、URL 参数与最小连接代码2.1 驱动包引入mysql-connector-j 与 Class.forName 的真相JDBC 连接 MySQL 的第一步是让驱动类出现在运行时环境里。当前官方驱动坐标已经改名为mysql-connector-j旧的mysql-connector-java仍可用但已不是新代码的推荐选择。用 Maven 管理依赖可以这样写dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency这个依赖会把com.mysql.cj.jdbc.Driver打包进来。JDBC 4.0 之后驱动 jar 包通过META-INF/services/java.sql.Driver完成了自动注册所以现代 JDK 里不写Class.forName也能通过DriverManager.getConnection建立连接。那为什么教程里还经常看到这行代码原因有两个一是老项目、老驱动版本确实需要手动加载二是某些 Web 容器或类加载器隔离环境里自动注册会失效写上一句更稳妥。Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/test; try (Connection conn DriverManager.getConnection(url, root, 123456)) { System.out.println(conn.getCatalog()); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); }这段代码里DriverManager.getConnection会从已注册的 Driver 列表中找到一个能处理jdbc:mysql协议的驱动并返回一个物理连接。conn.getCatalog()只是验证连接可用正常开发中不会这样用。这里有一个容易被忽略的参数url中没有带任何连接参数所以 MySQL 8.0 驱动会按默认行为去认证和处理时区很容易触发后面要说的时区异常。2.2 连接 URL 参数时区、字符编码、SSL 与超时连接串绝对不是只写jdbc:mysql://localhost:3306/test就完了。MySQL 8.0 驱动对时区、认证插件、SSL 行为都比较敏感缺了参数轻则警告重则直接拒绝连接。我通常会在一开始就把下面这些参数带全String url jdbc:mysql://localhost:3306/test ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8mb4 connectTimeout3000 socketTimeout10000 allowPublicKeyRetrievaltrue rewriteBatchedStatementstrue;这些参数各管一件事最重要的几个如下表所示参数示例值作用serverTimezoneAsia/Shanghai指定服务器时区避免驱动无法识别 MySQL 系统时区而抛异常useSSLfalse本地开发和内网场景关闭 SSL 握手节省连接时间并避免警告characterEncodingutf8mb4让中文和 emoji 字符正确传输不要用utf8connectTimeout3000建连超时毫秒数网络不通时快速失败socketTimeout10000读取数据超时毫秒数SQL 卡住时能事后排查allowPublicKeyRetrievaltrue配合caching_sha2_password认证插件使用rewriteBatchedStatementstrue批处理时把多条 INSERT 重写为多值 INSERT大幅提升批量写入性能serverTimezone是新手最常踩的坑。旧驱动不强制要求MySQL 8.0 驱动遇到连接串里没有时区时会尝试读取数据库系统的默认时区如果宿主机是 Linux 且时区不太标准就会抛出类似The server time zone value CST is unrecognized的异常。characterEncodingutf8mb4也是从 8.0 驱动开始更强调的utf8在 MySQL 里是utf8mb3的别名存不了四字节 emoji 和一些生僻字。2.3 连接失败先看异常定位连接 MySQL 失败时的报错信息看起来很长但大多对应几个固定原因可以直接按下表定位异常现象大概率原因Public Key Retrieval is not allowed认证插件是caching_sha2_password连接串少allowPublicKeyRetrievaltrueThe server time zone value ... is unrecognized连接串少serverTimezone参数Communications link failureMySQL 未启动、端口不对、防火墙拦截、目标 IP 不可达Access denied for user rootlocalhost用户名密码错误或该用户没有从当前主机连接的权限Unknown database数据库名写错ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动 jar 没有进入 classpath定位时先看是不是连接串参数问题再看网络端口能不能通最后看 MySQL 用户授权。mysql -h 127.0.0.1 -P 3306 -uroot -p能通就能排除大多数网络问题剩下的基本是 JDBC 参数和驱动版本问题。3. 用 PreparedStatement 实现 MySQL 的增删改查查询、更新与自增主键3.1 为什么优先 PreparedStatementJDBC 里有三种执行 SQL 的方式Statement、PreparedStatement、CallableStatement。做增删改查时绝大多数场景应该选PreparedStatement。它会把 SQL 中的可变部分用?占位符表达参数通过setXxx传入驱动负责把参数转义和类型转换。这样做有实际收益参数内容不会破坏 SQL 语义避免字符串拼接导致的注入问题SQL 串相同但参数不同时驱动可以复用预编译结果代码可读性远好于SELECT * FROM user WHERE name name 。一个常见误用是只把 PreparedStatement 当字符串替身仍然用参数拼接出完整 SQL再调用prepareStatement(sql)这样预编译失去意义注入防线也形同虚设。3.2 查询操作executeQuery 与 ResultSet 遍历查询操作是最常用的入口。下面这段代码查出一张user表里指定用户名的记录String sql SELECT id, name, email FROM user WHERE name ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, 张三); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { int id rs.getInt(id); String name rs.getString(name); String email rs.getString(email); System.out.println(id - name - email); } } }executeQuery()只用于 SELECT返回ResultSet。rs.next()有两个作用第一把游标从当前位置移到下一行第二返回布尔值判断是否还有数据。所以while (rs.next())是遍历结果集的标准写法。rs.getInt(id)里的参数可以是列名也可以是列的索引列索引从 1 开始。用列名比用索引可读性好但如果 SQL 里用了SELECT name AS n要拿别名。try-with-resources在这里同时管理了Connection、PreparedStatement和ResultSet后面章节会专门说资源释放。3.3 增删改executeUpdate 的返回值与自增主键插入、更新、删除都使用executeUpdate()它返回受影响的行数。增删改的核心区别只在 SQL 和执行后的处理String insertSql INSERT INTO user(name, email) VALUES(?, ?); try (PreparedStatement ps conn.prepareStatement( insertSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, 李四); ps.setString(2, lisiexample.com); int rows ps.executeUpdate(); try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { System.out.println(新用户 id keys.getInt(1)); } } }注意prepareStatement的第二个参数Statement.RETURN_GENERATED_KEYS。它让驱动在 insert 执行后把数据库生成的字段值取回来通常联自增主键。如果不加这个参数getGeneratedKeys()拿不到数据。getGeneratedKeys()同样返回一个ResultSet但里面只有一列就是新生成的主键。更新和删除不需要这个机制String updateSql UPDATE user SET email ? WHERE id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setString(1, newexample.com); ps.setInt(2, 1); int rows ps.executeUpdate(); if (rows 0) { System.out.println(没有匹配的记录); } }executeUpdate的返回值对业务判断很有用。更新时返回 0可能是没有匹配的WHERE条件也可能是数据更新前后没有变化。这两个场景在 MySQL 默认行为里都返回 0需要业务自己决定是否区分。3.4 JDBC 核心方法返回值对照写实现的时候先想清楚该用哪个方法可以减少大量无效试错。下表是增删改查中最常用的方法方法适用场景返回值executeQuery()查询ResultSetexecuteUpdate()INSERT / UPDATE / DELETEint受影响行数execute()任意 SQL一般不用boolean表示是否返回结果集addBatch()把当前参数加入批无executeBatch()批量执行int[]每个元素是一条 SQL 影响的行数getGeneratedKeys()获取自增主键ResultSetexecute()很少用到它要额外判断第一个结果是结果集还是更新计数代码可读性差。CRUD 里用前面两个就够。另外MySQL 里executeUpdate也可以执行 DDL比如CREATE TABLE返回 0但业务代码不应该用 JDBC 去跑 DDL。4. JDBC 事务、批处理与隔离级别让增删改查不再是单条 SQL 的拼接4.1 手动事务从 setAutoCommit 到 commit 和 rollbackJDBC 连接默认是自动提交模式即每条 SQL 执行完就立即提交不用写commit。但真实业务里转账、下单这类操作需要多条 SQL 保证原子性必须手动控制事务。手动事务的固定套路是Connection conn null; try { conn DbUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement( UPDATE account SET balance balance - 100 WHERE id 1)) { ps1.executeUpdate(); } try (PreparedStatement ps2 conn.prepareStatement( UPDATE account SET balance balance 100 WHERE id 2)) { ps2.executeUpdate(); } conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } e.printStackTrace(); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } }setAutoCommit(false)之后当前连接上执行的每一条 SQL 都不会立即落库而是等commit()统一提交。如果中途抛异常rollback()会把本连接内所有未提交的改动撤销。这里有几个要点conn不能放在 try-with-resources 里再手动提交因为 try 块结束后连接会先关未提交事务默认回滚所以手动事务里连接要用传统 try-catch-finally 管理PreparedStatement可以放在内层 try 里它们关闭不会影响连接上的事务状态rollback()也不是只能全部回滚可以先Savepoint sp conn.setSavepoint()再conn.rollback(sp)实现部分回滚事务要短尽量不在事务里执行远程调用、文件读写等长耗时操作。4.2 批处理addBatch 与 executeBatch 的组合一万条 INSERT 一条条执行会非常慢网络往返占了大头。JDBC 的批处理可以把多条 SQL 攒起来一次性发给 MySQL。配合连接串里的rewriteBatchedStatementstrue驱动甚至能把多条单行 INSERT 重写为一条多值 INSERT写入性能会有量级提升。String sql INSERT INTO user(name, email) VALUES(?, ?); try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); for (int i 0; i 10000; i) { ps.setString(1, user_ i); ps.setString(2, i test.com); ps.addBatch(); if (i % 500 0) { ps.executeBatch(); ps.clearBatch(); } } int[] result ps.executeBatch(); conn.commit(); } catch (SQLException e) { conn.rollback(); e.printStackTrace(); }addBatch()只是把当前参数存入批缓冲区不真正执行。executeBatch()执行批内所有 SQL返回int[]数组每个元素对应该条 SQL 影响的行数。需要注意三点不是攒到一万条再一次执行内存和事务长度都受不了。按 500 条或 1000 条一批是经验值clearBatch()能清空批缓冲区避免批内 SQL 重复执行批处理与手动事务配合时如果中间某批失败想保留之前批次的话就需要按批提交否则全部回滚。生产环境一般按批提交但也要接受“部分成功”的后果。rewriteBatchedStatements参数对批量 INSERT 非常关键。不加它executeBatch()本质上仍是串行发送每条语句加了它MySQL 驱动会把同一条 SQL 的不同参数合并成INSERT INTO user(name, email) VALUES(?,?),(?,?)...一次网络往返就传过去。4.3 事务隔离级别什么时候用 whatJDBC 允许通过conn.setTransactionIsolation(int level)设置当前连接的事务隔离级别。MySQL 支持四档级别隔离级别脏读不可重复读幻读典型场景TRANSACTION_READ_UNCOMMITTED可能可能可能几乎不用TRANSACTION_READ_COMMITTED阻止可能可能大多数业务场景TRANSACTION_REPEATABLE_READ阻止阻止可能MySQL InnoDB 默认TRANSACTION_SERIALIZABLE阻止阻止阻止数据强一致并发极低MySQL 默认是REPEATABLE_READ这是 InnoDB 的默认值。JDBC 里想设置就写成conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);这个设置只对当前连接生效连接归还给连接池后可能会被重置。使用连接池时最好在获取连接后确认隔离级别或者在连接池配置里指定transactionIsolation。注意副作用隔离级别越高锁竞争和性能损失越明显不要为所有事务统一调成SERIALIZABLE那基本等于把并发线程串行化。5. JDBC 连接管理与资源释放fetchSize、try-with-resources 与异常对照5.1 try-with-resources 的正确姿势与 ResultSet 生命周期JDBC 的三个核心对象都实现了AutoCloseable可以直接用 try-with-resources 管理。正确的层次结构是String sql SELECT id, name FROM user WHERE age ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 18); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理一行 } } }关闭顺序是ResultSet先关再关PreparedStatement最后关Connectiontry-with-resources 会自动按声明顺序的逆序关闭所以这样写没问题。如果ResultSet不需要单独关闭只声明Connection和PreparedStatement也可以因为Statement.close()会连带关闭它的结果集。但显式声明ResultSet可以让“用完之后马上释放游标”这件事更清晰。这里最容易犯的错是把ResultSet传出方法。比如写一个方法执行查询并返回ResultSet// 错误演示连接关闭后结果集不可用 public ResultSet findUser(int id) throws SQLException { Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(SELECT ...); ResultSet rs ps.executeQuery(); return rs; }方法返回后Connection还没有被显式关闭但连接池或调用方很难知道什么时候该关。一旦连接关闭或归还连接池ResultSet关联的游标就失效了。正确做法是把查询逻辑放在同一个 try 块内处理或者把结果转换为 DTO 列表再返回。5.2 fetchSizeMySQL JDBC 查询流式输出的关键参数大结果集查询是 JDBC 里比 CRUD 更考验功底的场景。默认情况下MySQL Connector/J 会尝试把查询结果全部拉到客户端内存里几百万行的表直接SELECT *很危险。想让它流式返回必须正确设置fetchSizeString url jdbc:mysql://localhost:3306/test?useCursorFetchtrue; String sql SELECT id, name FROM user; try (Connection conn DriverManager.getConnection(url, root, 123456); PreparedStatement ps conn.prepareStatement(sql)) { ps.setFetchSize(200); ps.setFetchDirection(ResultSet.FETCH_FORWARD); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 每次从 MySQL 服务端取 200 行 } } }关键在连接 URL 里的useCursorFetchtrue。没有这个参数ps.setFetchSize(200)会被驱动忽略数据仍然一次性拉取。开启后MySQL 服务端会维护游标客户端读取时再分批取数据。这种方式适合全表扫描导出但要注意它会让事务或连接的持有时间变长取数据期间不能释放连接也不能在同一个连接上执行其他 SQL。如果只是普通分页查询更可靠的做法是 JDBC SQL 里加LIMIT ? OFFSET ?而不是依赖流式。5.3 对照表连接与执行阶段的典型异常异常信息原因处理方式No operations allowed after connection closed使用的连接已被关闭或归还连接池后继续借用检查连接池 maxLifetime 配置使用连接前不要持有过长时间java.sql.SQLSyntaxErrorExceptionSQL 语法错误比如表名用了保留字打印完整 SQL在客户端工具里执行验证java.sql.SQLException: sql injection violation请求被安全网关或代理拦截通常是因为 SQL 里有拼接痕迹改写为参数化查询关闭对 SQL 文本的拼接Communication link failure后连接断开网络超时或 MySQLwait_timeout将空闲连接断开连接池开启testWhileIdle适当设置socketTimeoutThe table xxx is full磁盘满了或 MySQL 表大小限制清理数据、扩容、分区表网关报sql injection violation这种错误比较特殊你的代码明明是安全的但网关注册了敏感规则看到 SQL 里有疑似OR 11或注释符号就拦截。排查时先把真实 SQL 打印出来确认是网关误判还是手工拼接。6. 手写一个 JDBC 增删改查模板类把重复代码留在基类里6.1 一个不依赖框架的 JdbcTemplate如果项目只有几张表不想引入 MyBatis又不想每个 DAO 都写一遍 try-with-resources可以把变化的部分收敛成参数。下面这个模板类封装了增删改查中最常用的两个方法一个执行更新一个执行查询并把结果映射成对象FunctionalInterface public interface RowMapperT { T map(ResultSet rs) throws SQLException; }public class JdbcTemplate { private final Connection conn; public JdbcTemplate(Connection conn) { this.conn conn; } public int update(String sql, Object... params) throws SQLException { try (PreparedStatement ps conn.prepareStatement(sql)) { bindParams(ps, params); return ps.executeUpdate(); } } public T ListT query(String sql, RowMapperT mapper, Object... params) throws SQLException { try (PreparedStatement ps conn.prepareStatement(sql)) { bindParams(ps, params); try (ResultSet rs ps.executeQuery()) { ListT list new ArrayList(); while (rs.next()) { list.add(mapper.map(rs)); } return list; } } } private void bindParams(PreparedStatement ps, Object... params) throws SQLException { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } } }调用方式很直接JdbcTemplate jdbc new JdbcTemplate(conn); String username 张三; User user jdbc.query(SELECT id, name, email FROM user WHERE name ?, rs - new User(rs.getInt(id), rs.getString(name), rs.getString(email)), username) .stream().findFirst().orElse(null); int rows jdbc.update(UPDATE user SET email ? WHERE id ?, newexample.com, 1);ps.setObject会根据参数实际类型自动调用对应的setXxx减少模板方法数量。它的缺点是很隐晦如果传入了null需要根据 SQL 里的列类型决定setNull的类型参数极端情况下要写ps.setNull(i, Types.VARCHAR)。6.2 这个模板类的事务归属注意JdbcTemplate的构造器接收外部传入的Connection模板类内部不负责关闭连接。这样设计是因为连接归属调用方调用方可以在同一个连接上开启手动事务try (Connection conn DbUtil.getConnection()) { conn.setAutoCommit(false); JdbcTemplate jdbc new JdbcTemplate(conn); jdbc.update(UPDATE account SET balance balance - 100 WHERE id 1); jdbc.update(UPDATE account SET balance balance 100 WHERE id 2); conn.commit(); }如果把关闭连接的逻辑放进模板类手动事务就没法跨多个方法共用连接。让模板类保持 stateless是封装 JDBC 工具时最值得记住的边界。需要连接池时只要把DbUtil.getConnection()换成dataSource.getConnection()模板代码不用动。这个模板类已经能满足中小工具、课程设计、内部后台这类“数据量不大、表结构明确、不需要动态 SQL”的场景当查询条件开始频繁变化、需要动态拼接 SQL 时再考虑引入 MyBatis 或 jOOQ 也不迟。本文还有配套的精品资源点击获取
返回列表