MyBatis入门实验全解析:从JDBC到半自动ORM的核心实践
1. 我的第一个 MyBatis 实验从 JDBC 泥潭里爬出来如果你正在学 Java 后端大概率已经受够了传统 JDBC 的折磨手动注册驱动、获取连接、拼 SQL、预编译、一条条从 ResultSet 里取值、关资源……一个最简单的查用户列表功能写出来的代码一半以上和业务无关。我第一次带学生做这个实验时就说MyBatis 的出现本质上就是把“和数据库打交道”这摊子脏活累活从你手里接过去让你只关心两件事SQL 怎么写结果怎么映射。那这个“实验一 MyBatis 入门操作”到底是做什么的一句话概括用 MyBatis 完成一张表的增删改查理解它“半自动 ORM”的核心思路——SQL 你自己写但连接管理、参数注入、结果集封装这些机械操作全部交给框架。它适合刚学完 JDBC、准备进入框架阶段的初学者也适合那些想搞清楚 MyBatis 到底和 Hibernate、Spring Data JPA 有什么不同的同学。做完这个实验你会明白 MyBatis 为什么能在国内企业里占有率这么高也会为后面整合 Spring Boot、理解 MyBatis 源码打下底子。这篇文章我不打算照着实验指导书念一遍而是把“实验一”背后真正值得搞懂的东西拆开揉碎为什么用 XML 不用注解、核心配置文件里每个节点是干什么的、占位符和拼接符的区别、主键回填怎么玩以及我这些年见过的初学者最容易踩的坑。你照着做一遍不只是“跑通”而是彻底搞明白每一步在干什么。1.1 为什么学习 MyBatis 会让写数据访问代码的体验彻底改变先回一下 JDBC 时代。你要查一个用户列表代码大概是这样的Class.forName 加载驱动DriverManager.getConnection 拿连接然后 Statement 拼 SQL万一里面有单引号你还得转义接着 ResultSet 循环取值每取一个字段调一次 getString最后还要在 finally 里把 connection、statement、resultSet 一个个关掉。这还只是查询如果是增删改你还得关心事务提交。写多了你会发现真正和业务相关的就那一行 SQL剩下的全是模板代码复制粘贴还容易出错。MyBatis 的做法是你写一个接口方法再写一个 XML 文件或者注解把 SQL 绑上去MyBatis 在运行时动态生成实现类自动完成参数绑定和结果集映射。这就是我常给学生打的比方JDBC 是你在厨房里从洗菜切菜到炒菜全包MyBatis 是请了个帮厨你只管说“今天做番茄炒蛋番茄切块、蛋打散”剩下的备菜他全干了但掌勺的还是你——因为 SQL 是你自己写的所以叫“半自动”ORM。也正是因为“SQL 自己写”这个特性MyBatis 没像 Hibernate 那样试图帮你生成所有 SQL而是在你明确告诉它“我要查什么”的基础上把 Java 对象和数据库字段之间的翻译工作自动化。对于复杂查询、多表联查、SQL 调优这些场景MyBatis 的优势非常明显——你手里握的是完整的 SQL 控制权。1.2 实验一到底解决什么问题很多初学者做这个实验时容易陷入“照着敲一遍、跑通了就完事”的状态。实际上它要解决的核心问题有三个第一让你理解 MyBatis 的分层设计——SqlSessionFactoryBuilder、SqlSessionFactory、SqlSession 这三者的关系这在面试里是高频考点第二让你掌握 XML 映射文件里常用标签的写法从简单的 select 到复杂的动态 SQL这是以后开发中每天都要用的技能第三让你明白参数传递和结果映射的底层机制搞清楚为什么有时候接口方法一个参数不用加 Param两个以上就必须加。另外这个实验还会接触到一个很重要的概念MyBatis 的一级缓存。虽然入门阶段不会刻意去讲但你在同一个 SqlSession 里执行两次相同的查询第二次其实不会打到数据库。理解这个行为对排查以后“数据明明改了为什么查出来还是旧的”这种诡异问题很有帮助。后面我会单独写一节讲。2. 环境搭建和工程初始化Maven 工程 MyBatis 3.5.x做实验第一步是搭环境。我建议直接用 IntelliJ IDEA 建一个普通的 Maven 工程不要一开始就上 Spring Boot。原因很简单Spring Boot 的 starter 帮你把很多东西自动配置好了你反而看不到 MyBatis 原生运行的脉络。入门阶段把每一步都摊开在眼前理解成本是最低的。2.1 创建 Maven 工程并引入依赖我用的是 MyBatis 3.5.15 版本JDK 8 或 11 都行。Maven 依赖就三个mybatis、mysql-connector-java、junit。如果你用其他数据库比如 PostgreSQL就把驱动换成对应的即可。pom.xml 关键内容如下dependencies dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.15/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjunit/groupId artifactIdjunit/artifactId version4.13.2/version scopetest/scope /dependency /dependencies注意如果你用的 MySQL 是 8.0 以上版本驱动类名是 com.mysql.cj.jdbc.DriverURL 需要带 serverTimezone 参数比如 jdbc:mysql://localhost:3306/test?serverTimezoneAsia/Shanghai。这是很多新手第一个报错来源。我建议把依赖版本集中放在 properties 里管理虽然这个实验只有三个依赖但是养成良好的习惯后面依赖多了你就知道好处了。2.2 准备数据库表和实体类本实验我用最经典的 student 表。建表语句CREATE TABLE student ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(32) DEFAULT NULL, age int(11) DEFAULT NULL, email varchar(64) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实体类 Student属性名与表字段一一对应。这里有一个关键点MyBatis 的默认映射规则是“驼峰转下划线”也就是说 Java 里的 userName 能自动映射到数据库的 user_name前提是你开启了 mapUnderscoreToCamelCase 配置。本实验字段都是单单词不存在这个问题但你要有这个意识。实体类我习惯写成这样public class Student { private Integer id; private String name; private Integer age; private String email; public Student() { } public Student(String name, Integer age, String email) { this.name name; this.age age; this.email email; } // getter/setter 方法这里省略 }注意一定要保留无参构造方法。MyBatis 底层是通过反射调用无参构造创建对象的如果你重载了有参构造却忘了写无参构造运行时会直接报 NoSuchMethodException这个错误很隐蔽。3. 核心配置文件 mybatis-config.xml每个节点都是干什么的MyBatis 的全局配置文件是整个框架的“总开关”。实验一里最重要的几个配置项是环境配置数据源和事务、mapper 映射文件注册、以及可选的类型别名和驼峰映射。配置文件默认放在 resources 目录下确保编译后能在 classpath 里找到。3.1 从 environment 到 dataSource 的配置逻辑看一段最标准的配置然后逐个解释?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN https://mybatis.org/dtd/mybatis-3-config.dtd configuration !-- 引入外部 properties 文件如数据库账号密码 -- properties resourcedb.properties/ !-- 开启驼峰命名自动映射 -- settings setting namemapUnderscoreToCamelCase valuetrue/ /settings !-- 类型别名让 Mapper XML 里可以写 student 代替全限定类名 -- typeAliases package namecom.example.entity/ /typeAliases environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ property nameusername value${username}/ property namepassword value${password}/ /dataSource /environment /environments !-- 注册 Mapper 映射文件 -- mappers package namecom.example.mapper/ /mappers /configurationenvironment 节点定义了一个“运行环境”。default 属性指定默认使用哪个环境如果你想整多套环境开发、测试、生产就在 environments 下写多个 environment然后切换 default 即可。transactionManager 的 type 如果是 JDBC表示事务由 MyBatis 自行管理提交和回滚都是手动调用 SqlSession 的 commit/rollback如果是 MANAGED则表示把事务交给外部容器管理比如 Spring这个到整合阶段再接触本实验用 JDBC 就对了。dataSource 的 type 有三种可选值UNPOOLED、POOLED、JNDI。POOLED 是带连接池的MyBatis 内置了一个简单的连接池实现实验里选它没问题目的就是让连接复用性能更好。生产环境中这个位置通常由 Druid 或 HikariCP 替代但 MyBatis 的 POOLED 连接池机制是相通的你先学会这个后面换连接池就是改个类的事。3.2 mapper 注册的两种方式配置文件里 mappers 节点用来告诉 MyBatis“你的 SQL 映射文件在哪里”。有两种常见方式一是用 mapper resource 直接指定 XML 路径比如mappers mapper resourcecom/example/mapper/StudentMapper.xml/ /mappers二是用 package name 指定包名MyBatis 会自动扫描包下所有接口并要求接口和 XML 文件同名且同目录。第一种适合实验一这种 XML 和接口分开的场景清晰直观第二种适合后面项目化开发。我建议实验阶段先用 resource 方式因为你不容易搞错路径。如果你用了 package 扫描一定要保证 XML 文件的 resources 目录结构和接口的 package 结构保持一致否则启动时会报 Invalid bound statementnot found。这个报错我见得太多了几乎每个班级都会有人遇到。它出现的原因99%是 mapper 接口和 XML 文件没有正确关联要么 XML 文件的 namespace 写错了要么 mapper 接口的方法名和 XML 里语句的 id 对不上要么 XML 文件压根没被 MyBatis 扫描到。排查顺序就是一看 namespace二看 id三看 mappers 配置基本百发百中。4. Mapper 接口与 XML 映射文件从 namespace 到 resultMapMyBatis 从 3.0 开始支持 Mapper 接口编程也就是你定义一个接口MyBatis 动态生成实现类。这个机制让调用方只需要面向接口编程不需要关心实现细节。实验一里我们要建一个 StudentMapper 接口和对应的 StudentMapper.xml。4.1 为什么 XML 里要有 namespace先看接口public interface StudentMapper { Student selectById(Integer id); ListStudent selectAll(); int insert(Student student); int update(Student student); int deleteById(Integer id); }对应的 XML?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.mapper.StudentMapper select idselectById resultTypestudent select id, name, age, email from student where id #{id} /select select idselectAll resultTypestudent select id, name, age, email from student /select insert idinsert parameterTypestudent useGeneratedKeystrue keyPropertyid insert into student(name, age, email) values(#{name}, #{age}, #{email}) /insert update idupdate parameterTypestudent update student set name #{name}, age #{age}, email #{email} where id #{id} /update delete iddeleteById parameterTypeint delete from student where id #{id} /delete /mappernamespace 必须写接口的全限定名这是 MyBatis 把接口和 XML 绑定的凭据。MyBatis 在启动时会做两件事一是注册这个 namespace 下的所有 SQL 语句二是把 namespace 和对应的接口 class 建立映射。运行时当你调用 mapper 接口的方法MyBatis 会根据“接口全限定名 方法名”找到唯一一条 SQL 语句。所以 namespace 一旦写错哪怕只差一个字母运行时就报 binding exception。4.2 resultType 和 resultMap 的选择对于这张 Student 表字段名和属性名完全一致直接用 resultType 指定别名student 是我们在 mybatis-config.xml 里配的 typeAliasesMyBatis 会自动把查询结果的列名映射到 Student 的属性上。但如果你的结果集列名和 Java 属性名对不上比如数据库里是 user_nameJava 里是 userName有两种解决方案第一是开启驼峰映射 mapUnderscoreToCamelCase前提是列名符合下划线风格第二是写 resultMap 显式映射。实验一我用 resultType 就够了但我会给学生演示一下 resultMap因为它是理解 MyBatis 结果集映射机制的核心resultMap idstudentMap typecom.example.entity.Student id propertyid columnid/ result propertyname columnname/ result propertyage columnage/ result propertyemail columnemail/ /resultMapresultMap 里的 id 标签是主键列result 标签是普通列。这个映射机制在后面做多表联查、一对多嵌套结果集时是你的救命稻草。入门阶段先认识它知道它是“列名和属性名的翻译官”就够了。4.3 #{} 和 ${} 的区别初学者必须分清的坑这是 MyBatis 面试题里的第一题也是实际操作中最容易出事的地方。#{} 是预编译占位符MyBatis 会把它解析成 JDBC 的 ?由 PreparedStatement 来设置参数天然防止 SQL 注入${} 是字符串拼接MyBatis 直接把它替换成参数的字符串值不经过预编译。例如select idselectByName resultTypestudent select * from student where name #{name} /select执行时 MyBatis 会生成 select * from student where name ? 这样的 SQL然后 setString(1, name)。换成 ${} 则是 select * from student where name 张三如果 name 里带着单引号SQL 就会被注入。所以结论很明确能用 #{} 的地方绝对不用 ${}。${} 的唯一合理应用场景是动态表名、排序列名这种无法用占位符的地方而你必须在代码层做白名单校验不能直接把用户输入拼进去。5. 核心操作实操从 getAll 到主键回填五种常用 SQL 一次说清实验一的重点体现其实就是写一个工具类、写一个测试类把 6 种常见数据库操作跑通。下面我把每一步的细节都给你捋一遍包括踩过的坑。5.1 工具类 SqlSessionFactoryUtil为什么要用单例MyBatis 的入口是 SqlSessionFactoryBuilder 读入配置构建出 SqlSessionFactory再由 SqlSessionFactory 开启 SqlSession。一个常见的误区是每次都 new 一个 SqlSessionFactoryBuilder 重新读配置——这没必要因为 SqlSessionFactory 是线程安全的一个应用只需要创建一次。我习惯写一个工具类public class SqlSessionFactoryUtil { private static SqlSessionFactory sqlSessionFactory; static { try { String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } public static SqlSession getSqlSession() { return sqlSessionFactory.openSession(); } }Resources.getResourceAsStream 是 MyBatis 提供的工具方法会从 classpath 根路径加载资源等价于类加载器的 getResourceAsStream。openSession() 默认是不自动提交的也就是说你要对数据库做写操作时必须手动 commit这个下面具体说。SqlSession 不是线程安全的每个线程必须使用自己的 SqlSession 实例。绝不能在多个线程间共享同一个 SqlSession。即使你用 SqlSession 直接操作也别这样做。这是 MyBatis 源码里明写的很多坑都源于此。5.2 测试类的完整演示查询、插入、更新、删除JUnit 测试类如下public class StudentMapperTest { Test public void testSelectById() { SqlSession sqlSession SqlSessionFactoryUtil.getSqlSession(); try { StudentMapper mapper sqlSession.getMapper(StudentMapper.class); Student student mapper.selectById(1); System.out.println(student.getName()); } finally { sqlSession.close(); } } Test public void testInsert() { SqlSession sqlSession SqlSessionFactoryUtil.getSqlSession(); try { StudentMapper mapper sqlSession.getMapper(StudentMapper.class); Student student new Student(小明, 18, xiaomingexample.com); int rows mapper.insert(student); sqlSession.commit(); System.out.println(影响行数 rows); System.out.println(自增主键 student.getId()); } finally { sqlSession.close(); } } Test public void testUpdate() { SqlSession sqlSession SqlSessionFactoryUtil.getSqlSession(); try { StudentMapper mapper sqlSession.getMapper(StudentMapper.class); Student student mapper.selectById(1); student.setAge(20); mapper.update(student); sqlSession.commit(); } finally { sqlSession.close(); } } Test public void testDelete() { SqlSession sqlSession SqlSessionFactoryUtil.getSqlSession(); try { StudentMapper mapper sqlSession.getMapper(StudentMapper.class); mapper.deleteById(2); sqlSession.commit(); } finally { sqlSession.close(); } } }注意几个细节第一查询操作不 commit 没关系但写操作insert/update/delete一定要 sqlSession.commit()否则数据不会真正写入数据库而且连接不释放的话第二次操作还会拿到一把“脏连接”。第二用 try-finally 保证 SqlSession 一定关闭。忘记关闭会导致连接池连接数慢慢耗尽服务器最终“卡死”。第三插入后从 student.getId() 就能拿自增主键这依赖 XML 里的 useGeneratedKeys 和 keyProperty 配置。5.3 主键回填useGeneratedKeys 背后的机制在 insert 语句里我写了 useGeneratedKeystrue keyPropertyid。这两个配置的意思是执行完 INSERT 后把数据库生成的自增主键值回填到传入对象的 id 属性上。它的底层实现是 JDBC 的 getGeneratedKeys 方法MySQL 驱动支持这种操作。为什么不回填的话你就拿不到 id因为自增主键是数据库生成的你的 Java 对象在 insert 之前那个 id 是 null。如果你需要在插入后立刻拿到主键去做一些关联操作比如插入订单后马上插入订单明细明细要带订单id这一步非常关键。如果你用 MySQL也可以不用这个功能而是重新查询一次但那样多一次数据库交互性能就差一些了。5.4 参数传递的两个细节Param 和 parameterType看上面的 mapper 接口selectById(Integer id) 只有一个参数XML 里写 #{id} 没问题。但如果你的方法有两个以上参数比如 selectByNameAndAge(String name, Integer age)MyBatis 就不知道 #{name} 具体是哪个参数了。这时你必须用 Param 注解Student selectByNameAndAge(Param(name) String name, Param(age) Integer age);XML 里写 #{name}、#{age}。如果你不写 ParamMyBatis 会以 arg0、arg1 或 param1、param2 作为参数名代码可读性会大打折扣而且在某些版本下行为可能不一样。入门阶段我给的建议是凡是一个方法参数是两个及以上的一律加 Param养成习惯后面写复杂 SQL 时省很多事。另外XML 里 select 标签的 parameterType 属性可以省略MyBatis 会自动推断参数类型。但入门阶段建议写上一是可读性二是某些老版本 MyBatis 会在某些边界场景下推断出错。insert、update 标签同理。6. 进阶知识点串联动态 SQL、缓存、逻辑删除面试前必须搞懂的硬货实验一跑通之后很多人会问这也就够做个单表 CRUDMyBatis 凭什么是国内 Java 开发的主流答案在于它的动态 SQL 能力——在 XML 里写逻辑分支极大简化了多条件查询。这个部分我单独展开讲也顺带把热搜里的几个高频问题串进来。6.1 动态 SQL 的核心标签和“if test indexof”的奇技淫巧比如根据姓名和年龄做条件查询学生可能只传姓名、只传年龄、两个都传、都不传。用 JDBC 你得拼 SQL 加判断用 MyBatis 可以这样select idselectByCondition resultTypestudent select id, name, age, email from student where if testname ! null and name ! and name #{name} /if if testage ! null and age #{age} /if /where /selectwhere 标签会自动处理掉第一个 and 前缀这个很省心。如果你不用 where 标签而是自己写 where 11也能跑但 where 标签更优雅而且语义清晰。热搜词里有个“mybatis if test indexof”这其实是在问 if 标签里怎么写字符串包含判断。比如查询名字里带“明”字的学生你可以这样if testname.indexOf(明) ! -1 and name like concat(%, #{name}, %) /if但这里有个大坑OGNL 表达式里写单引号字符串时外部要用双引号否则会解析异常。正确写法是if testname.indexOf(明) ! -1这种冷门细节在面试里倒是常被拿来考实际开发中更常见的做法是把这类条件直接放在 Java 代码层处理而不是全堆在 XML 里。动态 SQL 还有两个高频标签foreach 用于批量操作choose 相当于 Java 的 switch。实验一没用到但建议自己试着写一个批量插入insert idbatchInsert insert into student(name, age, email) values foreach collectionlist itemstu separator, (#{stu.name}, #{stu.age}, #{stu.email}) /foreach /insert这背后其实是把多个 values 段拼接成一条 SQL减少了数据库交互次数。但要注意MySQL 默认单条 SQL 大小有限制max_allowed_packet一次性插入几千条也可能触发报错所以批量插入最好分批一般每批 500 条左右比较稳妥。6.2 MyBatis 缓存机制一级缓存、二级缓存、以及逻辑删除MyBatis 缓存在入门实验里不一定会演示但它是面试题重灾区。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 里执行两次相同的查询第二次直接从缓存返回不查数据库。前提是查询中间没有执行 insert/update/delete这些操作会清空缓存。二级缓存是 namespace 级别的需要手动开启多个 SqlSession 共享但要注意脏数据问题——如果两个表关联查询缓存数据就可能因为另一张表的更新而过期。再说说“mybatis plus 查询 禁用逻辑删除”。逻辑删除是指数据不真正删除而是通过一个 deleted 字段标记为已删除。MyBatis-Plus 默认会在查询时自动拼接 deleted 0 条件。如果你想在一次查询里把已删除的数据也查出来就得在查询条件里设置 TableLogic 相关的处理或者在 mapper 接口方法上把逻辑删除的自动填充关掉。这个属于 MyBatis-Plus 的范畴但它背后反映的是 MyBatis 拦截器原理——MyBatis 在执行 SQL 前可以通过插件机制动态改写 SQL。6.3 MyBatis 工作原理和源码从哪入手面试官问“MyBatis 工作原理”时其实是想听一条完整链路解析配置文件生成 Configuration 对象、通过 SqlSessionFactoryBuilder 构建 SqlSessionFactory、SqlSessionFactory 打开 SqlSession、SqlSession 根据 Mapper 接口动态代理生成实现类、执行 SQL 时通过 Executor 调用 StatementHandler、经过 ParameterHandler 设置参数、再经 ResultSetHandler 处理结果集。这条链路里最重要的几个类Configuration、MappedStatement、Executor 和四个核心组件。源码阅读的话我建议从 SqlSessionFactoryBuilder.build() 入手跟着它一路看到 XMLConfigBuilder 解析配置文件的过程然后再看 mapper 的 bindMapperForNamespace。第二步去看 SqlSessionManager 的动态代理是怎么把接口方法映射到 MappedStatement 上的。面试官很少要求你背代码但你说得出“Configuration 是根对象、MappedStatement 封装 SQL 语句信息、Executor 负责读写数据库”这套就能过关。6.4 打印 SQL 和可执行 SQL 插件开发排查的必备工具在 mybatis-config.xml 里配置日志开发阶段强烈建议把 SQL 打出来settings setting namelogImpl valueSTDOUT_LOGGING/ /settings这样 MyBatis 会把执行的 SQL、参数、返回行数打印到控制台。缺点是打印的是预编译 SQL参数是问号你想复制出来直接执行还得自己替换。所以社区里有了“打印可执行 SQL 插件”比如 p6spy、MyBatis SQL 日志插件它们通过拦截 PreparedStatement 参数把 SQL 和参数拼成可直接执行的完整语句。如果你用 Idea也可以装 MyBatis Log Plugin 插件。这类插件本质上是基于 MyBatis 的拦截器机制Interceptor在 Executor 或 StatementHandler 层面拦截在参数设置完成后把 SQL 拼接成一个字符串再打日志。学了 MyBatis 插件机制后你自己也可以写一个原理不复杂。6.5 查询行号、只读模式报错等边界问题速查热搜里还有几个有意思的关键词。“mybatis查询增加行号”指的是在 SQL 里用 ROW_NUMBER() OVER (ORDER BY xxx) 生成序号列这在分页列表展示时经常用到。MyBatis 里直接写在 SQL 里就行比如select idselectWithRowNum resultTypemap select ROW_NUMBER() OVER (ORDER BY id) as rownum, name, age from student /select如果你用的 MySQL 5.x 不支持窗口函数可以改用 rownum : rownum 1 这种写法。“mybatis报错write operations are not allowed in read-only mode (flushmode.man)” 这个报错翻译过来是“不允许在只读模式下执行写操作”。它出现的场景往往是你的数据源比如某些云数据库、只读从库设置了 read-onlytrue或者 MyBatis 环境里的拦截器/事务管理器把连接标记为只读。解决办法是检查数据源配置确保写操作走的是可写连接。如果用的是 Spring 事务还要检查 Transactional(readOnly true) 是不是误加在了写方法上。7. 常见报错与排查经验五个坑每一个都有同学踩过这个章节我以自己的教学和项目经历整理一个速查表遇到问题先对着查大概率能省半小时。异常现象根本原因解决方案java.io.IOException: Could not find resource mybatis-config.xml配置文件不在 classpath 下或名字拼错确认 resources 目录下存在该文件使用 target/classes 检查编译输出org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)Mapper 接口和 XML 未正确绑定检查 namespace 是否等于接口全限定名、方法 id 是否存在、mapper 是否已注册Cause: com.mysql.cj.exceptions.InvalidConnectionAttributeExceptionMySQL 8 连接 URL 缺少时区参数在 URL 添加 serverTimezoneAsia/ShanghaiException in thread main org.apache.ibatis.exceptions.PersistenceExceptionSQL 语法错误或参数类型不匹配打开 SQL 日志把 SQL 拿出来在数据库客户端执行验证插入数据但数据库无变化未提交事务写操作后调用 sqlSession.commit()数据库中自增 id 值跳动或变慢自增步长设置不是 MyBatis 的问题检查 MySQL 的 auto_increment 配置和 InnoDB 锁策略还有一些初学者容易忽视的细节XML 文件放在 resources 目录但命名和接口不同或者漏掉了 mapper 映射的 package 声明这些都可能导致前面说的 Invalid bound statement。我的排查顺序永远是先看控制台日志和 SQL 输出再定位配置最后才怀疑代码逻辑本身。多数情况下问题就出在“配置里少写了一行”或者“包路径写错了”。手动测试时如果 MySQL 8 驱动版本和 MySQL 服务端版本不匹配可能会报 Connection refused 之类的问题。检查驱动版本和 server 端的兼容性8.0.33 驱动连接 MySQL 5.7 服务端一般是没问题的但反过来连接 8.0 可能会因认证插件报错。这个属于环境层面问题发生后先到 MySQL 的日志文件里找线索。8. 从入门到深入实验一之后还可以怎么扩展做完 CRUD 实验如果你还有余力我强烈建议做几个小扩展它们能帮你把 MyBatis 的掌握程度从“会用”拉到“用得明白”而且这些扩展不会花太多时间。第一加一个分页查询功能。先用最朴素的方式 limit #{offset}, #{pageSize} 做手动算 offset。然后你再去了解 RowBounds 的原理以及 PageHelper 这个分页插件是怎么通过拦截器来自动拼出 count 查询和 limit 的。把这个搞明白MyBatis 插件机制你就入门了。第二写一个多表联查。比如加一张 course 表和 score 表学生选课用 resultMap 实现一对多映射。这个练习能帮你彻底搞懂 resultMap 里的 collection 和 association 标签这是 MyBatis 映射能力最强大的地方也是面试重点。第三把整个流程从 MyBatis 原生编程迁移到 Spring Boot 整合版本。你对比一下整合前后的配置差异就能理解 Spring Boot 的 AutoConfiguration 帮我们做了什么。学到这里你已经能应对实际工作中的大多数场景了。第四如果想研究打印可执行 SQL 插件可以自己动手基于 Interceptor 写一个简单的打印拦截器。插件的注解 Intercepts、Signature 怎么用源码里 Executor 和 StatementHandler 的 execute 方法在什么时机调用动手写一下比看十篇文章都有效。9. 我个人在带实验过程中的几点体会最后说几句凭经验得出的建议。做这个实验的时候我见过好几种学习姿态一种是把代码敲一遍跑通就算完一种是试着把每个配置删掉再看有什么影响还有一种是改各种参数看运行结果。多年下来我的体会是最能拉开差距的恰恰是后两种。实验一的内容本身不难真正的分水岭在于你有没有借此建立起“配置驱动”的思维方式——这个框架里几乎每个行为都是配置决定的你改配置文件框架行为就变。比如你把 transactionManager 从 JDBC 换成 MANAGED不 commit 也能写数据了吗你把 dataSource 从 POOLED 换成 UNPOOLED跑大量查询时会变慢多少你把 logImpl 换成 LOG4J2日志输出有什么不同这些问题亲手试一遍比背一百个面试题都管用。还有一个小技巧运行实验时强制自己开两个 SqlSession 做对比实验比如同一个查询分别在两个 SqlSession 里执行观察 SQL 日志打印了几次。你会发现第二次查询没走数据库这就是一级缓存的直观体现。然后再在同一 SqlSession 里先查后改再查看看日志有没有多打一次。这种“实验式学习”的方式能让你对 MyBatis 的理解快速超过那些只停留在“会用”层面的人。

相关新闻

最新新闻

日新闻

周新闻

月新闻