[{"data":1,"prerenderedAt":520},["ShallowReactive",2],{"\u002Fjava-interview\u002Fmybatis":3,"java-interview-search:\u002Fjava-interview\u002Fmybatis":475},{"id":4,"title":5,"body":6,"category":461,"cover":462,"date":463,"description":464,"extension":465,"group":466,"meta":467,"navigation":468,"order":469,"path":470,"seo":471,"sitemap":472,"stem":473,"__hash__":474},"javaInterview\u002Fjava-interview\u002Fmybatis.md","MyBatis：映射原理、缓存与批量处理",{"type":7,"value":8,"toc":445},"minimark",[9,13,27,44,49,66,73,77,88,110,117,128,132,146,152,159,167,184,194,201,204,208,235,238,253,260,264,286,289,307,311,314,317,320,323,327,333,348,371,382,385,403,406,409],[10,11,12],"p",{},"MyBatis 的面试重点不是会写 CRUD，而是理解它如何在「手写 SQL」和「对象映射」之间做取舍，以及这些设计在事务、缓存和批量场景下的副作用。",[14,15,17,18,22,23,26],"h2",{"id":16},"_1-和-有什么区别","1. ",[19,20,21],"code",{},"#{}"," 和 ",[19,24,25],{},"${}"," 有什么区别？",[10,28,29,31,32,35,36,39,40,43],{},[19,30,21],{}," 会被编译成 ",[19,33,34],{},"PreparedStatement"," 的占位符 ",[19,37,38],{},"?","，参数通过 ",[19,41,42],{},"TypeHandler"," 绑定，能防止 SQL 注入，也能复用执行计划，应该作为传值的默认选择。",[10,45,46,48],{},[19,47,25],{}," 是字符串直接替换，发生在 SQL 解析之前。它适合表名、列名、排序字段这类无法用占位符表达的位置，但必须对输入做白名单校验，否则就是注入漏洞。",[10,50,51,52,54,55,57,58,61,62,65],{},"一个常见误区是「",[19,53,21],{}," 会加引号，",[19,56,25],{}," 不会」——本质区别是预编译绑定与文本拼接，引号只是表象。",[19,59,60],{},"order by #{column}"," 会变成 ",[19,63,64],{},"order by 'name'","，排序不生效，正是这个原因。",[10,67,68,69,72],{},"另外，可空参数在部分数据库（如 Oracle）需要显式 ",[19,70,71],{},"jdbcType","，否则可能报无效列类型；MySQL 通常不强制要求。",[14,74,76],{"id":75},"_2-mapper-接口没有实现类为什么能调用","2. Mapper 接口没有实现类，为什么能调用？",[10,78,79,80,83,84,87],{},"启动时 MyBatis 扫描 Mapper 接口，把每个方法解析成 ",[19,81,82],{},"MappedStatement","，以「命名空间 + 方法名」为 key 存入 ",[19,85,86],{},"Configuration","。",[10,89,90,91,94,95,98,99,102,103,102,106,109],{},"调用时通过 ",[19,92,93],{},"MapperProxyFactory"," 生成 JDK 动态代理。",[19,96,97],{},"MapperProxy"," 拦截方法调用，根据方法签名找到对应语句，转换成 ",[19,100,101],{},"SqlSession.selectOne","、",[19,104,105],{},"selectList",[19,107,108],{},"insert"," 等操作。",[10,111,112,113,116],{},"所以接口只是「声明」，SQL 的真正来源是 XML 或注解。方法重载在 MyBatis 中通常不被支持，因为语句 id 只由方法名决定；参数名在编译时若没保留，需要用 ",[19,114,115],{},"@Param"," 显式命名。",[10,118,119,120,123,124,127],{},"理解这一点也能解释为什么 Mapper 不能被随意 ",[19,121,122],{},"new","：它依赖 ",[19,125,126],{},"SqlSession"," 和配置上下文，脱离容器创建出来的代理无法工作。",[14,129,131],{"id":130},"_3-一级缓存和二级缓存是怎么回事生产环境要不要开","3. 一级缓存和二级缓存是怎么回事？生产环境要不要开？",[10,133,134,135,137,138,141,142,145],{},"一级缓存是 ",[19,136,126],{}," 级别的，默认开启，底层是一个 ",[19,139,140],{},"PerpetualCache","。同一会话中相同的语句和参数会命中缓存，执行增删改或提交、回滚后失效。",[19,143,144],{},"localCacheScope=STATEMENT"," 可以让它只在单条语句内生效。",[10,147,148,149,151],{},"与 Spring 集成后，如果方法没有事务，每次 Mapper 调用都可能使用新的 ",[19,150,126],{},"，一级缓存几乎不命中；处于同一事务时才会共享。",[10,153,154,155,158],{},"二级缓存是命名空间级别，需要显式配置 ",[19,156,157],{},"\u003Ccache\u002F>","，跨会话共享，要求结果可序列化。它在多节点部署下容易读到陈旧数据，写操作也只清空当前命名空间的缓存，因此生产环境通常建议关闭，改用 Redis 等集中式缓存。",[14,160,162,163,166],{"id":161},"_4-resultmap-和-n1-问题怎么处理","4. ",[19,164,165],{},"resultMap"," 和 N+1 问题怎么处理？",[10,168,169,171,172,175,176,179,180,183],{},[19,170,165],{}," 负责列到属性的映射，支持 ",[19,173,174],{},"association","（一对一）和 ",[19,177,178],{},"collection","（一对多）。它能处理下划线转驼峰、类型转换和复杂嵌套，比 ",[19,181,182],{},"resultType"," 的自动映射更可控。",[10,185,186,187,189,190,193],{},"嵌套结果有两种写法：一种是 join 一次查回，用 ",[19,188,165],{}," 的 id 标签合并重复行；另一种是「嵌套查询」，主查询后再按 id 发子查询，配置 ",[19,191,192],{},"fetchType=\"lazy\""," 可以延迟加载。",[10,195,196,197,200],{},"后者就是 N+1 的来源：查 100 条订单会再发 100 次明细查询。解决思路是 join 一次查回，或先收集主键再用 ",[19,198,199],{},"IN"," 批量查、在内存里组装。延迟加载只能推迟开销，不能消除次数。",[10,202,203],{},"还要注意 join 一对多时结果集行数会膨胀，分页要谨慎，先分页主表再查明细通常更稳。",[14,205,207],{"id":206},"_5-插件机制是怎么实现的分页插件原理是什么","5. 插件机制是怎么实现的？分页插件原理是什么？",[10,209,210,211,102,214,102,217,102,220,223,224,22,227,230,231,234],{},"MyBatis 允许拦截四类对象的方法：",[19,212,213],{},"Executor",[19,215,216],{},"StatementHandler",[19,218,219],{},"ParameterHandler",[19,221,222],{},"ResultSetHandler","。插件通过 ",[19,225,226],{},"@Intercepts",[19,228,229],{},"@Signature"," 声明拦截点，再由 ",[19,232,233],{},"InterceptorChain"," 逐层包装成动态代理。",[10,236,237],{},"多个插件形成责任链，顺序由配置决定；包装的是同一批核心对象，所以顺序会影响 SQL 改写和参数处理的结果。",[10,239,240,241,244,245,248,249,252],{},"分页插件通常在 ",[19,242,243],{},"Executor.query"," 处拦截：先根据原 SQL 生成 ",[19,246,247],{},"count"," 语句，再改写为带 ",[19,250,251],{},"limit"," 的语句，最后把结果包装成分页对象。有的实现依赖方言识别，复杂 SQL 的 count 优化需要谨慎。",[10,254,255,256,259],{},"使用分页插件时要注意：分页参数常放在 ",[19,257,258],{},"ThreadLocal"," 中，必须「设置后立即查询」，中间插入其他查询会串页；异步线程里也不会自动传递。",[14,261,263],{"id":262},"_6-大批量写入和超大结果集怎么处理","6. 大批量写入和超大结果集怎么处理？",[10,265,266,267,270,271,273,274,277,278,281,282,285],{},"批量写入有三条路：在 ",[19,268,269],{},"foreach"," 中拼多值 ",[19,272,108],{},"；使用 ",[19,275,276],{},"ExecutorType.BATCH"," 并定期 ",[19,279,280],{},"flushStatements","；或交给上层做分批提交。MySQL 连接串加 ",[19,283,284],{},"rewriteBatchedStatements=true"," 才能把批量语句真正合并发送。",[10,287,288],{},"批处理要注意事务大小：一次提交几十万行会撑大 undo、拉长锁持有时间。按几百到几千条分批，并评估失败重试的幂等性。",[10,290,291,292,294,295,298,299,302,303,306],{},"超大结果集不要用 ",[19,293,105],{}," 一次装进内存。可以用 ",[19,296,297],{},"ResultHandler"," 逐行处理，或用 ",[19,300,301],{},"Cursor"," 流式读取，同时把 ",[19,304,305],{},"fetchSize"," 设为合适的值；MySQL 驱动需要流式模式才真正逐行拉取，连接不能被其他查询复用。",[14,308,310],{"id":309},"_7-mybatis-和-jpa-hibernate-怎么选","7. MyBatis 和 JPA \u002F Hibernate 怎么选？",[10,312,313],{},"MyBatis 把 SQL 控制权交给开发者，适合复杂查询、遗留库、需要精细优化的场景；代价是重复的映射和手写 SQL，简单 CRUD 显得啰嗦。",[10,315,316],{},"JPA\u002FHibernate 以对象模型为中心，自动生成 SQL，开发效率高，但复杂查询容易失控，懒加载和缓存的行为需要经验才能驾驭。",[10,318,319],{},"实际项目里常见混合方案：核心写操作和复杂报表用 MyBatis，后台管理等简单 CRUD 用 JPA 或 MyBatis-Plus。MyBatis-Plus 在 MyBatis 之上提供通用 CRUD、条件构造器和分页，但复杂 SQL 仍要手写，别把它当成完全的 ORM。",[10,321,322],{},"选型的关键不是「哪个更先进」，而是团队能否为生成出来的 SQL 负责。",[14,324,326],{"id":325},"_8-日常使用中有哪些容易踩的坑","8. 日常使用中有哪些容易踩的坑？",[10,328,329,330,332],{},"参数名：Spring Boot 编译默认保留参数名，但依赖构建配置；多参数方法建议显式 ",[19,331,115],{},"，避免升级 JDK 或改编译参数后失效。",[10,334,335,336,339,340,342,343,345,346,87],{},"映射：开启 ",[19,337,338],{},"mapUnderscoreToCamelCase"," 后仍要注意别名覆盖；",[19,341,182],{}," 与 ",[19,344,165],{}," 不要混用；枚举、JSON 字段需要自定义 ",[19,347,42],{},[10,349,350,351,354,355,358,359,362,363,366,367,370],{},"查询：",[19,352,353],{},"selectOne"," 返回多行会抛 ",[19,356,357],{},"TooManyResultsException","；",[19,360,361],{},"\u003Cif test=\"flag == 'Y'\">"," 在 OGNL 中比较的是字符，字符串判断应写成 ",[19,364,365],{},"\u003Cif test='flag == \"Y\"'>","，或使用 ",[19,368,369],{},"'Y'.toString().equals(flag)"," 这类明确写法。",[10,372,373,374,377,378,381],{},"事务与连接：批处理、流式查询会长时间占用连接；多数据源要确认事务管理器和 ",[19,375,376],{},"SqlSessionFactory"," 是否配对；",[19,379,380],{},"allowMultiQueries"," 打开后要评估注入风险。",[14,383,384],{"id":384},"易错点与自测",[386,387,388,397,400],"ul",{},[389,390,391,393,394,396],"li",{},[19,392,25],{}," 不是「不加引号的 ",[19,395,21],{},"」，而是文本替换，任何来自用户的输入都不能直接拼。",[389,398,399],{},"二级缓存是命名空间级、跨会话的，分布式下默认不要开。",[389,401,402],{},"延迟加载解决的是「何时查」，不是「查多少次」，N+1 仍要改成批量或 join。",[10,404,405],{},"自测：① 描述一次 Mapper 方法调用从代理到 JDBC 的完整链路；② 写出批量插入的两种实现并说明事务边界；③ 解释为什么分页插件在异步线程里会失效。",[14,407,408],{"id":408},"参考资料",[386,410,411,421,434],{},[389,412,413,420],{},[414,415,419],"a",{"href":416,"rel":417},"https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002F",[418],"nofollow","MyBatis 3 官方文档"," — 动态 SQL、resultMap、缓存与插件",[389,422,423,428,429,102,431,433],{},[414,424,427],{"href":425,"rel":426},"https:\u002F\u002Fgithub.com\u002Fmybatis\u002Fmybatis-3",[418],"MyBatis 源码（GitHub）"," — ",[19,430,97],{},[19,432,213],{}," 与拦截链实现",[389,435,436,428,441,444],{},[414,437,440],{"href":438,"rel":439},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Fconnector-j\u002Fen\u002Fconnector-j-reference-configuration-properties.html",[418],"MySQL Connector\u002FJ 配置",[19,442,443],{},"rewriteBatchedStatements"," 与流式结果集参数",{"title":446,"searchDepth":447,"depth":447,"links":448},"",2,[449,451,452,453,455,456,457,458,459,460],{"id":16,"depth":447,"text":450},"1. #{} 和 ${} 有什么区别？",{"id":75,"depth":447,"text":76},{"id":130,"depth":447,"text":131},{"id":161,"depth":447,"text":454},"4. resultMap 和 N+1 问题怎么处理？",{"id":206,"depth":447,"text":207},{"id":262,"depth":447,"text":263},{"id":309,"depth":447,"text":310},{"id":325,"depth":447,"text":326},{"id":384,"depth":447,"text":384},{"id":408,"depth":447,"text":408},"java-interview",null,"2026-10-24","讲清 #{} 与 ${} 的区别、Mapper 动态代理、一二级缓存、N+1 与插件机制，并给出大数据量读写和常见踩坑的解法","md","框架与数据库",{},true,"70","\u002Fjava-interview\u002Fmybatis",{"title":5,"description":464},{"loc":470},"java-interview\u002Fmybatis","r2lQvwo2OMwsIhOfbRNZLjYo0F6OORf-USm8Lr5dzH4",[476,480,484,488,492,496,500,504,508,512,516],{"id":470,"title":5,"titles":477,"content":478,"level":479},[],"讲清 #{} 与 ${} 的区别、Mapper 动态代理、一二级缓存、N+1 与插件机制，并给出大数据量读写和常见踩坑的解法 MyBatis 的面试重点不是会写 CRUD，而是理解它如何在「手写 SQL」和「对象映射」之间做取舍，以及这些设计在事务、缓存和批量场景下的副作用。",1,{"id":481,"title":450,"titles":482,"content":483,"level":447},"\u002Fjava-interview\u002Fmybatis#_1-和-有什么区别",[5],"#{} 会被编译成 PreparedStatement 的占位符 ?，参数通过 TypeHandler 绑定，能防止 SQL 注入，也能复用执行计划，应该作为传值的默认选择。 ${} 是字符串直接替换，发生在 SQL 解析之前。它适合表名、列名、排序字段这类无法用占位符表达的位置，但必须对输入做白名单校验，否则就是注入漏洞。 一个常见误区是「#{} 会加引号，${} 不会」——本质区别是预编译绑定与文本拼接，引号只是表象。order by #{column} 会变成 order by 'name'，排序不生效，正是这个原因。 另外，可空参数在部分数据库（如 Oracle）需要显式 jdbcType，否则可能报无效列类型；MySQL 通常不强制要求。",{"id":485,"title":76,"titles":486,"content":487,"level":447},"\u002Fjava-interview\u002Fmybatis#_2-mapper-接口没有实现类为什么能调用",[5],"启动时 MyBatis 扫描 Mapper 接口，把每个方法解析成 MappedStatement，以「命名空间 + 方法名」为 key 存入 Configuration。 调用时通过 MapperProxyFactory 生成 JDK 动态代理。MapperProxy 拦截方法调用，根据方法签名找到对应语句，转换成 SqlSession.selectOne、selectList、insert 等操作。 所以接口只是「声明」，SQL 的真正来源是 XML 或注解。方法重载在 MyBatis 中通常不被支持，因为语句 id 只由方法名决定；参数名在编译时若没保留，需要用 @Param 显式命名。 理解这一点也能解释为什么 Mapper 不能被随意 new：它依赖 SqlSession 和配置上下文，脱离容器创建出来的代理无法工作。",{"id":489,"title":131,"titles":490,"content":491,"level":447},"\u002Fjava-interview\u002Fmybatis#_3-一级缓存和二级缓存是怎么回事生产环境要不要开",[5],"一级缓存是 SqlSession 级别的，默认开启，底层是一个 PerpetualCache。同一会话中相同的语句和参数会命中缓存，执行增删改或提交、回滚后失效。localCacheScope=STATEMENT 可以让它只在单条语句内生效。 与 Spring 集成后，如果方法没有事务，每次 Mapper 调用都可能使用新的 SqlSession，一级缓存几乎不命中；处于同一事务时才会共享。 二级缓存是命名空间级别，需要显式配置 \u003Ccache\u002F>，跨会话共享，要求结果可序列化。它在多节点部署下容易读到陈旧数据，写操作也只清空当前命名空间的缓存，因此生产环境通常建议关闭，改用 Redis 等集中式缓存。",{"id":493,"title":454,"titles":494,"content":495,"level":447},"\u002Fjava-interview\u002Fmybatis#_4-resultmap-和-n1-问题怎么处理",[5],"resultMap 负责列到属性的映射，支持 association（一对一）和 collection（一对多）。它能处理下划线转驼峰、类型转换和复杂嵌套，比 resultType 的自动映射更可控。 嵌套结果有两种写法：一种是 join 一次查回，用 resultMap 的 id 标签合并重复行；另一种是「嵌套查询」，主查询后再按 id 发子查询，配置 fetchType=\"lazy\" 可以延迟加载。 后者就是 N+1 的来源：查 100 条订单会再发 100 次明细查询。解决思路是 join 一次查回，或先收集主键再用 IN 批量查、在内存里组装。延迟加载只能推迟开销，不能消除次数。 还要注意 join 一对多时结果集行数会膨胀，分页要谨慎，先分页主表再查明细通常更稳。",{"id":497,"title":207,"titles":498,"content":499,"level":447},"\u002Fjava-interview\u002Fmybatis#_5-插件机制是怎么实现的分页插件原理是什么",[5],"MyBatis 允许拦截四类对象的方法：Executor、StatementHandler、ParameterHandler、ResultSetHandler。插件通过 @Intercepts 和 @Signature 声明拦截点，再由 InterceptorChain 逐层包装成动态代理。 多个插件形成责任链，顺序由配置决定；包装的是同一批核心对象，所以顺序会影响 SQL 改写和参数处理的结果。 分页插件通常在 Executor.query 处拦截：先根据原 SQL 生成 count 语句，再改写为带 limit 的语句，最后把结果包装成分页对象。有的实现依赖方言识别，复杂 SQL 的 count 优化需要谨慎。 使用分页插件时要注意：分页参数常放在 ThreadLocal 中，必须「设置后立即查询」，中间插入其他查询会串页；异步线程里也不会自动传递。",{"id":501,"title":263,"titles":502,"content":503,"level":447},"\u002Fjava-interview\u002Fmybatis#_6-大批量写入和超大结果集怎么处理",[5],"批量写入有三条路：在 foreach 中拼多值 insert；使用 ExecutorType.BATCH 并定期 flushStatements；或交给上层做分批提交。MySQL 连接串加 rewriteBatchedStatements=true 才能把批量语句真正合并发送。 批处理要注意事务大小：一次提交几十万行会撑大 undo、拉长锁持有时间。按几百到几千条分批，并评估失败重试的幂等性。 超大结果集不要用 selectList 一次装进内存。可以用 ResultHandler 逐行处理，或用 Cursor 流式读取，同时把 fetchSize 设为合适的值；MySQL 驱动需要流式模式才真正逐行拉取，连接不能被其他查询复用。",{"id":505,"title":310,"titles":506,"content":507,"level":447},"\u002Fjava-interview\u002Fmybatis#_7-mybatis-和-jpa-hibernate-怎么选",[5],"MyBatis 把 SQL 控制权交给开发者，适合复杂查询、遗留库、需要精细优化的场景；代价是重复的映射和手写 SQL，简单 CRUD 显得啰嗦。 JPA\u002FHibernate 以对象模型为中心，自动生成 SQL，开发效率高，但复杂查询容易失控，懒加载和缓存的行为需要经验才能驾驭。 实际项目里常见混合方案：核心写操作和复杂报表用 MyBatis，后台管理等简单 CRUD 用 JPA 或 MyBatis-Plus。MyBatis-Plus 在 MyBatis 之上提供通用 CRUD、条件构造器和分页，但复杂 SQL 仍要手写，别把它当成完全的 ORM。 选型的关键不是「哪个更先进」，而是团队能否为生成出来的 SQL 负责。",{"id":509,"title":326,"titles":510,"content":511,"level":447},"\u002Fjava-interview\u002Fmybatis#_8-日常使用中有哪些容易踩的坑",[5],"参数名：Spring Boot 编译默认保留参数名，但依赖构建配置；多参数方法建议显式 @Param，避免升级 JDK 或改编译参数后失效。 映射：开启 mapUnderscoreToCamelCase 后仍要注意别名覆盖；resultType 与 resultMap 不要混用；枚举、JSON 字段需要自定义 TypeHandler。 查询：selectOne 返回多行会抛 TooManyResultsException；\u003Cif test=\"flag == 'Y'\"> 在 OGNL 中比较的是字符，字符串判断应写成 \u003Cif test='flag == \"Y\"'>，或使用 'Y'.toString().equals(flag) 这类明确写法。 事务与连接：批处理、流式查询会长时间占用连接；多数据源要确认事务管理器和 SqlSessionFactory 是否配对；allowMultiQueries 打开后要评估注入风险。",{"id":513,"title":384,"titles":514,"content":515,"level":447},"\u002Fjava-interview\u002Fmybatis#易错点与自测",[5],"${} 不是「不加引号的 #{}」，而是文本替换，任何来自用户的输入都不能直接拼。 二级缓存是命名空间级、跨会话的，分布式下默认不要开。 延迟加载解决的是「何时查」，不是「查多少次」，N+1 仍要改成批量或 join。 自测：① 描述一次 Mapper 方法调用从代理到 JDBC 的完整链路；② 写出批量插入的两种实现并说明事务边界；③ 解释为什么分页插件在异步线程里会失效。",{"id":517,"title":408,"titles":518,"content":519,"level":447},"\u002Fjava-interview\u002Fmybatis#参考资料",[5],"MyBatis 3 官方文档 — 动态 SQL、resultMap、缓存与插件 MyBatis 源码（GitHub） — MapperProxy、Executor 与拦截链实现 MySQL Connector\u002FJ 配置 — rewriteBatchedStatements 与流式结果集参数",1793263559726]