MyBatis-vs-MyBatisPlus
MyBatis 与 MyBatis-Plus 深度对比:从传统 XML 到自动化 ORM
在 Java 后端开发中,MyBatis 是最主流的持久层框架之一。而 MyBatis-Plus(简称 MP)作为 MyBatis 的增强工具,在保留 MyBatis 全部特性的基础上,大幅简化了 CRUD 操作。本文将从实际项目出发,详细对比两者的区别。
一、整体架构对比
| 维度 | MyBatis | MyBatis-Plus |
|---|---|---|
| SQL 定义 | 手写 XML 或注解 | 通用 CRUD 自动生成,复杂 SQL 仍可手写 |
| 表名映射 | XML 中写死 | @TableName 注解声明 |
| 字段映射 | XML 中逐个 #{} 绑定 |
自动根据字段名驼峰转下划线 |
| 批量操作 | 需手写 <foreach> |
内置 saveBatch()、updateBatchById() |
| 条件构造 | 手写 SQL 拼接或动态 SQL 标签 | Lambda 链式条件构造器 |
| 分页 | 需手写 LIMIT 或集成分页插件 | 内置分页插件,开箱即用 |
| 代码量 | 较多(Mapper 接口 + XML) | 较少(继承 BaseMapper 即可) |
二、项目结构对比
MyBatis 传统方式
├── mapper/
│ ├── UserMapper.java # Mapper 接口
│ └── UserMapper.xml # SQL 映射文件
├── domain/
│ └── User.java # 实体类
MyBatis-Plus 方式
├── mapper/
│ └── UserMapper.java # 继承 BaseMapper,无需 XML
├── persistobject/
│ └── UserPo.java # 持久化对象(带 @TableName 注解)
├── repository/
│ └── UserRepositoryImpl.java # 继承 ServiceImpl,封装业务逻辑
三、代码示例对比
以一个「用户银行账户」的增删改查为例,对比两种方式的写法。
3.1 实体类定义
MyBatis 传统方式 — 普通 POJO,无特殊注解:
@Data
public class BankAccount {
private Long id;
private String userId;
private String bankName;
private String accountNo;
private String accountHolder;
private String status;
private Date gmtCreate;
private Date gmtModified;
}
MyBatis-Plus 方式 — 使用注解声明表名和主键策略:
@Data
@TableName(value = "bank_account", autoResultMap = true)
public class BankAccountPo {
@TableId(type = IdType.INPUT)
private Long id;
private String userId;
private String bankName;
private String accountNo;
private String accountHolder;
private String status;
private Date gmtCreate;
private Date gmtModified;
}
关键区别:MyBatis-Plus 通过
@TableName指定表名,@TableId指定主键生成策略。字段名自动按驼峰规则映射到数据库下划线列名(如accountNo→account_no)。
3.2 Mapper 接口
MyBatis 传统方式 — 需要为每个操作声明方法:
public interface BankAccountMapper {
int insert(BankAccount record);
int deleteByUserId(@Param("userId") String userId);
List<BankAccount> selectByUserId(@Param("userId") String userId);
int updateById(BankAccount record);
}
MyBatis-Plus 方式 — 继承 BaseMapper,通用 CRUD 方法开箱即用:
public interface BankAccountMapper extends BaseMapper<BankAccountPo> {
// 无需声明任何方法!
// BaseMapper 已内置:insert、deleteById、selectById、updateById 等 17+ 个方法
}
3.3 SQL 映射
MyBatis 传统方式 — 需要手写完整的 XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.BankAccountMapper">
<resultMap id="BaseResultMap" type="com.example.domain.BankAccount">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="VARCHAR"/>
<result column="bank_name" property="bankName" jdbcType="VARCHAR"/>
<result column="account_no" property="accountNo" jdbcType="VARCHAR"/>
<result column="account_holder" property="accountHolder" jdbcType="VARCHAR"/>
<result column="status" property="status" jdbcType="VARCHAR"/>
<result column="gmt_create" property="gmtCreate" jdbcType="TIMESTAMP"/>
<result column="gmt_modified" property="gmtModified" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, user_id, bank_name, account_no, account_holder,
status, gmt_create, gmt_modified
</sql>
<insert id="insert" parameterType="com.example.domain.BankAccount">
INSERT INTO bank_account (
id, user_id, bank_name, account_no, account_holder,
status, gmt_create, gmt_modified
) VALUES (
#{id}, #{userId}, #{bankName}, #{accountNo}, #{accountHolder},
#{status}, #{gmtCreate}, #{gmtModified}
)
</insert>
<delete id="deleteByUserId">
DELETE FROM bank_account WHERE user_id = #{userId}
</delete>
<select id="selectByUserId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM bank_account
WHERE user_id = #{userId}
</select>
<update id="updateById" parameterType="com.example.domain.BankAccount">
UPDATE bank_account
SET bank_name = #{bankName},
account_no = #{accountNo},
account_holder = #{accountHolder},
status = #{status},
gmt_modified = #{gmtModified}
WHERE id = #{id}
</update>
</mapper>
MyBatis-Plus 方式 — 完全不需要 XML,以上所有操作自动生成!
3.4 Service 层 / Repository 层
MyBatis 传统方式 — 直接注入 Mapper 调用:
@Service
public class BankAccountService {
@Autowired
private BankAccountMapper bankAccountMapper;
public void saveBankAccounts(List<BankAccount> accounts) {
// 需要自己循环插入,或在 XML 中写 <foreach> 批量插入
for (BankAccount account : accounts) {
bankAccountMapper.insert(account);
}
}
public void deleteByUserId(String userId) {
bankAccountMapper.deleteByUserId(userId);
}
public List<BankAccount> findByUserId(String userId) {
return bankAccountMapper.selectByUserId(userId);
}
}
MyBatis-Plus 方式 — 继承 ServiceImpl,获得丰富的内置方法:
@Repository
public class BankAccountRepositoryImpl
extends ServiceImpl<BankAccountMapper, BankAccountPo>
implements BankAccountRepository {
@Override
public void save(List<BankAccount> accountList) {
// 领域对象 → 持久化对象(通过 Converter 转换)
List<BankAccountPo> poList = BankAccountConverter.INSTANCE.reverse(accountList);
// 内置批量保存,自动分批提交,无需手写循环或 <foreach>
saveBatch(poList);
}
@Override
public void deleteByUserId(String userId) {
// Lambda 条件构造器,类型安全,避免硬编码字段名字符串
this.remove(
Wrappers.<BankAccountPo>lambdaQuery()
.eq(BankAccountPo::getUserId, userId)
);
}
@Override
public List<BankAccount> findByUserId(String userId) {
List<BankAccountPo> poList = this.list(
Wrappers.<BankAccountPo>lambdaQuery()
.eq(BankAccountPo::getUserId, userId)
);
return BankAccountConverter.INSTANCE.convert(poList);
}
}
3.5 条件查询对比
MyBatis 传统方式 — 动态 SQL 需要用 <if> 标签:
<select id="selectByCondition" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM bank_account
<where>
<if test="userId != null and userId != ''">
AND user_id = #{userId}
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="bankName != null and bankName != ''">
AND bank_name LIKE CONCAT('%', #{bankName}, '%')
</if>
</where>
ORDER BY gmt_create DESC
</select>
MyBatis-Plus 方式 — Lambda 链式构造,代码即 SQL:
public List<BankAccount> findByCondition(String userId, String status, String bankName) {
LambdaQueryWrapper<BankAccountPo> wrapper = Wrappers.<BankAccountPo>lambdaQuery()
.eq(StrUtil.isNotBlank(userId), BankAccountPo::getUserId, userId)
.eq(StrUtil.isNotBlank(status), BankAccountPo::getStatus, status)
.like(StrUtil.isNotBlank(bankName), BankAccountPo::getBankName, bankName)
.orderByDesc(BankAccountPo::getGmtCreate);
List<BankAccountPo> poList = this.list(wrapper);
return BankAccountConverter.INSTANCE.convert(poList);
}
优势:条件构造器的第一个参数是
boolean condition,为false时自动跳过该条件,等价于 XML 中的<if>标签,但更简洁、类型安全。
四、批量操作对比
这是两者差异最大的地方之一。
MyBatis 传统方式 — 需要手写 <foreach>:
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO bank_account (id, user_id, bank_name, account_no)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.id}, #{item.userId}, #{item.bankName}, #{item.accountNo})
</foreach>
</insert>
MyBatis-Plus 方式 — 一行搞定:
// 默认每批 1000 条,自动分批提交
saveBatch(poList);
// 也可以自定义批次大小
saveBatch(poList, 500);
五、需要注意的"坑"
5.1 字段缺失问题
MyBatis-Plus 的 saveBatch 只会处理 PO 类中声明的字段。如果数据库表中有字段但 PO 类中没有定义,INSERT 时该列会被忽略,使用数据库默认值(通常为 NULL)。
例如,数据库表 bank_account 有 15 个字段,但 BankAccountPo 只定义了 8 个字段,那么剩余 7 个字段在通过 MyBatis-Plus 写入时不会被赋值。
这在新老系统并存时尤其需要注意:老系统的 XML 可能写入了所有字段,而新系统的 PO 类可能遗漏了部分字段。
5.2 主键策略
// INPUT:由应用程序自行设置 ID
@TableId(type = IdType.INPUT)
private Long id;
// AUTO:使用数据库自增
@TableId(type = IdType.AUTO)
private Long id;
// ASSIGN_ID:MyBatis-Plus 内置雪花算法(默认)
@TableId(type = IdType.ASSIGN_ID)
private Long id;
5.3 逻辑删除
MyBatis-Plus 支持通过 @TableLogic 注解实现逻辑删除,调用 remove() 时自动转为 UPDATE:
@TableLogic
private String isDeleted; // "Y" 表示已删除,"N" 表示未删除
配置后,this.remove(wrapper) 实际执行的 SQL 是:
UPDATE bank_account SET is_deleted = 'Y' WHERE ...
六、如何选择?
| 场景 | 推荐 |
|---|---|
| 简单 CRUD、快速开发 | MyBatis-Plus — 零 XML,效率高 |
| 复杂多表关联查询 | MyBatis XML — 灵活度更高 |
| 需要精细控制 SQL 性能 | MyBatis XML — 可手动优化 |
| 新项目、微服务 | MyBatis-Plus — 开发效率优先 |
| 老项目维护 | 保持现有方式,避免混用带来的认知负担 |
最佳实践:MyBatis-Plus 并不排斥 XML。在同一个项目中,简单 CRUD 用 MP 自动生成,复杂查询仍然可以写 XML,两者完全兼容。
七、总结
MyBatis-Plus 的核心理念是 "只做增强不做改变",它通过以下方式提升开发效率:
@TableName+@TableId替代 XML 中的<resultMap>和表名硬编码BaseMapper内置 17+ 通用方法,替代手写 Mapper 接口和 XMLServiceImpl提供saveBatch、updateBatchById等批量操作LambdaQueryWrapper提供类型安全的条件构造,替代<if>动态 SQL
对于新项目,推荐直接使用 MyBatis-Plus;对于老项目,可以渐进式引入,在新模块中使用 MP,老模块保持 XML 不变。

浙公网安备 33010602011771号