MyBatis、MyBatis-Plus、JPA、MyBatisGX 写法比较:同一个需求,四种解法

2 阅读7分钟

MyBatis、MyBatis-Plus、JPA、MyBatisGX 写法比较:同一个需求,四种解法

用两个真实场景,看看四种框架在日常开发中的真实体感。


场景 1:一个"普通"的用户管理需求

产品经理说,用户模块需要支持:

  • 新增用户
  • 查询用户详情(带出 1:1 的用户详情关联)
  • 列表查询(姓名模糊、年龄区间、状态、注册时间范围、分页)
  • 更新用户状态(按部门 ID 批量更新)
  • 删除用户

这几乎是每个后端都写过的需求,足够把各框架的体感完全暴露出来。


MyBatis 写法

1. 查询

UserMapper.xml 核心片段:

<select id="findUserList" resultMap="userResultMap">
    select u.*, d.phone, d.address from user u
    left join user_detail d on u.id = d.user_id
    <where>
        <if test="name != null">and u.name like concat('%', #{name}, '%')</if>
        <if test="minAge != null">and u.age >= #{minAge}</if>
        <if test="maxAge != null">and u.age <= #{maxAge}</if>
        <if test="status != null">and u.status = #{status}</if>
        <if test="startTime != null">and u.create_time >= #{startTime}</if>
        <if test="endTime != null">and u.create_time <= #{endTime}</if>
    </where>
</select>

再加上 <resultMap> 做关联映射、Param 对象、Service 分页拼接……查询 alone 就要写约 50 行 XML。

2. 增删改
<insert id="insert">insert into user(...) values(...)</insert>
<update id="updateStatusByDeptId">update user set status=#{status} where dept_id=#{deptId}</update>
<delete id="deleteById">delete from user where id=#{id}</delete>

增删改本身不复杂,但每个方法都要在 XML 里写一遍,重复劳动不少。


MyBatis-Plus 写法

1. 列表查询

Service 层常见写法:

LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.like(StringUtils.isNotBlank(name), User::getName, name)
       .ge(minAge != null, User::getAge, minAge)
       .le(maxAge != null, User::getAge, maxAge)
       .eq(status != null, User::getStatus, status)
       .ge(startTime != null, User::getCreateTime, startTime)
       .le(endTime != null, User::getCreateTime, endTime);

Page<User> page = userMapper.selectPage(new Page<>(current, size), wrapper);

关联查询?MP 基本要回退到手写 SQL 或自己组装数据。

2. 增删改
// 增
userMapper.insert(user);

// 查单条
userMapper.selectById(id);

// 改
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(User::getStatus, newStatus).eq(User::getDeptId, deptId);
userMapper.update(null, updateWrapper);

// 删
userMapper.deleteById(id);

单表增删查很顺手,但更新照样要在 Service 层拼 Wrapper


JPA 写法

1. 列表查询

方法名查询:

Page<User> findByNameContainingAndAgeBetweenAndStatusAndCreateTimeBetween(
    String name, Integer minAge, Integer maxAge, 
    Integer status, Date startTime, Date endTime, Pageable pageable);

或者用 Specification:

Specification<User> spec = (root, query, cb) -> {
    List<Predicate> predicates = new ArrayList<>();
    if (name != null) predicates.add(cb.like(root.get("name"), "%" + name + "%"));
    if (minAge != null && maxAge != null) 
        predicates.add(cb.between(root.get("age"), minAge, maxAge));
    // ... 省略类似条件
    return cb.and(predicates.toArray(new Predicate[0]));
};
Page<User> page = userRepository.findAll(spec, pageable);
2. 增删改
// 增/改
userRepository.save(user);

// 查单条
userRepository.findById(id);

// 按条件更新(需要写 JPQL)
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.deptId = :deptId")
int updateStatusByDeptId(@Param("deptId") Long deptId, @Param("status") Integer status);

// 删
userRepository.deleteById(id);

关联查询可以在实体上配 @OneToOne,但 LAZY 容易触发 N+1,EAGER 又可能拖出不需要的数据。


MyBatisGX 写法

1. 只需要定义这些

实体(带关联):

@Entity
@Table(name = "user")
public class User {
    @Id
    private Long id;
    private String name;
    private Integer age;
    private Integer status;
    private Long deptId;
    
    @OneToOne(mappedBy = "user", fetch = FetchType.EAGER)
    @Fetch(FetchMode.BATCH)
    private UserDetail userDetail;
}

查询实体:

@QueryEntity(User.class)
public class UserQuery extends User {
    private String nameLike;
    private List<Integer> ageBetween;
    private List<Date> createTimeBetween;
}

DAO 接口——注意:这里不需要写任何自定义方法

@Mapper
public interface UserDao extends SimpleDao<User, UserQuery, Long> {
}
2. Service 层写法
// 增
userDao.insert(user);

// 查单条(自动带出 userDetail,BATCH 模式解决 N+1)
userDao.findById(id);

// 列表查询 —— 这是内置方法,直接传 QueryEntity 就行
Pageable pageable = new Pageable(pageNum, pageSize);
userDao.findPage(userQuery, pageable);

// 按部门 ID 更新状态
User updateData = new User();
updateData.setStatus(newStatus);
userDao.updateByDeptId(deptId, updateData);

// 删
userDao.deleteById(id);
3. 为什么列表查询不需要写方法?

因为 SimpleDao 已经内置了:

@Dynamic
Page<ENTITY> findPage(ENTITY entity, Pageable pageable);

UserQuery 中的字段命名会自动映射成查询条件:

  • nameLikename LIKE '%value%'
  • ageBetweenage BETWEEN min AND max
  • createTimeBetweencreate_time BETWEEN start AND end
  • statusstatus = value

@Dynamic 注解会让 null 字段自动不参与 WHERE 拼接。


场景 2:产品经理改需求了

本来以为这个需求已经够"标准"了。

但上线两周后,产品经理改需求了:

  • 状态从等值变多值
  • 用户名称从等值变模糊
  • 用户需要带角色(多对多)
  • 角色页要反查用户

关系从 1:1 变成了 多对多。看看四个框架分别要改多少代码。


MyBatis 改写

1. 状态等值改多值(IN 查询)

XML 里找 findUserList

<!-- 之前 -->
<if test="status != null">and u.status = #{status}</if>

<!-- 之后 -->
<if test="statusList != null and statusList.size() > 0">
    and u.status in
    <foreach collection="statusList" item="s" open="(" separator="," close=")">
        #{s}
    </foreach>
</if>

Param 对象里的 Integer status 也要改成 List<Integer> statusList

2. 名称从等值改模糊

如果之前用的是 =,现在改成 like concat('%', #{name}, '%')

3. 加多对多关联

需要在 UserMapper.xml 里写新的 resultMap,加入 roleListcollection 映射,再写一条带 JOIN user_roleJOIN role 的 SQL。role 查用户 反向也要再写一套 ResultMap 和 SQL。

约新增 40~60 行 XML。

痛点:改一个查询条件 + 加一层关联,Param → XML → ResultMap 全要动。


MyBatis-Plus 改写

1. 状态等值改多值

找到 Service 里的 Wrapper:

// 之前
.eq(status != null, User::getStatus, status)

// 之后
.in(statusList != null && !statusList.isEmpty(), User::getStatus, statusList)
2. 名称改模糊
// 之前可能用了 .eq()
// 之后
.like(StringUtils.isNotBlank(name), User::getName, name)
3. 加多对多关联

MP 不原生支持多对多自动关联。方案:

  • 手写 SQL 带多表 JOIN
  • 或者查完用户后,再循环查角色列表手动塞进去(又会触发 N+1)

痛点:查询条件一改,所有拼过这个 Wrapper 的 Service 方法都要检查;关联查询仍是短板。


JPA 改写

1. 状态等值改多值 + 名称改模糊

如果你用的是方法名查询:

// 之前
Page<User> findByNameContainingAndAgeBetweenAndStatusAndCreateTimeBetween(...)

// 之后
Page<User> findByNameContainingAndAgeBetweenAndStatusInAndCreateTimeBetween(...)

方法名又变长一截。如果用 Specification,改 cb.equalcb.incb.like,但要找到所有复用了这个条件的地方。

2. 加多对多关联

实体改造:

@Entity
public class User {
    // ...
    @ManyToMany(mappedBy = "userList", fetch = FetchType.LAZY)
    private List<Role> roleList;
}

@Entity
public class Role {
    // ...
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "user_role",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "user_id"))
    private List<User> userList;
}

