首页 / 资讯中心 / 文章详情

MyBatis核心学习路径:从JDBC到动态SQL与缓存实战

MyBatis核心学习路径:从JDBC到动态SQL与缓存实战 ★ FEATURED ARTICLE
1. 为什么我劝你再忙也要把MyBatis学好先说个背景。这些年我看了不少项目的持久层代码有老项目还在用JDBC裸写SQL的有跟风上了JPA结果业务一复杂就各种别扭的但最常见的还是MyBatis。国内互联网公司的Java后端MyBatis几乎成了持久层的默认选项尤其是那些SQL复杂、需要人工精细控制的业务系统用MyBatis简直顺手到不行。你可能听说过MyBatis就是写SQL的地方这种说法对但也不全对。MyBatis本质是一个半自动化的ORM框架它不像Hibernate那样帮你把对象和表之间的关系完全自动化管理而是把SQL的执行、参数绑定、结果映射这几件事拆开让你自己控制SQL框架帮你处理那些繁琐的样板代码。这个设计取向非常务实SQL是程序员最可控的东西把控制权还给程序员同时把连接管理、参数处理、结果集转换成对象这些脏活累活接过去。这篇文章想跟你聊的是MyBatis的核心学习内容不是那种抄一遍官方文档的笔记而是我把实际项目里用到的、踩过的坑、面试里被问烂的点串成一条学习路径。内容定位于已经会写SQL、懂一点Java基础、但还没系统学过MyBatis的开发者也适合学过一遍但觉得很多细节似是而非的人。学完你应该能做到拿到一个业务需求知道怎么设计Mapper接口和XML映射文件遇到动态查询能利索地写动态SQL搞清楚参数传递和结果映射的细节明白一级缓存和二级缓存各自的坑知道跟Spring整合的时候哪些配置容易出错。这些不是炫技全是干活要用到的硬功夫。2. 整体设计思路MyBatis到底解决什么问题2.1 从JDBC的痛点说起如果你只用过MyBatis可能很难理解它为什么存在。我建议你回去看一眼JDBC原生代码的样子看一眼就够了你就懂MyBatis的设计动机了。JDBC写一次查询流程大概是加载驱动、拿到Connection、准备PreparedStatement、设置参数、执行查询、遍历ResultSet、手动把每一列的值set到对象里、最后还得关掉ResultSet、Statement、Connection。一套下来SQL本身的逻辑只占一小部分大部分代码都在干跟业务无关的事。更烦人的是这些样板代码还不能不写漏掉一个资源关闭连接池就被你搞泄露了。MyBatis的思路是把这些重复劳动收敛到框架内部。你只需要定义SQL长什么样参数怎么传结果怎么映射剩下的连接获取、语句预编译、参数填充、结果集遍历、资源释放全部交给框架。这就是半自动的含义你要写的SQL还是你自己写但SQL之外的东西框架帮你包圆了。2.2 对比Hibernate为什么很多团队选MyBatis提到ORM就绕不开Hibernate。全自动ORM框架的核心卖点是对象关系映射你定义好实体类和表的关系框架自动生成SQL你操作对象就行。听起来很美好但实际业务一复杂自动生成的SQL往往不是最优的尤其在多表关联、分页、批量更新这类场景DBA看了都想打人。MyBatis走了另一条路SQL掌握在开发手里优化空间完全自主。多表关联你写JOIN分页你写LIMIT批量更新你写动态SQL。再加上MyBatis对存储过程、复杂结果映射的支持比Hibernate灵活得多国内团队普遍SQL功力比较强所以MyBatis的接受度一直很高。我自己带项目时的选型标准很简单如果业务是CRUD居多、对象关系简单用不用ORM都无所谓如果业务有大量复杂查询、报表统计、动态条件那无脑选MyBatis它能让你把SQL能力发挥到极致。2.3 MyBatis的三层架构认知学习MyBatis先在心里搭一个整体框架别一上来就抠细节。MyBatis从使用角度可以分成这么几层配置层全局配置文件mybatis-config.xml和映射文件Mapper XML定义了数据源、事务、SQL映射规则。核心处理层SqlSessionFactory、SqlSession、Executor、StatementHandler、ParameterHandler、ResultSetHandler这是MyBatis执行SQL的主流程。接口层Mapper接口你写的业务代码通过它跟框架交互。MyBatis的Mapper接口没有实现类用的是JDK动态代理这是很多新手困惑的点。这三个层次你不需要一开始全弄懂但心里要有这个概念。后面的配置、映射、动态SQL、缓存都是在这三层的某一环里做文章。3. 环境搭建和核心配置这些细节最容易被忽略3.1 引入依赖的最低配置现在基本都用Maven或Gradle管理依赖MyBatis本身的依赖很简单dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency如果你用的是Spring Boot一般引入的是mybatis-spring-boot-starter版本会带Spring Boot的兼容性要求这个后面讲整合的时候再说。只引入MyBatis核心依赖还不够你还得准备一个数据库驱动。比如MySQL就加mysql-connector-j注意选跟数据库版本匹配的驱动版本旧驱动连新数据库可能出现认证协议不兼容的问题。3.2 mybatis-config.xml 里最重要的几个配置全局配置文件是MyBatis的入口配置内容不多但有几个点很关键。环境配置environments是必配的。一个environment对应一个数据源和事务工厂你可以配多个环境比如开发环境、生产环境通过default属性切换。注意配置了多个environment但default只能指定一个。事务工厂这块JDBC事务工厂是常用的它直接使用JDBC的commit和rollback。如果跟Spring整合一般用SpringManagedTransactionFactory让Spring管理事务这个后面要专门说。数据源可以用MyBatis内置的PooledDataSource、UnpooledDataSource也可以配第三方的Druid、HikariCP。实际项目很少用MyBatis自带的数据源但学的时候拿自带的练手完全够用。你只要知道type属性可以取POOLED、UNPOOLED、JNDI生产上多是用外部数据源来接管。映射文件注册也是全局配置文件里必须做的通过mappers标签引入Mapper XML文件mappers package namecom.example.demo.mapper/ /mappers用package指定包名可以一次性注册该包下所有Mapper接口和对应的XML文件比逐个写mapper资源路径省事得多。前提是Mapper接口和XML文件要同名且在同一个包下这个约定后面还要再强调一次。3.3 映射文件的namespace和SQL标签Mapper XML是MyBatis的核心namespace属性绑定到Mapper接口的全限定名。这个绑定关系是MyBatis的一等公民约束namespace写错运行时直接报BindingException。在映射文件里每一个SQL操作对应一个标签select、insert、update、delete。每个标签都有一个idid跟Mapper接口的方法名对应。接口方法没有实现类运行时MyBatis通过namespaceid找到要执行的SQL这就是为什么namespace必须和接口全限定名完全一致。举个例子。接口定义public interface UserMapper { User selectById(Long id); }XML就对应mapper namespacecom.example.demo.mapper.UserMapper select idselectById resultTypecom.example.demo.entity.User select * from user where id #{id} /select /mapper这套对应关系理解了MyBatis一大半的困惑就解开了。很多人上来就去记各种标签我建议先把接口方法怎么映射到SQL这条链路捋清。4. 参数传递的底层原理开发中80%的坑都在这4.1 单个参数和多个参数的区别参数处理是MyBatis里最容易出低级错误的地方。单个简单类型的参数比如一个Long idSQL里随便写什么名字都能取到值因为MyBatis对单参数做了特殊处理参数名不重要。多参数就需要注意了。假设接口方法是这样ListUser selectByNameAndAge(String name, Integer age);如果你在XML里写#{name}和#{age}大概率会报错提示Parameter name not found。因为MyBatis对多个参数的处理方式是把参数封装成一个Mapkey是param1、param2或者你在参数上加了Param注解后指定的名字。解决办法有两个一是在接口方法参数上加Param注解ListUser selectByNameAndAge(Param(name) String name, Param(age) Integer age);XML里就可以用#{name}和#{age}了。二是直接用param1、param2这种默认key但可读性太差项目里不建议这么写。4.2 POJO、Map和Param怎么选单个参数是POJO对象的情况最简单SQL里可以直接用对象的属性名。比如参数是User对象#{name}取的就是user.getName()。单个参数是Map的情况也常见适合动态条件特别多的场景SQL里直接取map的key。缺点是类型不安全key拼错了编译期发现不了运行时才报错。我的建议固定业务场景的查询用POJO查询条件不固定、字段经常加减的用Map多个零散参数必须用Param。这三种方式可以混用MyBatis不限制一个方法只能有一种参数形式。4.3 #{} 和 ${}一个天一个地这两个占位符的区别是MyBatis的经典考点也是生产事故的高发点。#{}是预编译参数占位符MyBatis会把它替换成?走PreparedStatement的参数绑定能有效防止SQL注入。你传什么值它就被当成什么值数据库类型转换也是框架处理。${}是字符串直接替换MyBatis会把参数值直接拼接到SQL语句里再整体发送给数据库执行。如果参数是用户能控制的内容这就是SQL注入的活靶子。项目中什么场景必须用${}动态表名、动态列名、排序字段。比如你根据前端传入的排序字段排序想写order by #{sortField}这是不行的因为排序字段如果被当成预编译参数数据库不会把它当字段名解析。这种情况只能order by ${sortField}所以我的原则是能用#{}绝不用${}必须用${}的地方一定要在代码里做白名单校验确保传入内容完全可控。5. 结果映射从resultType到高级映射5.1 resultType到底映射了什么最常用的结果映射方式是resultType指定一个类的全限定名或别名。MyBatis查询返回的每一行数据会自动把列名跟对象的属性名对应调用setter方法把值填进去。这里就有一个隐性约定数据库列名和Java属性名的映射规则。默认情况下列名user_name不会自动映射到属性userName除非开启了驼峰映射配置mybatis: configuration: map-underscore-to-camel-case: true这个配置几乎是必开的不开的话你会被下划线列名和驼峰属性名的对齐问题搞得想砸键盘。我见过不少老项目没开这个配置SQL里每个字段都得写别名SQL长了一屏都放不下。5.2 resultMap解决复杂映射当你的查询结果涉及关联对象、集合属性、多表查询时resultType就不够用了。resultMap可以显式定义列和属性之间的映射关系还能处理嵌套结果。一对多查询是经典场景。一个订单对应多个订单项你希望查询结果是一个Order对象里面有个ListOrderItem。resultMap还能用于解决多表字段名冲突的问题。比如两张表都有id列你不加处理的话返回的结果里两个id列会串。select时用别名区分列resultMap里用column指代别名property指代Java属性这样就彻底对齐了。resultMap的映射关系一旦定义清楚MyBatis会按照你定义的规则去填充对象。这里有个建议resultMap里的column映射建议写全不要只写对象需要的那几个字段因为后面如果需要复用这个resultMap少列了字段会莫名其妙丢数据。5.3 别名机制少写一点全限定名resultType里写全限定名很啰嗦MyBatis支持在全局配置里配typeAliasestypeAliases package namecom.example.demo.entity/ /typeAliases这样resultType可以直接写类名。需要注意的是别名不区分大小写ibatis会把类名转换成小写别名特性也有但建议直接用类名首字母小写的形式。如果不同的包下有同名类别名就会冲突这时候要么用Alias注解改别名要么老老实实写全限定名。6. 动态SQL实际项目最常用的功能没有之一6.1 if和where拼SQL的正确姿势动态SQL是MyBatis最实用的能力。业务系统里根据条件查询用户列表条件可有可无这种需求太常见了用动态SQL处理非常优雅。select idlistByCondition resultTypecom.example.demo.entity.User select * from user where if testname ! null and name ! and name #{name} /if if testage ! null and age #{age} /if /where /selectwhere标签的作用是如果内部有条件成立会自动在SQL里加上WHERE关键字并且去掉第一个多余的AND或OR。如果你自己写where 11再拼and xxx也能实现同样的效果但不太好看而且多了个永远为真的条件性能上没多大问题纯粹是SQL风格不干净。用where标签是最规范的做法。test属性里的表达式用的是OGNL写成name ! null and name ! 要注意字符串判断的时候别漏了null判断。漏了的话name为null时OGNL求值会怎样我踩过这个坑结果是你以为条件没生效实际上报错或者取到了错误的结果。6.2 set标签处理动态更新更新语句里set标签很实用update idupdateById update user set if testname ! nullname #{name},/if if testage ! nullage #{age},/if /set where id #{id} /updateset标签的作用是动态生成SET关键字并去掉末尾多余的逗号。如果你不用set标签就得自己拼逗号最后一个if里的逗号还得处理特别容易出问题。set标签还有个隐藏的坑如果所有if都不成立生成的SQL会变成update user where id ?这是语法错误。所以动态更新场景至少要保证有一个字段会更新或者在业务代码里提前判断。6.3 foreach处理集合参数批量查询、批量插入都离不开foreach。select idselectByIds resultTypecom.example.demo.entity.User select * from user where id in foreach collectionids itemid open( separator, close) #{id} /foreach /selectcollection属性对应的就是传入的集合参数名。如果参数是List可以用list或collection作为名称如果是数组用array。建议在接口方法参数上加Param注解名称就明确多了。批量插入也可以拿foreach写本质是拼接多条INSERT语句insert idbatchInsert insert into user(name, age) values foreach collectionlist itemitem separator, (#{item.name}, #{item.age}) /foreach /insert这里有一个性能细节MySQL默认的max_allowed_packet能扛住的批量大小有限一次插入几百条没问题上万条就可能会报包大小超限的错。通常建议每批500条以内循环分批执行。6.4 choose、when、otherwise实现分支逻辑if是独立的条件判断多个if之间是并列关系。但业务里经常有互斥分支的场景比如有name就按name查没name有email就按email查都没有就查所有VIP用户。这种需求用if硬写会非常别扭正确的做法是choose标签select idselectByKeyword resultTypecom.example.demo.entity.User select * from user where choose when testname ! null and name ! and name #{name} /when when testemail ! null and email ! and email #{email} /when otherwise and vip 1 /otherwise /choose /where /selectchoose标签相当于Java里的switchwhen分支从上往下匹配匹配到第一个就停止不会像if一样都执行。when条件都不成立时走otherwise。这个结构比连续if清晰得多。6.5 script标签和注解SQL的取舍MyBatis支持在Mapper接口方法上直接用Select、Insert等注解写SQL还可以配合SelectProvider写Provider类生成SQL。注解方式在SQL简单的时候很清爽但SQL一复杂就难受了动态SQL注解里写起来跟字符串拼接一样可读性很差。还有script标签可以让你在注解里写动态SQL但基本上也是把XML的内容塞到字符串里体验并不好。我的经验是单表简单CRUD可以注解多表查询、动态条件多的一律XML。XML的好处是SQL可以独立维护、格式化、DBA直接看得懂、改SQL不用改Java代码重新编译。项目里统一约定大查询走XML这个习惯能减少很多后期维护的麻烦。7. 主键回填和批量操作细节里藏着性能7.1 useGeneratedKeys回填自增主键插入数据后经常需要拿到数据库生成的主键。MyBatis里用useGeneratedKeysinsert idinsertUser useGeneratedKeystrue keyPropertyid insert into user(name, age) values(#{name}, #{age}) /insertuseGeneratedKeys设置为true后数据库自动生成的主键值会被回填到传入对象的id属性上。keyProperty指定的是Java对象的属性名不是数据库列名别写错了。如果你用的是非自增主键比如分布式ID、UUID那不需要这个配置你自己把主键值set到对象里就行。如果数据库不支持自增主键也可以用selectKey子标签在插入后查询但用得不多了。7.2 批量操作的性能不可忽视批量插入用foreach拼接SQL在MySQL下是最快的因为减少了网络往返和SQL解析次数。批量更新如果你想用foreach拼多条updateMySQL默认连接参数里是不支持在一条语句里执行多条update的需要加allowMultiQueriestrue但这个配置在部分场景下会带来额外的SQL注入风险我一般不推荐。更稳妥的批量更新方式是循环调用单条update但要注意控制事务大小不然更新几百条要跑半天。更进一步的方案是case when拼一条update语句在数据量大、更新字段一致的时候性能提升非常明显但SQL写起来比较费劲一般我只有在写批量状态变更这类需求时才用。8. 缓存机制用好了是性能利器用不好是数据事故8.1 一级缓存的生命周期MyBatis一级缓存是默认开启的作用范围是SqlSession。同一个SqlSession里执行相同的SQL相同的语句和参数第二次查询会直接命中缓存不会真正查数据库。这个机制看起来是优化但也暗藏条件两次查询之间如果执行了update、insert、delete一级缓存会被清空。注意清空粒度是整个SqlSession的缓存不是只针对某一条语句的。生产环境里一级缓存容易踩的坑是在同一个SqlSession里先查了一次数据被别人改了你再查一次拿到的还是旧数据。为什么因为一级缓存只在这个SqlSession内生效你每次查询都是同一个会话缓存没有失效。建议不要显式依赖一级缓存做业务。一级缓存更像是一个附赠的默认行为你最好当它不存在查询结果以数据库为准来设计代码。8.2 二级缓存开启需谨慎二级缓存的作用域是namespace级别的也就是一个Mapper对应一个二级缓存。多个SqlSession之间可以共享。开启方式是在Mapper XML里加cache/就这么简单一个标签但后果需要想清楚。二级缓存默认是批量提交时刷新如果两个Mapper操作了同一张表比如订单表被OrderMapper和OrderDetailMapper都查过其中一个更新了数据另一个的缓存不会自动失效你可能会查到脏数据。我几乎不在核心业务表上开二级缓存只在配置类数据、字典数据这种极少更新的场景才用。如果你的查询结果涉及多表join更别开缓存粒度根本控制不住。8.3 了解缓存机制能帮你选择学习缓存不能只背概念要理解MyBatis为什么这么设计。一级缓存的出发点是解决同一个会话内的重复查询但由于SqlSession通常是一次请求一个实际命中率不高二级缓存想把缓存粒度提升到Mapper级别但脏数据风险也随之上升。实际面试或项目评审中被问到缓存方案最加分的回答是明确说明缓存失效条件、脏数据风险以及在什么场景下用Redis做分布式缓存替代MyBatis本地缓存。本地缓存单机用还行一旦多节点部署缓存数据就无法共享了。9. 与Spring整合配置踩坑实录9.1 SqlSessionFactory交给Spring管理单独用MyBatis时你要自己创建SqlSessionFactory。跟Spring整合后SqlSessionFactory的创建和管理交给Spring容器。Spring Boot项目引入mybatis-spring-boot-starter后大部分配置都能自动完成。关键的整合点是事务。MyBatis的原生事务是通过SqlSession来控制的Spring整合后控制权交给了Spring。你需要确保DataSource的配置只有一个如果MyBatis配置的数据源跟Spring管理的数据源不一致事务就控制不到MyBatis那层去会出现看起来提交了但数据没写进去这种诡异问题。9.2 Mapper扫描和XML扫描Spring Boot项目里Mapper接口要被Spring容器管理有两个条件一是接口上加上Mapper注解二是在启动类或者配置类上用MapperScan指定扫描包。两个都用也行但别混用出奇怪效果。XML文件的扫描也要注意。如果Mapper XML没有放在MyBatis默认扫描的位置会报Invalid bound statement (not found)错误。这个错误的排查思路很固定先查namespace和接口全限定名是否一致再查XML文件的路径是否被MyBatis配置扫描到。最省心的做法Mapper接口放src/main/java下的某个包对应的XML放src/main/resources下相同包路径的目录同时配置里指定mapper-locationsmybatis: mapper-locations: classpath:mapper/**/*.xml这是Spring Boot项目里最常见的配置方式按这个来基本不会出问题。9.3 PageHelper分页的正确姿势分页插件是MyBatis生态中最常用到的工具。用PageHelper时要注意几点。第一PageHelper的分页原理是基于拦截器它会在查询SQL后面拼接LIMIT。所以查询语句必须紧跟PageHelper.startPage那一行中间不能有别的查询语句否则分页会作用到错误的查询上。第二分页结果用PageInfo接收它可以拿到总条数、总页数、当前页码等分页信息。那个total值PageHelper是通过自动执行count查询得到的这个count查询在高成本SQL下可能会拖慢性能数据量大时建议手动优化count语句。第三PageHelper版本跟MyBatis版本要兼容。老版本的PageHelper在新版本MyBatis上偶尔会出现拦截不生效的问题升级的时候要一块看。10. 逻辑删除和乐观锁实践中的两个约定10.1 逻辑删除的标准写法现在的业务系统大部分不物理删除数据而是用一个is_deleted字段标记。MyBatis没有内置的逻辑删除拦截器需要自己约定SQL写法。最简单的做法是查询统一加条件where is_deleted 0更新和删除都只做标记更新。但人的记性不可靠。所以一些团队会引入通用Mapper框架或者MyBatis-Plus它们自带逻辑删除配置写代码时不需要每次手动加条件。如果你用的是原生MyBatis建议至少在基础查询方法上统一封装不要每个Mapper自己写一套逻辑删除的SQL不然代码review时你会怀疑自己眼睛。10.2 乐观锁防并发更新乐观锁的常见实现是版本号机制。表里加一个version字段更新前先查出version更新时在SQL里带条件where version #{version}更新成功后version加1。如果更新影响行数为0说明数据在读取后被别人改过业务上要重试或提示用户。MyBatis实现乐观锁不难但要注意update user set name #{name}, version version 1 where id #{id} and version #{version}这个SQL里的version条件要用查出来的旧版本号不是新版本号很多人这块写反了导致乐观锁永远不生效。11. 常见的坑和排查经验11.1 Invalid bound statement (not found)这个报错几乎每个用MyBatis的人都遇到过。含义是MyBatis在运行期找不到Mapper接口对应的方法和SQL的绑定关系。排查顺序检查Mapper接口的包路径和XML文件的namespace是否完全一致。检查XML文件名是否和Mapper接口名一致大小写也要一致。检查XML文件是否在classpath下target目录里有没有。检查mybatis配置的mapper-locations是否覆盖到了XML文件所在路径。这个报错不是编译期报的是运行期调用方法时才报所以一开始很难察觉。我的建议是项目启动后写一个简单的接口测试把每个Mapper调一遍别等跑业务了才发现。11.2 查询结果字段全是null如果查询出来了记录条数是对的但对象里的字段全是null十有八九是数据库列名和Java属性名对不上。解决办法就是开驼峰映射或者手动写resultMap。还有一个容易忽略的点POJO类的属性名跟数据库列名对应上了但SQL里写的列别名跟POJO的属性名对不上也会null。11.3 分页查询total不准确PageHelper返回的total有时候跟你预期不一致通常是因为SQL里有多个resultMap嵌套或者用了DISTINCT。PageHelper的count查询是自动生成的遇到复杂SQL它可能生成得不准。处理方式是自定义count语句或者分页前先手动执行一次count查询。11.4 大批量foreach插入内存溢出foreach拼接插入的SQL字符串如果太大MyBatis在拼接阶段会占用大量内存OOM不是没可能。就算不OOM数据库端也无法处理超大的SQL包。处理手段是分批执行控制在500条一批同时注意一次事务不要开太大。11.5 Mapper接口重载问题Java里接口方法是不允许重载的同名不同参数的方法在MyBatis里会冲突。因为MyBatis通过namespaceid定位SQL接口重载会导致id重复启动时可能直接报错。所以Mapper接口里别写同名方法这是设计上的硬约束。12. MyBatis-Plus和原生MyBatis怎么选现在很多新项目直接用MyBatis-Plus它是在MyBatis基础上做了一层增强封装内置了单表CRUD、通用Mapper、分页插件、逻辑删除、字段自动填充等能力。对于简单CRUD占多数的项目MyBatis-Plus能省掉大量重复SQL。但你要清楚MyBatis-Plus的底层还是MyBatis它的复杂查询能力并没有超过原生MyBatis增强的只是单表操作的便利性。复杂SQL、多表关联、动态查询最终还是要写XML。我的建议是新手学习阶段先用原生MyBatis把核心原理搞清楚再去看MyBatis-Plus的封装你会理解它做了什么、没做什么。直接上MyBatis-Plus容易把底层机制当黑盒出了问题无从排查。13. 最后分享一点我的学习体会MyBatis这个框架表面看就是一套SQL映射工具但深入进去会发现它的设计很克制。它给了你最大程度的SQL掌控力又把那些高度重复的样板细节抽走了。学习它的核心路径我总结起来就是先会用再理解原理最后踩坑补齐细节。我自己带新人的时候会让他们专门花一个下午把JDBC到MyBatis的演进路径走一遍亲手写一个最简单的Mapper从配置文件到API调用全手写一遍不要靠自动化工具生成。这个练习做完后面遇到任何MyBatis的问题至少知道往哪查、怎么查。还有一个小经验遇到奇怪的MyBatis行为优先开日志看实际执行的SQL。MyBatis的SQL日志打印配置在logback或log4j里设置mapper包路径为DEBUG级别能看到参数替换后的完整SQL和每个参数的取值。超过一半的问题看着实际SQL就能定位了。如果你现在刚开始学MyBatis别急着追求那些花哨的插件和框架组合先把XML映射、动态SQL、结果映射这三样练扎实。这三样是MyBatis的根根扎稳了后面什么扩展都接得住。
阅读完成 · 觉得有帮助?
咨询建站