资讯详情

资讯详情

JDBC入门到实战:MySQL连接原理、PreparedStatement与新手避坑指南

如果你是刚学Java、准备接触数据库的初学者大概率已经听过“JDBC”这个名字。JDBC全称Java Database Connectivity是Java官方提供的一套数据库访问标准接口。说白了它就是Java程序跟MySQL这类数据库之间的一座桥——你在Java里写SQL、把数据读出来、写进去全靠它。MySQL是后端开发里用得最多的数据库之一所以“MySQL JDBC”几乎是Java后端入门的必走路径。这篇是系列第一篇我按自己的实操经验把JDBC的原理、环境准备、第一批增删改查代码、还有新手最常见的报错一次性讲清楚。适合学完Java基础、正准备学数据访问的同学参考也适合刚入职需要快速上手项目的老同学快速过一遍基础链路。1. JDBC到底解决什么问题先理解它的设计思想1.1 没有JDBC的年代换数据库等于重写代码在JDBC出现之前Java程序要操作数据库每个数据库厂商都提供自己的一套客户端库。比如要连MySQL就得用MySQL官方的C API封装要连Oracle就得用Oracle自带的一套调用接口代码和具体数据库强绑定。假如公司一开始用MySQL后来因为某些原因要迁移到PostgreSQL那所有数据库操作代码基本要推翻重来工作量巨大。JDBC的核心价值就在这里Java官方定义一套统一的接口标准和调用规范数据库厂商各自实现这套接口对外提供“驱动包”。你的业务代码只面向JDBC接口写要换数据库时换一个驱动包和连接串主体代码不用动。这个思路有点像USB接口——键盘厂商、鼠标厂商、U盘厂商都按USB标准生产电脑上插哪个设备都能用接口统一了使用方省心得多。JDBC这套标准从1997年推出到现在设计理念一直稳如老狗底层再怎么迭代上层抽象几乎没有变化这也是它能穿越技术周期的根本原因。1.2 JDBC核心四件套Driver、Connection、Statement、ResultSetJDBC这套标准里最核心的四个角色是DriverManager驱动管理器负责加载驱动并根据连接URL找到合适的驱动创建连接。Connection数据库连接代表一条真实的会话通道事务也基于它控制。Statement / PreparedStatementSQL语句的载体负责把SQL发送到数据库执行。ResultSet查询结果集相当于数据库返回的一张虚拟表格通过游标逐行读取。这四个角色的协作关系是一条流水线先由DriverManager加载并匹配驱动然后通过DriverManager.getConnection()拿到Connection再通过Connection创建Statement或PreparedStatement执行SQL后得到ResultSet。等数据操作结束资源关闭顺序要和创建顺序相反——ResultSet先关Statement再关Connection最后关。这条流水线后面每一节都会反复用到建议先在脑子里留下这个流程骨架。1.3 为什么有MyBatis还要学JDBC框架的本质还是JDBC很多人会问现在后端项目基本都用MyBatis、Hibernate这些ORM框架为什么还要从JDBC学起我的回答很直接所有Java数据库框架的底层都是JDBC。MyBatis内部就是封装了JDBC的Connection和PreparedStatement框架帮你省掉重复代码可一旦遇到问题比如慢SQL、连接泄漏、事务不生效不懂JDBC根本无从下手。调试框架底层报错信息时能不能看懂那些类和调用栈取决于你对JDBC的理解深度。而且实际工作中有些场景比如一次性数据迁移脚本、定时任务、批量数据导入导出直接写JDBC反而更清爽不用引入一堆框架依赖。所以我的建议是JDBC这个环节别想着跳过去哪怕你以后主要用框架这段基础早晚要还的。2. 动手前的准备工作环境、驱动、建库建表2.1 版本匹配MySQL 5.7与8.0的驱动使用差异写JDBC代码之前先把环境选对。我这里推荐的组合有两种都是实测过非常稳的搭配MySQL 5.7 mysql-connector-java 5.1.49MySQL 8.0 mysql-connector-j 8.0.33这里有个特别容易踩的坑驱动类名变了。老版本驱动类是com.mysql.jdbc.Driver新版本8.x改成了com.mysql.cj.jdbc.Driver。如果你用的是8.x驱动包却按老教程写com.mysql.jdbc.Driver运行时会直接抛ClassNotFoundException。很多网上流传的教程还在贴老代码新手跟着做就莫名其妙报错问题就出在这。另外MySQL 8.0默认的认证插件是caching_sha2_password老的5.1系列驱动不兼容连接时会报Public Key Retrieval is not allowed要么升级驱动版本要么在连接串里加allowPublicKeyRetrievaltrue。这个我后面在排查章节再展开。如果你在Windows服务器上装MySQL搞不定可以先在Docker里拉一个官方镜像跑docker run --name mysql8 -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0提问人经常碰到的问题大多在版本兼容上先把环境跑通再谈代码。2.2 建库建表提前把测试表准备好写代码之前先建一个测试库和测试表这样后面调JDBC时数据都在眼前方便验证。我这里用一个极简的t_user表做演示SQL如下CREATE DATABASE IF NOT EXISTS jdbc_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE jdbc_demo; CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;三个细节说一下建库时指定utf8mb4字符集不要用默认的latin1。数据库底层字符集如果不对后面中文写入读出全是乱码你还得先怀疑代码排查成本翻倍。存储引擎用InnoDB事务支持和并发性能都更好MySQL 5.7之后这基本是默认值但显式写上更稳妥。create_time用DATETIME而不是TIMESTAMPTIMESTAMP有2038年问题而且受时区影响大DATETIME没有这些麻烦省心。执行SQL的方式可以用mysql命令行直接粘贴也可以打开MySQL Workbench建一个SQL脚本文件执行。新手我更推荐Workbench图形界面能看到执行结果和报错位置操作上更友好。Workbench本身也是一个很值得熟悉的工具后面排查数据、看表结构都会用到。2.3 驱动包获取Maven坐标和手动jar包方式驱动jar包怎么拿有两种主流方式看你项目用什么管理依赖。方式一Maven依赖推荐在pom.xml里加上坐标dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意这个坐标在8.0.31之前叫mysql:mysql-connector-java8.0.31之后官方把GAV换成了com.mysql:mysql-connector-j版本号继续沿用。如果你用的旧坐标也没有问题下载到的还是那个驱动只是坐标不同。加了依赖后IDEA刷新一下就会自动下载。方式二手动下载jar包去MySQL官网下载Platform Independent版本的zip包解压出里面的jar文件放到项目的lib目录然后在IDE里右键选择Add as Library。这种方式适合没有用Maven的老项目或者纯命令行练手的学习项目。这里补充一个判断驱动是否加载成功的小技巧在代码里执行Class.forName(com.mysql.cj.jdbc.Driver)如果不报ClassNotFoundException就说明jar包路径和类名都对。这一步新手最容易卡住但排查起来也最简单——看构建路径里有没有这个jar包。3. 核心实操完整跑通JDBC增删改查3.1 连接MySQL的完整代码骨架我直接给一个能跑通的主类代码这个类可以当作JDBC入门的模板后续所有连接逻辑基本都长这样。import java.sql.*; public class JdbcDemo { public static void main(String[] args) { String url jdbc:mysql://localhost:3306/jdbc_demo ?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; String user root; String password 123456; Connection conn null; Statement stmt null; ResultSet rs null; try { // 1. 加载驱动 Class.forName(com.mysql.cj.jdbc.Driver); // 2. 建立连接 conn DriverManager.getConnection(url, user, password); System.out.println(连接成功: conn); // 3. 创建Statement并执行查询 stmt conn.createStatement(); String sql SELECT id, username, create_time FROM t_user; rs stmt.executeQuery(sql); // 4. 处理结果集 while (rs.next()) { int id rs.getInt(id); String username rs.getString(username); Timestamp createTime rs.getTimestamp(create_time); System.out.println(id id , username username , createTime createTime); } } catch (ClassNotFoundException e) { System.out.println(驱动类找不到检查jar包是否导入); e.printStackTrace(); } catch (SQLException e) { System.out.println(数据库操作异常错误码 e.getErrorCode() , 状态 e.getSQLState()); e.printStackTrace(); } finally { // 5. 关闭资源反向顺序 if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }这个代码跑通的标志是控制台打印出连接成功如果表里有数据还会把每一行打印出来。如果你之前一条数据都没插入过结果集为空是正常的可以先往表里插两条数据再跑。3.2 连接串里的每个参数都是干什么的useSSL、characterEncoding、serverTimezone新手极容易忽略连接串觉得照抄就行。实际上这一串参数直接影响能不能连上、数据会不会乱码、时间对不对每一个都值得知道原因。jdbc:mysql://JDBC子协议的固定前缀DriverManager看到这个头就知道找MySQL驱动。localhost:3306MySQL服务所在的机器地址和端口。连远程数据库就改成对应IP和端口。jdbc_demo数据库名这个库不存在时连接报Unknown database。useSSLfalse本地开发和测试建议关闭SSL加密省去握手开销连接速度更快。生产环境网络链路不安全时再考虑开SSL。characterEncodingutf8告诉服务器用UTF-8编码来解析SQL和返回数据这是解决中文乱码的关键环节之一。serverTimezoneAsia/Shanghai8.0驱动要求显式指定时区否则连本机也可能报错或时间偏差。指定成亚洲/上海时区后Java的Timestamp才能正确映射MySQL的DATETIME值。还有一个网络热词经常被问到的是usessl与sslmode使用这其实有两套配置体系。MySQL JDBC驱动用的是useSSL另一个PostgreSQL JDBC驱动用的是sslmode。如果遇到某个教程里写sslmode而你是连MySQL那大概率是教程本身串了数据库类型不用照着抄。useSSLfalse和sslmode对应的是同一个概念连接时是否强制SSL加密。3.3 PreparedStatement增删改查的正确打开方式上面骨架代码用的是Statement但我在真实项目里几乎不用它而是强烈推荐PreparedStatement。原因有三个预编译SQL结构提前发给数据库后续每次执行只传参数执行效率更高。防止SQL注入比如用户名输入 or 11如果靠字符串拼接SQL就变成SELECT * FROM t_user WHERE username or 11条件被整个绕过。PreparedStatement传参时把值当纯数据不会参与SQL结构解析从根上杜绝了这类攻击。可读性好SQL和参数分离代码结构清晰后续维护也方便。先看插入数据的写法String insertSql INSERT INTO t_user (username, password) VALUES (?, ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, zhangsan); ps.setString(2, 123456); int rows ps.executeUpdate(); System.out.println(插入影响行数: rows); }这里有一行很多人第一次看会懵——try(...)后面的括号里直接new了PreparedStatement。这是Java 7引入的try-with-resources语法允许在try后括号里声明资源对象代码块执行完自动调用close()。JDBC的Connection、Statement、ResultSet都实现了AutoCloseable接口都能这么写。这样能省掉finally里一大坨关闭代码。我在实际项目中早就全面切到这个写法代码量少一半以上还能杜绝忘记关闭导致的资源泄漏。3.4 executeQuery与executeUpdate的选择标准JDBC里执行SQL有三种方法很多人刚开始容易混executeQuery()只执行查询返回ResultSet。executeUpdate()执行INSERT、UPDATE、DELETE以及DDL语句返回int值表示受影响行数。execute()通用方法返回boolean一般用不到。完整的增删改查套路如下查用executeQuery增删改用executeUpdate。给一个更新和删除的示例String updateSql UPDATE t_user SET password ? WHERE username ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setString(1, newpass); ps.setString(2, zhangsan); int rows ps.executeUpdate(); System.out.println(更新影响行数: rows); } String deleteSql DELETE FROM t_user WHERE id ?; try (PreparedStatement ps conn.prepareStatement(deleteSql)) { ps.setInt(1, 1); int rows ps.executeUpdate(); System.out.println(删除影响行数: rows); }这里特别注意executeUpdate返回的int值一定要判断。UPDATE影响0行说明WHERE条件没匹配到数据DELETE影响0行说明数据本来就不存在。新手最容易犯的错误是把执行成功当成数据操作成功日志打印一句“更新成功”就完事实际上可能一行都没更新。我自己踩过这个坑后所有增删改代码都会先判断rows再决定下一步逻辑。3.5 ResultSet游标原理rs.next()到底在做什么ResultSet可以理解成数据库返回的一张虚拟表格内部通过游标逐行访问。它初始时游标指向“第一行之前”rs.next()会把游标移到下一行如果存在一行则返回true没有更多行则返回false。所以while (rs.next())几乎成了所有JDBC查询的统一写法内存里形成了一层循环迭代。取值时getXxx方法可以按列索引也可以按列标签。例如rs.getString(2)和rs.getString(username)作用一致。我个人习惯用列名因为SQL里列的排列顺序可能调整按索引取值容易断掉。还有个小注意点rs.next()在同一个查询里只能向前走不能回退所以如果你需要反复读同一行数据要么在循环里先存到对象里要么用rs.beforeFirst()重新定位部分驱动支持否则逻辑会出问题。4. 新手踩坑专场常见报错速查与排查思路4.1 ClassNotFoundException: com.mysql.jdbc.Driver这个报错出现频率最高原因基本就两种jar包没导入或者驱动类名和jar版本不匹配。前面说过8.x驱动的类名是com.mysql.cj.jdbc.Driver如果你用的是新驱动却写老类名结果必然是这个错。排查思路分两步走。先检查构建路径里有没有驱动包IDEA里在外部库和模块依赖里找一下mysql-connector开头的jar再确认类名可以直接解压jar包查看com/mysql/cj/jdbc/Driver.class是否存在存在就说明类名写错了。另外一个快速验证方法命令行执行jar tf mysql-connector-j-8.0.33.jar | grep Driver.class输出里能看到路径就对了。4.2 Communications link failure / Public Key Retrieval is not allowed这是连接MySQL 8.0时非常典型的报错。原因在于MySQL 8.0默认的认证插件caching_sha2_password客户端在连接握手阶段需要从服务器获取公钥来加密密码传输驱动出于安全考虑默认不允许主动请求公钥。解决办法就是在连接串里加参数jdbc:mysql://localhost:3306/jdbc_demo?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue允许客户端从服务器获取公钥配合useSSLfalse使用。这个场景在本地开发时特别常见生产环境一般通过SSL或已配置好服务端证书不太会遇到。4.3 Access denied for user rootlocalhost这个报错说明认证失败典型原因有密码错误、用户不存在、用户没有访问权限。本地连接基本就是密码问题。MySQL 5.7之后root用户的密码规则有些讲究——如果是从官方安装向导装的可能生成临时密码位置一般在安装日志或~/.mysql_secret如果是Docker起的MYSQL_ROOT_PASSWORD环境变量就是密码。登录后再用ALTER USER rootlocalhost IDENTIFIED BY 新密码;重置然后重新跑JDBC代码。4.4 中文乱码按这个顺序排查三个环节中文乱码是所有连接MySQL的人都会遇到的问题出现乱码时按顺序排查连接串有没有characterEncodingutf8。没有就先补上这是代码层最常见的原因。数据库和表字符集用SHOW CREATE TABLE t_user;查看表的CHARSET不是utf8mb4就改掉你建的库和表字符集不一致时也会乱。IDE控制台编码IDEA的Console Encoding和File Encoding都设成UTF-8。我遇到过代码和数据库都对最后发现是控制台输出把UTF-8按GBK解码了改完后乱码立刻消失。排查时不用瞎猜先跑一条SELECT * FROM t_user把结果打印出来看乱码类型——控制台的乱码和数据库里的乱码成因完全不同知道从哪看起能省很多时间。4.5 连接超时网络与MySQL配置双检查代码没问题但连接等很久然后超时十有八九是网络或配置问题。如果连的是远程数据库先用telnet 目标IP 3306测端口通不通不通就要看防火墙、云服务器安全组是否放行3306端口。如果本地连接也超时检查MySQL配置文件里bind-address是否绑定在127.0.0.1这种情况只有本机能连外部IP连不上。改成0.0.0.0并重启MySQL服务后远程才能访问。error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock属于同一类问题但层级不同。这个报错一般出现在MySQL命令行客户端原因是客户端默认走socket文件连接而socket文件不在指定位置。可以先确认mysqld进程有没有启动或者用TCP方式连接绕过socketmysql -h 127.0.0.1 -P 3306 -u root -p这个命令在排查时报错信息会更清晰。5. 新手必问这套基础学完以后还能往哪走5.1 把重复代码封装成DBUtil工具类第一篇讲完你的增删改查应该已经能跑通了。这时候我强烈建议做一件事把注册驱动、获取连接、关闭资源封装成一个DBUtil工具类比如这样public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/jdbc_demo?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; 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 RuntimeException(数据库驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这个工具类做出来后后面的数据访问代码会简洁很多。我把这件事放在第一篇结尾说是因为它意味着你已经从“看懂代码”进入“组织代码”的阶段这个能力后面做项目特别重要。5.2 连接池为什么不能每次都new一个Connection还有一个特别关键的概念我先在这里埋个伏笔连接池。上面所有示例都是每次请求新建Connection、用完就关闭。这在学习阶段没问题但真实项目里如果你毫不在意地每次都创建新连接系统的响应速度马上会受影响——数据库连接从创建到真正可用要经过TCP握手、认证、初始化会话是很耗时的操作。连接池就是预先创建一批连接放在池子里要用的时候拿一个用完还回去。业界常用的有HikariCP和Druid配置和使用我会在系列第二篇展开讲。5.3 事务autocommit关掉后发生了什么另一个必须掌握的是事务。JDBC里Connection默认是自动提交模式每条SQL执行完就立即提交。但业务场景经常需要多条SQL作为一个整体要么都成功要么都失败。这时就需要先conn.setAutoCommit(false)然后执行多条SQL最后conn.commit()某个SQL失败时conn.rollback()。事务隔离级别、MySQL的MVCC、并发情况下读到的数据是什么状态这些问题都是JDBC进阶的关键内容。5.4 从JDBC到MyBatis看看框架到底做了什么最后说说框架。你学完JDBC之后去看MyBatis会发现它的核心配置和操作逻辑隐隐有JDBC的影子SqlSession对应ConnectionMapperStatement对应PreparedStatement参数映射和结果集映射本质上就是JDBC的setXxx和getXxx自动化。所以我很建议你先通过JDBC把SQL执行链路摸透再上手框架这样框架里的很多配置就不会像魔法一样让人摸不着头脑。我个人在实际操作中的体会是JDBC这层东西最值钱的不是那几行API代码而是你理解了一条SQL从Java代码走到数据库再返回来的完整链路。这个链路懂了后面看连接池怎么复用连接、看MyBatis怎么组装参数、看慢SQL怎么排查都会顺畅很多。如果你第一篇看完还有卡住的地方不用急着刷第二篇回头把示例代码多跑几遍尤其是把PreparedStatement的增删改查和DBUtil封装这两件事做扎实后面学习速度会明显上一个台阶。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →