SpringData JPA 都能写 SQL,为啥还要用 MyBatis?

SpringData JPA 都能写 SQL,为啥还要用 MyBatis?
之前聊过JPA和MyBatis的核心区别,但总觉得没说透。实际开发里,很多人纠结选哪个,不是因为不知道“JPA面向对象、MyBatis面向SQL”,而是踩过具体的小坑后才明白——原来差的是这些细节。今天就从日常写代码的真实场景出发,把两者的差异拆得更细,再贴些实际用到的代码,看完你肯定能更清楚怎么选。
先从最基础的“字段映射”说起吧,这是每个项目都绕不开的。我认为JPA的映射看着方便,但遇到特殊场景就容易卡壳;MyBatis看似麻烦点,却能灵活应对各种奇葩表结构。
比如常见的“数据库字段和Java实体字段不一致”的情况。JPA里得用@Column注解指定列名,要是表字段有下划线(比如user_name),实体字段是驼峰(userName),还得在配置里开驼峰命名自动转换,或者每个字段都加@Column(name = “user_name”)。举个例子:
// JPA实体字段映射(应对下划线字段)
@Entity
@Table(name = "t_user") // 表名和实体名不一致,得指定
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 数据库字段是user_name,实体是userName,必须加注解
@Column(name = "user_name")
private String userName;
// 数据库字段是create_time,开了驼峰转换才能不用注解
private LocalDateTime createTime;
// getter/setter...
}
要是遇到更特殊的,比如数据库里是varchar类型存的JSON字符串,实体里想直接用JSONObject接收。JPA就得自定义转换器,写个类实现AttributeConverter接口,再在字段上加@Convert注解,步骤不少。
但MyBatis处理这些就简单直接。字段映射可以在XML里用resultMap明确指定,哪怕字段名完全不一样,也能精准匹配,不用改实体类注解:
<!-- MyBatis XML 字段映射 -->
<resultMap id="UserResultMap" type="com.example.entity.User">
<id column="id" property="id"/>
<!-- 数据库user_name对应实体userName,直接指定 -->
<result column="user_name" property="userName"/>
<!-- 数据库create_time对应实体createTime,不用额外配置 -->
<result column="create_time" property="createTime"/>
<!-- JSON字符串转JSONObject,直接用TypeHandler -->
<result column="ext_info" property="extInfo" typeHandler="com.example.handler.JsonTypeHandler"/>
</resultMap>
<!-- 查询时指定resultMap即可 -->
<select id="getUserById" resultMap="UserResultMap">
SELECT id, user_name, create_time, ext_info FROM t_user WHERE id = #{id}
</select>
在我看来,这种细节上的灵活度,就是很多人宁愿多写点SQL,也选MyBatis的原因——遇到奇葩表结构,不用跟框架“掰扯”,直接按自己的想法来就行。
再说说动态SQL,这也是日常开发高频场景。比如做一个列表查询,前端可能传用户名、状态、时间范围,也可能都不传,需要动态拼接WHERE条件。JPA和MyBatis都能做,但写起来的体验天差地别。
JPA里常用的是Specification或者JpaRepository的方法名派生。方法名派生只能应对简单条件,比如按用户名模糊查询+状态匹配,写个findByUserNameLikeAndStatus就行。但条件一多,方法名会变得巨长,比如findByUserNameLikeAndStatusAndCreateTimeBetweenAndDeptId,看着就头疼,后期改条件也容易写错。
用Specification会灵活点,但代码写起来很繁琐,尤其是多表关联的时候:
// JPA 用Specification做动态查询(用户名模糊+状态+时间范围)
public Page<User> queryUser(String userName, Integer status, LocalDateTime startTime, LocalDateTime endTime, Pageable pageable) {
return userRepository.findAll((root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 用户名模糊查询
if (StringUtils.isNotBlank(userName)) {
predicates.add(cb.like(root.get("userName"), "%" + userName + "%"));
}
// 状态匹配
if (status != null) {
predicates.add(cb.equal(root.get("status"), status));
}
// 时间范围
if (startTime != null && endTime != null) {
predicates.add(cb.between(root.get("createTime"), startTime, endTime));
}
// 拼接条件
return cb.and(predicates.toArray(new Predicate[0]));
}, pageable);
}
这段代码能跑,但读起来费劲,尤其是新人接手,得琢磨半天每个Predicate对应什么条件。而且多表关联时,root.join()的写法很容易出错,调试起来也麻烦。
再看MyBatis的动态SQL,直接在XML里用if、choose、where标签写,逻辑清晰,还能加注释,哪怕条件再多,也能看得明明白白:
<!-- MyBatis 动态SQL(和上面JPA同样的查询条件) -->
<select id="queryUser" resultMap="UserResultMap">
SELECT id, user_name, status, create_time FROM t_user
<where>
<!-- 用户名模糊查询,非空才拼接 -->
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<!-- 状态匹配,非空才拼接 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 时间范围,两个都非空才拼接 -->
<if test="startTime != null and endTime != null">
AND create_time BETWEEN #{startTime} AND #{endTime}
</if>
</where>
ORDER BY create_time DESC
LIMIT #{pageNum}, #{pageSize}
</select>
我们的经验是,动态条件超过3个,用MyBatis写起来效率更高,后期维护也更省心。哪怕是新人,只要懂SQL,就能看懂这段XML里的逻辑,改条件的时候直接加个if标签就行,不用记JPA的各种API。
再聊个深入点的——多表关联查询。JPA的优势是能通过@OneToMany、@ManyToOne这些注解定义实体间的关联关系,查询的时候用fetch指定懒加载或急加载,就能自动关联查询出关联对象。比如查询用户的时候顺带查出用户的订单列表:
// JPA 多表关联(用户-订单 一对多)
@Entity
public class User {
@Id
private Long id;
private String userName;
// 关联订单表,急加载
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id") // 订单表的user_id关联用户表id
private List<Order> orderList;
// getter/setter...
}
// 查询用户时自动带出订单列表
User user = userRepository.findById(1L).orElse(null);
List<Order> orderList = user.getOrderList(); // 直接获取,不用额外查询
这种写法看着方便,但坑也藏在这里。要是关联的表多,比如用户关联订单、订单关联商品、商品关联分类,JPA会生成一堆JOIN语句,甚至出现N+1查询问题(查1个用户带出N个订单,会执行1次查用户+N次查订单的SQL),性能直接拉胯。虽然能通过fetch join优化,但需要手动写JPQL,而且优化的度很难把握。
MyBatis处理多表关联,是手动写JOIN SQL,虽然要自己写关联条件,但能精准控制查询的字段和关联的表,避免冗余查询。比如同样查询用户和订单,只查需要的字段:
<!-- MyBatis 多表关联查询(用户+订单) -->
<resultMap id="UserWithOrderResultMap" type="com.example.entity.User">
<id column="user_id" property="id"/>
<result column="user_name" property="userName"/>
<!-- 关联订单列表 -->
<collection property="orderList" ofType="com.example.entity.Order">
<id column="order_id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="amount" property="amount"/>
</collection>
</resultMap>
<select id="getUserWithOrder" resultMap="UserWithOrderResultMap">
SELECT
u.id as user_id, u.user_name,
o.id as order_id, o.order_no, o.amount
FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE u.id = #{id}
</select>
这段SQL只查了需要的字段,没有多余的关联,执行效率更高。而且如果不需要订单列表,直接查用户表就行;需要的时候再写关联查询,灵活度拉满。要是遇到复杂的多表关联,还能拆分成多个简单查询,手动控制事务,比JPA的“黑盒优化”更让人放心。
最后再补充个细节——SQL的可调试性。JPA自动生成的SQL,有时候会很“奇葩”,比如字段名用奇怪的别名,排序逻辑不清晰。要是查询结果不对,想调试SQL都难,只能开启SQL日志,复制出来格式化后才能排查问题。
MyBatis就不一样了,写的SQL就是最终执行的SQL(除了动态拼接的部分),要是查询有问题,直接把XML里的SQL复制到数据库客户端,替换掉#{参数},执行一下就能定位问题。比如遇到查询结果为空,先在客户端跑SQL,看是条件错了还是字段错了,几分钟就能搞定,不用跟JPA的日志“较劲”。
总结下吧,我不是说JPA不好,它在标准CRUD、快速开发场景下确实香,适合领域模型清晰、SQL复杂度低的项目。但在细节把控上,比如字段映射、动态SQL、多表关联、性能优化,MyBatis的优势太明显了。
在我看来,选框架不是看哪个“高级”,而是看哪个更适配项目。如果是初创项目,追求快速上线,团队SQL能力一般,选JPA没问题;如果是中大型项目,有大量复杂查询,对性能要求高,或者需要DBA介入优化SQL,MyBatis绝对是更稳妥的选择。
现在很多项目都是“JPA+MyBatis”混着用,用JPA做简单的增删改查,用MyBatis处理复杂查询和报表。这种组合既能享受JPA的开发效率,又能拥有MyBatis的灵活度,算是兼顾效率和性能的最优解了。
更多推荐

所有评论(0)