关联声明确实优雅,但一不留神就会触发 N+1:查 10 个用户,每个用户再去查角色,变成 11 条甚至更多 SQL。要加 @EntityGraphJOIN FETCH 才能缓解,分页时又有新坑。

痛点:条件一变,方法名爆炸;关联加了,但 SQL 黑盒和 N+1 的焦虑也来了。


MyBatisGX 改写

1. 状态等值改多值 + 名称改模糊

打开 UserQuery,改两个字段后缀:

// 之前
private Integer status;

// 之后
private List<Integer> statusIn;

名称本来就是 nameLike,不需要改。

DAO 不需要改,Service 不需要改。 因为 findPage(userQuery, pageable) 是内置方法,框架会自动把 statusIn 解析成 status IN (...)

2. 加多对多关联

实体上直接加注解:

@Entity
@Table(name = "user")
public class User {
    // ...
    @ManyToMany(mappedBy = "userList", fetch = FetchType.LAZY)
    @Fetch(FetchMode.BATCH)
    private List<Role> roleList;
}

@Entity
@Table(name = "role")
public class Role {
    @Id
    private Long id;
    
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "user_role",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "user_id"))
    @Fetch(FetchMode.BATCH)
    private List<User> userList;
}

然后:

// 查用户详情(自动带出 userDetail + roleList)
userDao.findById(userId);

