MyBatis基础教程:Java持久层框架入门——一个成都后端仔的实战复盘
上周五晚上十点,办公室只剩我和运维小哥还在对峙。他盯着我改了三遍的SQL语句直摇头:“你这分页查全表,明天大促直接把DB干趴。”我一边在VSCode里疯狂按Ctrl+Z,一边默默打开MyBatis文档——是的,又一次被现实毒打。
我是成都某上市电商公司技术中台团队的一员,日常负责核心交易链路的抽象和复用。坐标天府软件园,生活节奏确实舒服,但一到双11、618,那叫一个“福报拉满”。我们团队有个不成文的规定:能不用原生JDBC就别用,否则半夜报警电话第一个打给你。
今天这篇不是教科书式教程,而是我踩坑、填坑、再踩坑后总结出的MyBatis入门最佳实践。如果你正准备面试,或者刚接手一个老项目发现DAO层全是手写PreparedStatement(别问,问就是祖传代码),那这篇可能救你命。
为什么是MyBatis?因为ORM没那么“香”
很多新人一上来就喊“我要上Hibernate!”,结果上线三天就哭着改回来。为啥?过度封装 = 失控。
在我们中台,数据库查询必须精确到毫秒级响应,还要支持动态拼接条件(比如运营后台的万能筛选器)。Hibernate的HQL和缓存机制在这种场景下简直就是黑盒炸弹。去年双11前压测,一个@OneToMany懒加载没关,直接导致线程池爆满,当时真的想砸电脑。
MyBatis的好处在于:SQL你写,映射你控,性能你说了算。它不替你做决定,只帮你省掉90%的样板代码。对于我们这种既要灵活性又要可控性的中台团队,简直是天作之合。
实战配置:从零搭建一个靠谱的MyBatis环境
别再用XML配置文件堆成山了!现在主流都是 Java Config + 注解 + XML混合。我们团队的标准做法如下:
// DataSourceConfig.java - 数据源交给Spring Boot自动配置
@Configuration
@MapperScan("com.yourcompany.platform.mapper") // 扫描Mapper接口
public class MyBatisConfig {
@Bean
@ConfigurationProperties("mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfiguration() {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true); // 自动驼峰转换,告别as xxx_yyy
config.setLogImpl(StdOutImpl.class); // 开发时打印SQL,上线记得关!
return config;
}
}
application.yml 里配好这些关键项:
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
cache-enabled: false # 默认关闭二级缓存!除非你真懂
血泪教训:二级缓存在分布式环境下极易失效,我们吃过亏。现在所有缓存逻辑都走Redis,MyBatis只干它该干的事——SQL映射。
Mapper怎么写?接口 + XML 是王道
虽然MyBatis支持纯注解(@Select等),但复杂查询必须用XML。原因很简单:可读性、可维护性、支持动态SQL。
来看一个典型的订单查询Mapper:
// OrderMapper.java
public interface OrderMapper {
List<Order> selectOrdersByConditions(@Param("userId") Long userId,
@Param("statusList") List<String> statusList,
@Param("startTime") LocalDateTime startTime);
}
对应的 OrderMapper.xml:
<select id="selectOrdersByConditions" resultType="com.yourcompany.domain.Order">
SELECT order_id, user_id, status, create_time
FROM t_order
WHERE 1=1
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach item="item" collection="statusList" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
ORDER BY create_time DESC
</select>
注意几个最佳实践:
- 用
<if>而不是AND (xxx OR 1=1),前者更安全; @Param注解强制命名参数,避免MyBatis猜错;resultType写全路径,避免同名类冲突。
防坑指南:那些让我通宵的“小问题”
1. 字段映射翻车现场
数据库字段 order_no,Java实体叫 orderNo —— 如果忘了开 mapUnderscoreToCamelCase,查出来全是null。测试同学拿着空数据来找你时,那种绝望……
2. 事务失效的玄学
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 注意:必须是public方法!
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// ...其他操作
}
}
曾经有个同事把方法写成private,事务当然不生效。运维监控看到DB连接数飙升,还以为是DDoS攻击。
3. 分页查询的性能陷阱
别再用 LIMIT #{offset}, #{size} 做深度分页了!超过1万条记录时,MySQL会扫描前面所有行。我们现在用游标分页(基于create_time + id):
SELECT * FROM t_order
WHERE (create_time, id) > (#{lastTime}, #{lastId})
ORDER BY create_time ASC, id ASC
LIMIT #{size}
产品经理说“用户不会翻100页”,但爬虫会。
面试题挑战:MyBatis高频考点拆解
最近帮团队面试,发现很多人对MyBatis的理解还停留在“会用就行”。分享几道我们常问的题:
| 问题 | 考察点 | 正确姿势 |
|---|---|---|
#{}和${}有什么区别? |
SQL注入防护 | #{}预编译,${}直接拼接(慎用!) |
resultMap和resultType怎么选? |
映射复杂度 | 简单对象用resultType,关联查询用resultMap |
一级缓存什么时候失效? |
事务边界理解 | SqlSession关闭或执行update/delete时清空 |
特别提醒:不要说“MyBatis缓存很牛”。在生产环境,我们基本只依赖一级缓存(SqlSession级别),且生命周期极短。二级缓存?除非你能回答清楚它的LRU策略、脏读风险、集群同步问题,否则别碰。
性能与运维:上线不是终点
在我们中台,每个MyBatis应用上线前必须过三关:
- 慢SQL审查:通过Arthas监控实际执行计划;
- 连接池配置:HikariCP的
maximumPoolSize根据DB最大连接数动态调整; - SQL日志脱敏:用户手机号、身份证号不能打到日志里!
曾经有个实习生把logImpl设成Slf4jImpl,结果全量SQL打到ELK,存储费用暴涨——财务部直接找CTO谈话。从此我们加了CI检查:禁止在prod profile开启SQL日志。
写在最后:框架是工具,人才是核心
MyBatis再强大,也挡不住乱写SQL的人。我们团队的文化是:SQL即代码,要CR、要测试、要敬畏。
如果你刚入门,别急着学插件、扩展、自定义TypeHandler。先把<foreach>、<choose>、association这些基础标签玩熟,把驼峰映射、事务边界、参数传递搞明白。这些才是日常开发中最常打交道的东西。
上周那个分页问题,最后我用游标方案重写,QPS从1200提升到8500,运维小哥终于对我笑了。走出公司已是凌晨,成都的夜风微凉,但心里踏实——搞定一个性能问题,比喝十杯奶茶都爽。
共勉。下次双11,希望不再通宵。

评论 0