ARTICLE DETAIL

资讯详情

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

Mybatis七种传参方式详解:从源码到实战一次讲透

Mybatis七种传参方式详解:从源码到实战一次讲透 1. 传参之前先把Mybatis参数绑定机制搞明白1.1 从一次真实报错看参数绑定的本质先讲一个我上个月刚帮同事排过的线上问题。他写了一个很普通的Mapper接口方法ListUser findByStatusAndType(Integer status, Integer type);XML里对应的SQL是这样写的select idfindByStatusAndType resultTypeuser select * from user where status #{status} and type #{type} /select一启动就报错错误信息很典型org.apache.ibatis.binding.BindingException: Parameter status not found. Available parameters are [arg1, arg0, param1, param2]很多新手甚至两三年经验的同学第一次看到这个错都会懵我方法参数明明叫statusXML里也写了#{status}凭什么找不到原因其实不复杂。Mybatis和普通Java方法调用不一样它不是靠方法参数名去自动匹配SQL占位符的。当你的Mapper接口方法有多个参数时Mybatis会把所有参数统一装进一个Map对象而这个Map里面的key不是方法参数原名而是arg0、arg1、param1、param2这种位置参数名——除非你用Param注解显式指定了名字。所以上面那种写法实际上要访问status参数只能写#{arg0}或#{param1}访问type参数只能写#{arg1}或#{param2}。这就是传参方式没掌握带来的第一个坑。搞明白这一层后面七种传参方式学起来就会非常顺。1.2 ParamNameResolver一条规则吃透参数名从哪来既然上面提到了参数会被包装成Map那就必须要讲Mybatis里负责干这件事的类ParamNameResolver。这个类的核心逻辑决定了你的参数在XML里到底以什么名字被访问。它的规则整理下来大致是这样的没有参数返回nullSQL里也不会有占位符。单个参数且没有Param注解、参数类型也不是集合/数组直接把这个参数值作为唯一的参数对象返回。此时XML里#{xxx}写什么名字都能取到值因为Mybatis只认这个参数的值本身不校验名字。多个参数包装成一个ParamMapSortedMapkey的规则是param1、param2按顺序排同时还会加上Param注解指定的名字如果编译时开启了-parameters参数且useActualParamNametrue还可能带上方法参数的真实名字。单个集合/数组参数且没有Param也会被特殊处理后面单独讲。这里有个关键细节param1、param2这种key是无论如何都会生成的它最稳定永远不会丢。而arg0、arg1这种key在不同JDK版本、不同编译参数下可能变成方法参数的真实名字所以不建议在XML里依赖argN这种写法。如果你在IDE里跑的是Java 8以上又开了-parameters编译参数Mybatis其实是能拿到参数真实名字的。但问题在于团队成员、CI服务器、生产环境的编译参数未必一致。今天本地能跑通部署到服务器就报Parameter xxx not found这种环境漂移的坑我见过不止一次。所以想用参数真名最靠谱的方式不是依赖编译器行为而是老老实实写Param。1.3 集合和Map参数的特殊包装wrapCollection在起作用刚才说的是ParamNameResolver的逻辑但还有一个容易忽略的环节——DefaultSqlSession.wrapCollection。当你的Mapper方法只有一个集合或数组参数而且没有加Param注解时比如ListUser findByIds(ListLong ids);Mybatis在真正执行SQL之前会把这个List包装成一个Mapkey是collection和list数组则是arrayvalue才是你传进来的那个集合本身。这就是为什么XML里的foreach标签可以写collectionlist。所以这套流程实际上有两层ParamNameResolver决定方法参数如何命名wrapCollection决定集合参数在SQL层面被包装成什么结构。如果方法参数是Map类型且是单参数则直接把Map本身作为参数对象不会额外包装。此时XML里写#{key}直接访问Map里的值就行。1.4 七种传参方式全景图在展开讲每一种之前先把总览表放出来面试前看这张表基本就能覆盖八成考点。序号传参方式XML中写法示例典型场景必记要点1单个基本类型/String参数#{id}按主键查询单参数时名字随便写都能取到值2多个基本类型参数无Param#{arg0}/#{param1}临时查询建议用paramN别用argN3多个参数使用Param注解#{status}日常业务方法名字由你定义最稳定4POJO对象传参#{name}/#{age}条件较多且可建模属性直接访问可嵌套5Map传参#{name}条件不固定/统计报表key必须和#{}完全一致6List/数组传参foreach collectionlist批量in查询/批量insertcollection取值规则最关键7混合传参ParamPOJO/Map等#{dept.id}查询条件辅助参数并存集合用Param名为collection这张表覆盖了从简单到复杂的全部传参姿势。接下来按场景逐个拆解包括代码示例、底层原理和我在实际项目中踩过的坑。2. 基础场景单参数、无注解多参数、Param2.1 方式一单参数直接传单个基本类型参数是最简单的情况比如User findById(Long id);select idfindById resultTypeuser select * from user where id #{id} /select这里有个很多人不知道的细节单参数且没有Param注解时XML里的#{id}写成#{abc}、#{xxx}都能正常执行。因为ParamNameResolver发现只有一个参数就直接把这个参数值交出去了根本不检查占位符的名字。名字只是摆设值才是关键。但这不代表你可以乱写。一旦在动态SQL里加了if判断问题就来了。比如select idfindById resultTypeuser select * from user where 11 if testid ! null and id #{id} /if /select当参数是单个Long类型且没有Param时动态SQL的test表达式是基于OGNL访问参数对象的属性。Long类型没有id属性所以这个if判断在OGNL层面是取不到值的运气好返回null运气不好直接抛表达式异常。我的建议是即使只有一个参数只要SQL里需要做if非空判断就给它加上Param(xxx)注解。这样OGNL和#{}访问的都是同一个命名参数行为完全可控。比如User findById(Param(id) Long id);Param包装之后ParamNameResolver就会把这个参数放进ParamMapkey为id和param1。XML里的#{id}和testid ! null都能正确访问不会出各种幺蛾子。2.2 方式二无注解多参数多参数方法没有任何注解是传参上最容易翻车的情况。比如User findByStatusAndType(Integer status, Integer type);此时XML里能用的只有arg0、arg1或param1、param2select idfindByStatusAndType resultTypeuser select * from user where status #{param1} and type #{param2} /select从结果上看没问题但这种方式我极其不推荐。理由很简单可读性差param1是什么业务含义没人知道得去翻Mapper接口方法签名。argN不稳定arg0在JDK 8且开启-parameters时会变成方法参数的真实名字也就是说XML里写#{arg0}可能在开发环境报错、生产环境正常或者反过来。参数顺序一旦调整就崩比如把status和type调换位置param1对应的值就变了SQL语义直接错乱编译期还发现不了。所以方法二我建议只在两种场景用一是临时写个排查脚本二是你想故意考一考同事对Mybatis源码的理解程度。正经业务代码里还是老老实实走方式三。2.3 方式三Param命名参数Param注解是Mybatis官方推荐的多参数写法也是我在生产项目里唯一允许的多基础参数写法。User findByStatusAndType(Param(status) Integer status, Param(type) Integer type);select idfindByStatusAndType resultTypeuser select * from user where status #{status} and type #{type} /select加了Param之后ParamNameResolver生成的ParamMap里会有三个keystatus、type、param1、param2。此时在XML里用#{status}、#{type}虽然读起来像参数名但它真正对应的key是注解里指定的字符串和Java方法参数名叫什么已经完全没有关系了。这种写法有几个实打实的好处名字稳定可靠不依赖编译参数和环境行为SQL可读性大幅提升#{status}一看就知道是状态条件动态SQL的if判断好使teststatus ! null直接按注解名访问重构友好Java方法参数改名不会影响XML只要注解名不变。还有一个容易踩的细节如果两个参数用了相同的Param名比如Param(id) Long userId和Param(id) Long roleIdMybatis不会报错但后一个会覆盖前一个导致拿到的是同一个值。这种问题在代码Review阶段基本看不出来只有跑业务的时候才会发现。所以团队里最好定个规范Param值必须全局唯一。2.4 三种方式的取舍场景推荐方式理由单个参数且SQL不做null判断方式一最简单无需额外注解单个参数但SQL要做null判断方式三Param动态SQL的if判断安全多个基础类型参数方式三Param稳定、可读、可维护演示/教学/临时排查方式二展示参数包装原理在正式项目里我基本只保留方式一和方式三。方式二属于知道它存在、能看懂报错即可不建议写进生产代码。3. 结构化传参POJO对象与Map3.1 方式四POJO传参当查询条件比较多的时候比如名称、状态、时间范围、分页信息凑在一起你再拆成一堆Param参数方法签名会变得非常长调用方也要一个个传维护成本很高。这时候就该上POJO了。public class UserQuery { private String name; private Integer status; private LocalDateTime startTime; private LocalDateTime endTime; // getter / setter 省略 }ListUser searchUsers(UserQuery query);select idsearchUsers resultTypeuser select * from user where if testname ! null and name ! and name #{name} /if if teststatus ! null and status #{status} /if if teststartTime ! null and create_time #{startTime} /if /where /selectPOJO传参底层走的是单参数逻辑ParamNameResolver发现只有一个参数且类型不是集合/Map就直接把这个对象作为参数对象传给SQL。XML里的#{name}、#{status}实际上是在对这个对象做属性访问等价于调用getName()、getStatus()。POJO传参有个特别实用的进阶玩法嵌套属性。public class UserQuery { private DeptQuery dept; private String name; }if testdept ! null and dept.deptId ! null and dept_id #{dept.deptId} /if这种对象套对象的写法在处理复杂查询条件时非常方便动态SQL里可以直接用点号访问内嵌对象的属性OGNL表达式和#{}都支持。还有一点值得说POJO传参是批量更新场景的最佳搭档。比如根据传入对象里的非null字段动态生成update语句这种需求用set标签配合POJO属性判断能轻松实现只更新传入的字段避免把其他字段误更新成null。3.2 方式五Map传参Map传参在某些场景下很灵活但也是我把坑字写在脑门上的一种方式。ListUser searchUsers(MapString, Object params);select idsearchUsers resultTypeuser select * from user where status #{status} if testname ! null and name #{name} /if /select调用方需要这样组装参数MapString, Object params new HashMap(); params.put(status, 1); params.put(name, 张三); ListUser users mapper.searchUsers(params);Map传参的底层逻辑在1.3节已经提过单个Map参数时Mybatis直接把Map本身作为参数对象#{status}对应map.get(status)。它的优势是灵活尤其是字段列表不确定、需要动态拼接SQL的统计报表场景。但缺点同样明显没有编译期类型检查params.put(age, abc)这种低级错误只有运行时报类型转换异常才发现key拼写没有IDE提示XML里写#{stauts}代码里put的是status报错信息不是马上能看明白的团队协作成本高你没法从Mapper方法签名看出这个方法需要哪些参数。Map传参典型的报错场景是这样的There is no getter for property named xxx in class java.util.HashMap看到这个错基本就是Map里的key和XML里的#{}名字对不上。排起来倒是不麻烦但对新手来说挺劝退的。3.3 POJO与Map怎么选维度POJO传参Map传参类型安全有编译期检查无运行期才暴露可读性方法签名即文档看不出有哪些key扩展性加字段要改类加key无需改类嵌套结构支持对象内嵌key可以拼成a.b但不直观适用场景业务查询、更新统计报表、临时查询、外部数据源字段不确定我的个人标准是业务主链路上的查询和写操作一律用POJO或ParamPOJO只有报表统计、定时任务、接口对接这种需要动态拼条件的场景才允许用Map。如果你们项目里发现Map传参用得越来越多那大概率是缺少对查询条件对象的抽象建议趁早收敛。4. 批量场景List/数组传参与foreach动态SQL4.1 方式六List/数组传参批量查询是后台开发出镜率最高的场景之一典型写法是where id in (...)。对应的Mapper方法一般是ListUser findByIds(ListLong ids);XML里这样写select idfindByIds resultTypeuser select * from user where id in foreach collectionlist itemid open( separator, close) #{id} /foreach /select这里最容易翻车的就是collection属性。它的取值规则要记清楚方法参数没加Param集合参数会被wrapCollection包装List对应的collection可以写list也可以写collection数组写array方法参数加了Param比如findByIds(Param(ids) ListLong ids)此时collection必须写ids或param1因为参数已经被包装成ParamMapMap里没有list这个key写list会报错。很多同学在没加Param的代码里写习惯了collectionlist后来为了配合其他参数加了Param忘了改collection结果原地踩坑。我把这条规律总结成一句话collection写什么取决于参数对象里有没有这个key加了Paramkey就是注解名没加ParamList就用list数组就用array。顺带说一句foreach里的item变量名是随便起的但#{}里的名字必须和item一致。比如itemuserId里面就写#{userId}别写#{id}。4.2 批量操作完整实战除了批量查询批量insert也是List传参的重头戏。典型写法insert idbatchInsert insert into user(name, age) values foreach collectionlist itemuser separator, (#{user.name}, #{user.age}) /foreach /insert这个方法签名如果是int batchInsert(Param(users) ListUser users);那么collection就得写成users不能写list。批量insert有两个性能层面的经验值供参考每批条数控制在500-1000条比较稳妥太小浪费数据库连接往返太大会导致单条SQL报文过大数据库可能直接拒收MySQL默认是不支持一条SQL里写多条insert语句的但insert into ... values (...),(...)这种单语句多values的形式是支持的上面这种写法正好利用了这个特性不需要额外开allowMultiQueries。批量update则要复杂一些因为MySQL不支持直接update ... set ... from这种写法常见的方案是拼case whenupdate idbatchUpdateStatus update user set status foreach collectionlist itemuser open separator case id when #{user.id} then #{user.status} /foreach end where id in foreach collectionlist itemuser open( separator, close) #{user.id} /foreach /update这种SQL写起来比较啰嗦但确实是绕过循环单条update的高效办法。如果业务量不大循环单条update也不是不行别过度优化。4.3 集合传参与缓存Key的关系既然谈到了批量查询顺便提一个和Mybatis缓存相关的冷门知识点。Mybatis的一级缓存和二级缓存在生成缓存Key时会根据方法ID、SQL语句、参数对象、RowBounds等计算hash值。参数对象在参与缓存Key计算时会调用其toString()方法。这对于列表传参意味着什么如果你传的是一个List缓存Key里会包含这个List的toString()结果。List的toString()是[1, 2, 3]这种格式所以findByIds(Arrays.asList(1,2,3))和findByIds(Arrays.asList(1,3,2))是两个不同的缓存Key不会互相命中——这是合理的。但要注意如果List里装的是POJO而POJO没有重写toString()默认打印的是对象地址那么每次new出来的对象就算数据一模一样缓存Key也会不一样二级缓存形同虚设。这也是为什么我建议核心实体类都重写一下toString()既能打日志又能提升缓存命中率。5. 方式七混合传参配合面试高频追问一起讲透5.1 方式七Param与POJO/Map的混搭真实业务里参数往往不是单一类型而是几个基础参数一个查询对象一个集合混着来。这就是第七种方式——混合传参。ListUser searchUsers(Param(name) String name, Param(query) UserQuery query, Param(ids) ListLong ids);XML里可以这样访问select idsearchUsers resultTypeuser select * from user where if testname ! null and name ! and name #{name} /if if testquery ! null and query.status ! null and status #{query.status} /if if testids ! null and ids.size() 0 and id in foreach collectionids itemid open( separator, close) #{id} /foreach /if /where /select注意几个细节Param(query)绑定了一个POJOXML里用#{query.status}访问内嵌属性Param(ids)绑定了一个List在foreach里collection写ids正是因为Param让这个List进入了ParamMapif里可以用ids.size() 0判断集合非空这是OGNL集合操作的常规写法。混合传参同时解决了条件可建模的部分用对象承载、零散条件用命名参数承载、批量条件用集合承载的问题。我在实际项目中凡是要做多条件组合查询的Mapper方法基本都是这种方式——零散参数用Param查询条件封装成Query对象集合参数单独命名。5.2 为什么Mapper接口没实现类也能跑代理链路的参数传递面试官聊到传参大概率会追问一句Mapper接口不是没有实现类吗它的方法怎么被调用的。这个问题背后的链路正好和传参机制串在一起。Mybatis启动时MapperScan或mapper扫描会为每个Mapper接口注册一个MapperFactoryBean。真正调用一个Mapper方法时走的链路是MapperProxy.invoke() → MapperMethod.execute() → SqlSession.selectOne/selectList → DefaultSqlSession.wrapCollection() // 集合参数包装 → Executor.query() → DefaultParameterHandler.setParameters() → typeHandler.setParameter(PreparedStatement)中间有一个关键环节叫做ParamNameResolver.getNamedParams()——就是它把我们前面讲的单参数直接返回、多参数包装ParamMap、Param命名这些规则执行出来的。所以你在XML里写的#{xxx}本质上不是直接访问Java方法参数而是访问ParamNameResolver生成的那个Map或对象。把这个链路答出来面试官基本就会认为你真的读过Mybatis源码。再配合说一句Mybatis用的是JDK动态代理不是CGLIB在原理层面就站稳了。5.3 面试答题思路七种方式这样讲才加分如果面试官问Mybatis有几种传参方式说说看不建议干巴巴地列七条。更稳的答法是分成四个层次讲基础参数层单个基础类型直接用多个参数加Param顺带说出无注解时param1/arg0的包装规则以及argN不稳定的原因。结构参数层条件多就封装POJO条件不固定就用Map。讲清楚POJO有类型安全检查、Map灵活但可能key拼错再举一个There is no getter for property named xxx的典型报错。集合参数层List/数组传参配合foreach重点说collection的取值规则——没加Param用list/array加了Param用注解名。这是面试官最爱挖的细节。混合参数层实际业务里ParamPOJO集合混搭说明这是最常用的工程姿势。高频追问大概率还有这几个提前准备好#{}和${}有什么区别答#{}是预编译占位符传参走PreparedStatement安全防注入${}是字符串拼接用于表名、排序字段等无法用占位符的地方必须做白名单校验。like模糊查询怎么写答推荐concat(%, #{keyword}, %)不要写%${keyword}%。in查询的传参怎么处理答List传参foreach并说清collection的取值规则。order by字段怎么传答用${}但只能传白名单内的字段名不能直接传用户输入。这样答下来不光是背知识点还体现了实际工程经验印象分会明显不一样。5.4 我踩过坑之后沉淀的传参规范最后把我这几年在传参上踩过的坑整理成一份团队落地规范供参考。每一条背后都对应一次线上问题或者一次代码评审血泪史。多参数必须加Param哪怕只有两个参数也别偷懒。别相信本地能跑这种话环境漂移专治侥幸心理。单参数如果SQL里要做if判断也加Param否则OGNL访问可能取不到值。业务查询条件超过3个字段就封装POJO方法签名保持简洁动态SQL用属性名直接访问。Map传参加白名单限制只允许用在统计报表、配置中心下发、外部接口对接等场景业务主链路禁止。集合参数统一加Param并且collection写注解名不要以任何形式依赖list/array这种隐式key。XML里的#{}名字必须和Param注解值完全一致建议在Code Review里加一条硬性检查。${}只用于排序字段和表名且必须经过白名单校验不接受任何用户原始输入。日志里开启Mybatis的SQL打印排查传参问题时Preparing和Parameters两行日志能让你一眼看出参数到底有没有传对、传成什么类型。传参这点事看着不起眼但它决定了Mybatis SQL映射的稳定性和可维护性。面试爱考是因为它能筛出只看过增删改查和真正理解底层机制的人线上问题频发是因为一旦参数名对不上报错信息对新手来说是真的劝退。把七种方式以及背后的ParamNameResolver规则吃透至少以后再看到Available parameters are [arg1, arg0, param1, param2]你能第一时间知道问题出在哪儿而不是默默把#{status}改成#{arg1}碰运气。
返回列表