被迫写Java后,我终于搞懂了MyBatis
上周五晚上十点,工位上只剩我和隔壁组的运维小哥还在对峙。他盯着我刚上线的接口报错日志,幽幽地说:“你这SQL注入漏洞,怕是连GPT-4o都救不了。”我默默掏出保温杯喝了一口枸杞水,心想:早知道入职新公司两个月就得从Vim里切出Java世界,还不如继续在前端圈里卷React hooks。
但现实是,作为团队里唯一一个“会点后端”的前端(其实只是刚学Node.js两个月),领导拍着我肩膀说:“你不是研究过分布式系统吗?来,这个订单服务重构交给你了,用MyBatis,别整那些花里胡哨的JPA。”
行吧,谁让我是那个“全栈”(其实是前后端都会一点,但都不精)的倒霉蛋。
为什么是MyBatis?
说实话,一开始我对Java持久层框架的认知还停留在“Hibernate很重,MyBatis很轻”这种模糊印象。直到真正上手,才发现MyBatis的“轻”不是指代码量少,而是控制权在你手里——SQL怎么写、缓存怎么配、事务怎么管,全都明明白白写在XML或注解里,不像某些ORM框架,魔法太多,debug时像在猜谜。
而且,我们团队的数据库设计偏传统,很多老表字段命名不规范(比如usr_nme、cr8_tme),用全自动映射的框架反而更麻烦。MyBatis允许你手动写SQL,还能用<resultMap>做字段映射,简直救星。
三步上手:从零到能跑
第一步:依赖安排上
Maven项目里加个依赖就行(Gradle用户请自行翻译):
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
如果你用的是Spring Boot,直接加mybatis-spring-boot-starter更省事。
第二步:配置数据源和SqlSessionFactory
MyBatis的核心是SqlSessionFactory,它负责创建SqlSession,而SqlSession就是执行SQL的入口。手动配置有点啰嗦,但有助于理解原理:
// 手动初始化(不推荐生产用,但学习时很有用)
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
mybatis-config.xml里主要配数据库连接、别名、Mapper位置等:
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/order_db"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/OrderMapper.xml"/>
</mappers>
</configuration>
小贴士:生产环境千万别把密码写死在配置文件里!我们用的是K8s的Secret挂载+环境变量,运维小哥上次就是因为这事骂我“安全意识为零”。
第三步:写Mapper和SQL
假设有个Order实体:
public class Order {
private Long id;
private String orderNo; // 注意:数据库字段可能是 order_no
private Date createTime;
// getter/setter 省略
}
对应的Mapper接口:
public interface OrderMapper {
Order selectById(Long id);
List<Order> selectAll();
void insert(Order order);
}
然后在OrderMapper.xml里写SQL:
<mapper namespace="com.example.mapper.OrderMapper">
<resultMap id="OrderResultMap" type="Order">
<id property="id" column="id"/>
<result property="orderNo" column="order_no"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="selectById" resultMap="OrderResultMap">
SELECT id, order_no, create_time FROM orders WHERE id = #{id}
</select>
<select id="selectAll" resultMap="OrderResultMap">
SELECT * FROM orders
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders (order_no, create_time)
VALUES (#{orderNo}, #{createTime})
</insert>
</mapper>
注意几个关键点:
resultMap解决了字段名和属性名不一致的问题useGeneratedKeys让自增主键回填到对象里#{}是预编译参数,防SQL注入(那位运维小哥终于闭嘴了)
坑与避坑指南
1. 缓存陷阱:一级缓存太“聪明”
MyBatis默认开启一级缓存(SqlSession级别)。我在测试时发现:同一个SqlSession里查两次同一个ID,第二次居然不走数据库!当时以为是Bug,后来才知道是缓存生效了。
但在Web应用中,每个请求通常新建一个SqlSession,所以一级缓存基本没用。如果真要缓存,建议用二级缓存(需手动开启),但要小心脏读——尤其是在分布式系统里,多个服务实例共享数据库,本地缓存很容易不一致。
我们的做法是:禁用MyBatis二级缓存,改用Redis做分布式缓存,由业务层控制缓存策略。毕竟,一致性比性能更重要,除非你是做秒杀系统(那又是另一个故事了)。
2. 动态SQL:别被<if>绕晕
产品经理上周提了个需求:“支持按订单号、用户ID、时间范围组合查询”。我第一反应是写一堆if-else拼SQL,但MyBatis的<where>和<if>标签让这事变得优雅:
<select id="searchOrders" resultMap="OrderResultMap">
SELECT * FROM orders
<where>
<if test="orderNo != null and orderNo != ''">
AND order_no = #{orderNo}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
</where>
</select>
<where>会自动去掉第一个AND,再也不用担心SQL语法错误了。不过要注意:test表达式里判断字符串非空,得同时判null和空串,不然前端传个空字符串进来,SQL就变成AND order_no = '',可能查出一堆脏数据。
3. 批量操作:别用foreach硬刚
有次我需要批量插入1万条订单,直接写:
<insert id="batchInsert">
INSERT INTO orders (order_no, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.orderNo}, #{item.createTime})
</foreach>
</insert>
结果数据库直接卡死。原因?MySQL单条SQL有长度限制,而且大事务会锁表。
后来改成分批提交(每500条一提交),配合ExecutorType.BATCH,性能提升十倍。不过现在我们更倾向用消息队列异步处理大批量任务——毕竟,系统稳定性比“一次搞定”更重要。
MyBatis vs 其他技术:一点个人思考
虽然我在学Go,也听说Go的gorm用起来很爽,但Java生态的成熟度还是碾压级的。MyBatis虽然要手写SQL,但换来的是极致的可控性——你可以优化每一条SQL,可以加Hint,可以分库分表(配合ShardingSphere),甚至可以在SQL里调用存储过程(虽然我不推荐)。
至于算法?MyBatis本身不涉及复杂算法,但它背后的反射机制、动态代理、缓存淘汰策略,其实都是经典算法的应用。比如它的TypeHandler用策略模式处理类型转换,SqlSession用工厂模式创建,这些设计思想比背LeetCode题更有工程价值。
另外,GPT-4o确实能帮我生成MyBatis的模板代码,但它不懂业务上下文。比如它不知道我们订单表的status字段用0/1/2表示,而不是枚举值;也不知道某些字段要脱敏。所以,AI是工具,不是替代品。
写在最后
从Vim里敲JavaScript,到IDEA里写Java,再到深夜和MyBatis的XML文件对线,这两个月的经历让我明白:所谓全栈,不是什么都会,而是在不同技术栈之间快速切换的能力。
MyBatis不算难,但细节很多。它不像Node.js那样“自由”,也不像Go那样“简洁”,但它稳、可靠、可控——这正是企业级应用最需要的。
现在,我的订单服务已经稳定跑了两周,QPS 2000+,没崩。运维小哥终于对我露出了微笑。而我,也准备开始研究MyBatis-Plus了——毕竟,能少写一行XML,都是对头发的尊重。
(完)

评论 0