别再死记硬背MyBatis面试题了!从ORM原理到SQL执行,带你真正搞懂#{}和${}的区别
从ORM原理到SQL执行深度解析MyBatis中#{}与${}的本质差异在技术面试中关于MyBatis参数替换的经典问题#{}和${}有什么区别几乎成了必考题。大多数候选人都能流利背诵标准答案#{}能防止SQL注入${}是直接替换。但当面试官追问为什么#{}能防注入、预编译具体如何实现、不同用法对缓存有何影响时往往暴露出对底层原理的认知空白。本文将从ORM框架的设计哲学出发带你穿透语法表层真正理解这两种占位符背后的技术本质。1. ORM框架的演进与MyBatis的半自动化设计1.1 从全自动到半自动的范式转变ORMObject-Relational Mapping技术的核心目标是弥合面向对象编程与关系型数据库之间的鸿沟。在Java生态中ORM框架大致可分为两类全自动ORM如Hibernate完全屏蔽SQL细节开发者通过操作对象即可完成数据库交互半自动ORM如MyBatis保留SQL控制权仅自动处理结果集映射// Hibernate典型用法全自动 Session session sessionFactory.openSession(); ListUser users session.createQuery(from User where age :age) .setParameter(age, 18) .list(); // MyBatis典型用法半自动 Select(SELECT * FROM users WHERE age #{minAge}) ListUser selectByAge(Param(minAge) int age);MyBatis选择半自动设计主要基于以下考量SQL可控性复杂查询场景下手写SQL往往比自动生成的更高效渐进式学习开发者可以逐步掌握ORM特性而非被迫接受整套新概念技术债务控制避免全自动框架常见的N1查询等性能陷阱1.2 MyBatis的核心组件协作流程当理解#{}和${}差异时需要先明确MyBatis处理SQL语句的完整生命周期解析阶段XML/注解中的SQL被解析为MappedStatement对象参数替换根据占位符类型进行不同处理SQL构造生成最终可执行的SQL字符串执行阶段通过JDBC与数据库交互graph TD A[Mapper接口调用] -- B[SqlSession获取MappedStatement] B -- C{占位符类型判断} C --|#{}| D[预编译参数设置] C --|${}| E[字符串直接替换] D -- F[生成PreparedStatement] E -- F F -- G[执行数据库操作]2. #{}的预编译机制与安全防线2.1 JDBC预编译原理解析#{}的本质是利用了JDBC的PreparedStatement机制。当MyBatis检测到#{}占位符时将原始SQL转换为带问号的预编译语句创建PreparedStatement对象通过setXxx()方法安全设置参数值// MyBatis生成的JDBC代码示例 String sql SELECT * FROM users WHERE username ?; PreparedStatement ps connection.prepareStatement(sql); ps.setString(1, admin); // 自动处理类型转换和特殊字符转义 ResultSet rs ps.executeQuery();这种机制从三个层面保障了安全性类型安全自动检测参数类型避免类型不匹配错误字符转义自动处理引号、分号等特殊字符二进制传输参数值通过单独通道传输不与SQL指令混合2.2 预编译的性能优势除了安全性预编译还带来显著的性能提升SQL解析缓存数据库服务器可以缓存编译后的执行计划批量操作优化相同SQL模板只需编译一次网络传输优化后续执行只需传输参数值而非完整SQL实测对比在MySQL 8.0中预编译语句的重复执行效率比普通Statement提升约40%3. ${}的字符串替换与适用场景3.1 直接替换的工作原理${}的处理方式则简单直接——纯粹的字符串替换。MyBatis在构建SQL时会直接将${}内的内容替换为参数值的字符串形式-- 原始Mapper配置 Select(SELECT * FROM ${tableName} WHERE id #{id}) User selectFromTable(Param(tableName) String table, Param(id) int id); -- 实际执行SQL假设tableNameusers SELECT * FROM users WHERE id ?这种机制在某些特定场景下非常必要动态表名/列名数据库元数据无法通过参数绑定实现SQL关键字控制如ORDER BY子句中的排序字段特殊语法需求某些数据库特有的语法元素3.2 安全风险与防控措施使用${}时必须警惕SQL注入风险。假设有以下代码Select(SELECT * FROM orders WHERE customer ${name}) ListOrder findByCustomer(Param(name) String name);当传入参数为name admin OR 11时将产生危险查询SELECT * FROM orders WHERE customer admin OR 11防范措施包括白名单校验对表名、列名等有限集合进行校验字符串转义使用StringEscapeUtils.escapeSql等工具最小权限原则数据库账户只授予必要权限4. 缓存机制与参数占位符的交互4.1 一级缓存的关键影响因素MyBatis的一级缓存SqlSession级别对#{}和${}的处理有显著差异特性#{}预处理${}直接替换缓存键生成包含SQL模板和参数值包含最终SQL字符串缓存命中率高相同模板不同参数可复用低每次SQL都不同内存占用较小存储参数值而非完整SQL较大存储完整SQL字符串// 示例相同SqlSession内执行两次 User user1 mapper.selectByAge(18); // 首次查询数据库 User user2 mapper.selectByAge(18); // 命中一级缓存 User user3 mapper.selectFromTable(users); // 查询数据库 User user4 mapper.selectFromTable(employees); // 无法命中缓存4.2 二级缓存的特殊考量二级缓存跨SqlSession的实现更加复杂序列化要求缓存值需要序列化${}生成的SQL可能包含不可序列化元素缓存粒度通常建议在频繁查询且数据变化少的场景使用刷新策略结合flushCache属性控制缓存失效时机!-- 显式配置二级缓存策略 -- cache evictionLRU flushInterval60000 size512 readOnlytrue/5. 实战中的最佳实践与陷阱规避5.1 选择指南何时用哪种占位符根据实际场景做出合理选择优先使用#{}的情况所有业务数据参数ID、名称、状态等条件查询中的比较值分页参数limit/offset插入/更新的字段值谨慎使用${}的情况动态表名如分表场景ORDER BY子句数据库特定函数调用GROUP BY分组字段5.2 性能调优技巧批量操作优化对${}动态SQL使用script标签update idbatchUpdate script foreach collectionlist itemitem separator; UPDATE ${tableName} SET status #{item.status} WHERE id #{item.id} /foreach /script /update分页查询优化结合#{}防止注入Select(SELECT * FROM users ORDER BY ${sortField} LIMIT #{offset}, #{size}) ListUser selectPage(Param(sortField) String field, Param(offset) int offset, Param(size) int size);动态SQL优选方案优先使用if,choose等标签select idsearchUsers SELECT * FROM users where if testname ! null AND name LIKE #{name} /if if testminAge ! null AND age #{minAge} /if /where /select5.3 常见问题排查问题1为什么${}在ORDER BY中不生效检查参数值是否包含特殊字符验证数据库列名是否合法考虑使用bind标签预处理问题2预编译语句日志显示问号配置log4j.logger.java.sql.PreparedStatementDEBUG查看参数值使用MyBatis内置的SQL打印插件问题3缓存未按预期失效检查flushCache配置确认SqlSession生命周期验证事务隔离级别在实际项目中使用MyBatis时曾经遇到过一个典型问题某个分页查询在预发环境表现正常但在生产环境出现性能急剧下降。通过日志分析发现生产环境中误将#{sortField}写成了${sortField}导致无法有效利用预编译缓存每次执行都需要重新解析SQL。这个案例充分证明了理解底层原理的重要性——表面相似的语法实际执行路径可能天差地别。