// 查角色详情(自动带出 userList)
roleDao.findById(roleId);

不需要写 ResultMap,不需要写 JOIN SQL。 FetchMode.BATCH 默认会自动解决 N+1:先查主表,再批量用 IN 查关联表,最后组装数据。

一句话总结:需求变了,MyBatisGX 的改动最小,而且关联查询的稳定性是可预期的。


框架优痛点分析

MyBatis

  • ✅ SQL 完全可控,复杂查询最稳
  • ❌ 改一个字段,Param → XML → ResultMap 全链路改
  • ❌ 动态 SQL 一多,XML 迅速膨胀

MyBatis-Plus

  • ✅ 单表 CRUD 效率很高,不用写 XML
  • ❌ 业务层被查询/更新逻辑严重侵入
  • ❌ Wrapper 到处传,DAO 抽象边界逐渐失效
  • ❌ 关联查询是明显短板

JPA

  • ✅ CRUD 极其舒服,面向对象体验好
  • ❌ SQL 黑盒,优化全靠猜
  • ❌ N+1 问题防不胜防
  • ❌ 复杂查询方法名长到离谱,最终往往还是写原生 SQL

MyBatisGX

  • ✅ Service 层零查询逻辑,所有条件收敛到 QueryEntity
  • ✅ 关联查询不用写 SQL / ResultMap,声明式关联 + BATCH 抓取策略 N+1 开箱解决
  • ✅ 分页、批量都是内置能力
  • ⚠️ 超复杂的多表聚合统计,仍然可以用 mapper.xml 兜底(优先级最高)

升维对比:四种框架,本质在解决什么?

框架核心解决的问题典型代价
MyBatisSQL 完全可控重复代码、维护成本高
MyBatis-Plus单表开发效率业务层侵入、复杂查询乏力
JPA对象建模与 CRUD 体验SQL 黑盒、复杂场景失控
MyBatisGX查询定义与业务解耦需要适应新的设计规范

更进一步看 CRUD + 关联的支持:

能力MyBatisMPJPAMyBatisGX
单表增删改需手写 XML内置,快捷内置,优雅内置,简洁
复杂条件查询XML 冗长Service 拼 WrapperSpecification/长方法名QueryEntity 直接传
1:1 关联查询手写 ResultMap需手写 SQL声明式,但 N+1 隐患声明式 + BATCH 自动解决
按条件更新手写 XMLService 拼 UpdateWrapper需写 JPQL方法名自动生成
分页手动处理内置内置内置,对关联友好
需求变更成本

三种框架都在做"取舍",而 MyBatisGX 试图回答的问题是:

能不能既保留 MyBatis 的 SQL 可控性,又拥有 JPA 的声明式关联体验,同时避免 Wrapper/Specification 对业务层的侵蚀?


总结

如果你也遇到过这些困境:

  • 写 MyBatis 写到手麻,每个查询都要复制粘贴 XML
  • 用 MyBatis-Plus 用到业务层一团乱,Wrapper 满天飞
  • 用 JPA 被黑盒 SQL 和 N+1 折磨到半夜调优

那或许可以了解一下 MyBatisGX 的思路:把查询还给 DAO,把业务还给 Service。

有的框架在“改实现” 有的框架在“改代码结构”

而 MyBatisGX,只是在“改定义”。