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 指定主键生成策略。字段名自动按驼峰规则映射到数据库下划线列名(如 accountNoaccount_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 接口和 XML
  • ServiceImpl 提供 saveBatchupdateBatchById 等批量操作
  • LambdaQueryWrapper 提供类型安全的条件构造,替代 <if> 动态 SQL

对于新项目,推荐直接使用 MyBatis-Plus;对于老项目,可以渐进式引入,在新模块中使用 MP,老模块保持 XML 不变。

posted @ 2026-03-27 11:28  cwp0  阅读(9)  评论(0)    收藏  举报