MyBatis-Flex:让 MyBatis 真正“柔”起来,告别 XML 的“铁镣铐”

4 阅读8分钟

MyBatis-Flex:让 MyBatis 真正“柔”起来,告别 XML 的“铁镣铐”!🚀💪

兄弟们,用过 MyBatis 吗?肯定用过!那你一定经历过这样的痛苦:写个简单查询,却要在 XML 和 Java 代码间反复横跳;改个字段名,要改实体类、Mapper、XML 三处;想用点高级特性,XML 配置能写到你怀疑人生。😫

MyBatis-Flex​ 来了!它不是一个新 ORM,而是 MyBatis 的“超级增强版”。如果说 MyBatis 是个稳重的中年人,那 MyBatis-Flex 就是个会街舞的年轻人——更灵活、更轻量、性能更高,关键是:彻底解放生产力!


一、MyBatis-Flex 是啥?—— MyBatis 的“Pro Max Ultra”版 📱➡️🚀

先看个对比:

传统 MyBatis

// 1. UserMapper.java
User selectById(@Param("id") Long id);

// 2. UserMapper.xml(写了100行)
<select id="selectById" resultMap="userMap">
    SELECT * FROM user WHERE id = #{id}
</select>
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <!-- 还有20个字段要配... -->
</resultMap>

// 3. 调用
User user = userMapper.selectById(1L);

MyBatis-Flex

// 1. 就这一行!😎
User user = userMapper.selectOneById(1L);

// 2. 或者用QueryWrapper
User user = userMapper.selectOneByQuery(
    QueryWrapper.create()
        .where(User::getId).eq(1L)
        .and(User::getStatus).eq(1)
);

所以,MyBatis-Flex 是什么?

它是一个构建在 MyBatis 之上的、高度灵活的 ORM 框架。核心特点:

  • 无 XML:所有 SQL 通过 Java API 动态构建
  • APT 自动生成:编译时生成实体类、Mapper、TableDef
  • 极致性能:比 MyBatis-Plus 快 5~10 倍(官方数据)
  • 轻量:只有 500KB,无第三方依赖

二、为什么需要 MyBatis-Flex?—— 当 MyBatis 遇上“中年危机” 🧓➡️🕺

MyBatis 的痛点:

  1. XML 地狱:简单项目还好,复杂项目 XML 文件比代码还多
  2. 维护困难:字段改了要改三处,容易遗漏
  3. 动态 SQL 难写<if>``<choose>标签嵌套到眼花
  4. 代码生成器弱:生成的代码“傻大粗”,不好定制
  5. 性能有损耗:反射多,XML 解析有开销

MyBatis-Plus 的不足:

  • 虽然解决了 XML 问题,但设计复杂,学习曲线陡
  • 性能有损耗:反射+缓存,大型项目明显
  • 不够灵活:复杂查询支持有限

MyBatis-Flex 的解决方案:

  1. APT 代码生成:编译时生成,零运行时反射
  2. QueryWrapper 极致灵活:链式调用,支持任意复杂查询
  3. 多租户、数据权限:原生支持,配置简单
  4. 字段加密/脱敏:注解搞定,业务无感知
  5. SQL 审计:自动记录,方便排查

核心思想“约定大于配置,但绝不限制灵活性”


三、核心特性“炫技”时刻 ✨

特性1:APT 自动生成(编译时魔法)🧙♂️

传统代码生成器:运行时生成,用反射,性能差

MyBatis-Flex 的 APT:编译时生成,像 Lombok 一样

// 1. 实体类(你写的)
@Table("user")
public class User {
    @Id(keyType = KeyType.Auto)
    private Long id;
    
    @Column("user_name")
    private String name;
    
    private Integer age;
    
    // getter/setter
}

// 2. 编译时自动生成(你看不到但存在)
public class Tables {
    public static final UserTable USER = new UserTable();
    
    public static class UserTable extends TableDef {
        public final Column ID = new Column(this, "id");
        public final Column NAME = new Column(this, "user_name");
        public final Column AGE = new Column(this, "age");
        
        public UserTable() {
            super("user");
        }
    }
}

优势

  • 零反射UserTable.NAME是编译时常量,不是字符串!
  • 类型安全:编译时检查字段名是否正确
  • IDE 提示:自动补全,不会写错字段名

特性2:QueryWrapper 的“七十二变” 🎭

基础查询:
// 1. 等值查询
QueryWrapper query = QueryWrapper.create()
    .where(User::getId).eq(1L)
    .and(User::getAge).ge(18)
    .and(User::getName).like("张%");

// 2. 动态条件(比MyBatis的<if>优雅多了!)
QueryWrapper query = QueryWrapper.create()
    .where(Account::getId).ge(1)
    .when(age != null, qw -> qw.and(Account::getAge).eq(age))
    .when(name != null, qw -> qw.and(Account::getName).like(name));

// 3. 复杂嵌套
QueryWrapper query = QueryWrapper.create()
    .where(User::getId).in(
        select("user_id").from("order").where("amount > 1000")
    )
    .and(qw -> qw
        .where(User::getStatus).eq(1)
        .or(User::getVip).eq(1)
    );
联表查询(不用写XML!):
// 用户和部门联表
QueryWrapper query = QueryWrapper.create()
    .select(USER.ALL_COLUMNS, DEPT.NAME.as("deptName"))
    .from(USER)
    .leftJoin(DEPT).on(USER.DEPT_ID.eq(DEPT.ID))
    .where(USER.STATUS.eq(1))
    .orderBy(USER.CREATE_TIME.desc());

List<UserDTO> list = userMapper.selectListByQueryAs(query, UserDTO.class);
分组聚合:
QueryWrapper query = QueryWrapper.create()
    .select(
        USER.GENDER,
        count().as("total"),
        avg(USER.AGE).as("avgAge")
    )
    .from(USER)
    .groupBy(USER.GENDER)
    .having(count().gt(10));

特性3:多租户支持(一行配置)🏢

@Configuration
public class MyBatisFlexConfig {
    
    @Bean
    public TenantFactory tenantFactory() {
        return new TenantFactory() {
            @Override
            public Expression getTenantId() {
                // 从ThreadLocal或SecurityContext获取租户ID
                return TenantManager.getTenantId();
            }
            
            @Override
            public String getTenantIdColumn() {
                return "tenant_id";  // 租户字段名
            }
        };
    }
}

// 使用时自动过滤
List<User> users = userMapper.selectAll();  
// 实际SQL:SELECT * FROM user WHERE tenant_id = 'current_tenant'

特性4:数据权限(注解驱动)🔐

@Table("order")
public class Order {
    // 字段省略
}

// Service层
@Service
public class OrderService {
    
    // 自动添加数据权限条件
    // 如:AND (creator_id = ? OR dept_id IN (?))
    @DataPermission(
        type = DataPermissionType.USER_AND_DEPT,
        userField = "creator_id",
        deptField = "dept_id"
    )
    public List<Order> getUserOrders() {
        return orderMapper.selectAll();
    }
}

特性5:字段加密/脱敏 🎭

@Table("user")
public class User {
    
    @Column("phone")
    @ColumnEncrypt("myEncryptor")  // 存储时自动加密
    private String phone;
    
    @Column("id_card")
    @ColumnMask(MaskType.ID_CARD)  // 查询时自动脱敏:510**********1234
    private String idCard;
}

// 插入时自动加密
user.setPhone("13800138000");
userMapper.insert(user);  
// 数据库存的是:U2FsdGVkX1+2pGz3V4vI7R5g...

// 查询时自动解密+脱敏
User user = userMapper.selectOneById(1L);
System.out.println(user.getPhone());  // 13800138000(自动解密)
System.out.println(user.getIdCard()); // 510**********1234(自动脱敏)

四、性能对比:真的能“起飞”吗?✈️

官方基准测试(查询 10000 次):

MyBatis: 基础线
MyBatis-Plus: 慢 20%~30%
MyBatis-Flex: 快 5~10 倍!

为什么快?

  1. 零反射:APT 生成,直接方法调用
  2. 无 XML 解析:SQL 动态构建,无解析开销
  3. 智能缓存:TableDef 编译时常量,永久缓存
  4. 最小化包装:QueryWrapper 最终生成最简 SQL

五、快速入门:5分钟上手 ⏰

步骤1:加依赖

<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-spring-boot-starter</artifactId>
    <version>1.6.5</version>
</dependency>

<!-- APT处理器 -->
<dependency>
    <groupId>com.mybatis-flex</groupId>
    <artifactId>mybatis-flex-processor</artifactId>
    <version>1.6.5</version>
    <scope>provided</scope>
</dependency>

步骤2:配置

# application.yml
mybatis-flex:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    # 启用APT
    processor:
      enable: true

步骤3:写实体类

@Table("user")
public class User {
    @Id(keyType = KeyType.Auto)
    private Long id;
    
    @Column("user_name")
    private String name;
    
    private Integer age;
    
    @Column(onInsertValue = "now()")
    private Date createTime;
    
    // getter/setter
}

步骤4:写 Mapper(可选,有通用 Mapper)

// 1. 继承 BaseMapper(已有CRUD)
public interface UserMapper extends BaseMapper<User> {
    // 自定义方法
    List<User> selectByName(@Param("name") String name);
}

// 2. 或者用 @Mapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

步骤5:开箱即用的 Service

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    // 1. 增删改查
    public void addUser(User user) {
        userMapper.insert(user);  // 自动回填ID
    }
    
    public User getUser(Long id) {
        return userMapper.selectOneById(id);
    }
    
    // 2. 复杂查询
    public List<User> findUsers(String name, Integer minAge) {
        return userMapper.selectListByQuery(
            QueryWrapper.create()
                .where(User::getName).like(name)
                .and(User::getAge).ge(minAge)
                .orderBy(User::getCreateTime.desc())
        );
    }
    
    // 3. 更新(只更新非null字段)
    public void updateUser(User user) {
        userMapper.update(user);  // 智能更新
    }
}

六、高级玩法:解放生产力的“骚操作” 🎩

玩法1:逻辑删除(一行注解)

@Table("user")
public class User {
    // 其他字段...
    
    @Column(isLogicDelete = true)
    private Boolean deleted;  // 1删除 0正常
    
    // 或时间戳
    // @Column(logicDelete = LogicDeleteType.DELETE_TIMESTAMP)
    // private Date deleteTime;
}

// 自动过滤已删除数据
List<User> users = userMapper.selectAll();
// SQL: SELECT * FROM user WHERE deleted = 0

玩法2:乐观锁(防并发更新)

@Table("product")
public class Product {
    @Version  // 乐观锁字段
    private Integer version;
    
    private Integer stock;
}

// 更新时自动检查版本
product.setStock(product.getStock() - 1);
productMapper.update(product);
// SQL: UPDATE product SET stock=?, version=? 
//      WHERE id=? AND version=?
// 返回影响行数=0表示被其他线程修改了

玩法3:多主键支持

@Table("user_role")
public class UserRole {
    @Id
    private Long userId;
    
    @Id  // 多个@Id表示联合主键
    private Long roleId;
    
    // 使用复合主键查询
    UserRole userRole = userRoleMapper.selectOneById(
        Id.ofKeys(1L, 2L)  // userId=1, roleId=2
    );
}

玩法4:SQL审计(记录所有操作)

@Configuration
public class AuditConfig {
    
    @Bean
    public SqlAudit sqlAudit() {
        return new SqlAudit() {
            @Override
            public void audit(SqlAuditInfo auditInfo) {
                // 记录到日志或数据库
                log.info("SQL: {}, 参数: {}, 耗时: {}ms",
                    auditInfo.getFullSql(),
                    auditInfo.getParameters(),
                    auditInfo.getElapsedTime()
                );
                
                // 慢SQL告警
                if (auditInfo.getElapsedTime() > 1000) {
                    alertSlowSql(auditInfo);
                }
            }
        };
    }
}

七、工作中的“防坑”指南 🕳️

坑1:APT 不生成代码

检查

  1. IDEA 中启用注解处理:Settings → Build → Annotation Processors
  2. 确保依赖中加了 mybatis-flex-processor
  3. 清理重新编译:mvn clean compile

坑2:字段名映射问题

// 错误:数据库是下划线,Java是驼峰
@Column("userName")  // 数据库字段是 user_name
private String userName;

// 正确1:指定column
@Column("user_name")
private String userName;

// 正确2:全局配置驼峰转换
mybatis-flex:
  global-config:
    column-to-property-style: camel

坑3:批量插入性能

// 错误:循环insert
for (User user : userList) {
    userMapper.insert(user);  // 每次都要获取连接
}

// 正确:批量插入
userMapper.insertBatch(userList);  // 一次插入,性能提升N倍

坑4:分页查询内存溢出

// 错误:一次性查全部
List<User> users = userMapper.selectAll();  // 100万数据?

// 正确:分页查询
Page<User> page = userMapper.paginate(
    Page.of(1, 20),  // 第1页,20条
    QueryWrapper.create().orderBy(User::getId.desc())
);

// 流式查询(大数据量)
try (Cursor<User> cursor = userMapper.selectCursorByQuery(
    QueryWrapper.create().orderBy(User::getId)
)) {
    cursor.forEach(user -> {
        // 处理
    });
}

坑5:事务管理

// 错误:多个操作没在事务中
public void updateUser(User user) {
    userMapper.update(user);
    logMapper.insert(log);  // 万一失败,user更新无法回滚
}

// 正确:添加@Transactional
@Transactional(rollbackFor = Exception.class)
public void updateUserWithLog(User user, Log log) {
    userMapper.update(user);
    logMapper.insert(log);
}

八、对比 MyBatis-Plus:我该选谁?🤔

特性MyBatis-FlexMyBatis-Plus
性能⭐⭐⭐⭐⭐(零反射,极致快)⭐⭐⭐(反射+缓存)
灵活性⭐⭐⭐⭐⭐(任意复杂查询)⭐⭐⭐(复杂查询受限)
学习成本⭐⭐⭐(简单直观)⭐⭐⭐⭐(概念多)
社区生态⭐⭐(较新,在增长)⭐⭐⭐⭐⭐(成熟)
代码生成⭐⭐⭐⭐⭐(APT,零反射)⭐⭐⭐(运行时生成)
多租户⭐⭐⭐⭐⭐(原生支持)⭐⭐⭐(需插件)
数据权限⭐⭐⭐⭐⭐(注解驱动)⭐⭐(需自定义)

选择建议

  • 新项目、追求性能:选 MyBatis-Flex
  • 老项目、求稳定:继续用 MyBatis-Plus
  • 复杂查询多:选 MyBatis-Flex(QueryWrapper 真香)
  • 需要丰富插件:选 MyBatis-Plus(生态成熟)

九、什么时候不用 MyBatis-Flex?🙅♂️

  1. 超级简单项目:JDBC Template 就够了
  2. 强依赖 MyBatis 插件生态:很多插件还没适配
  3. 团队不熟悉:学习新框架需要成本
  4. 需要 JPA 特性:用 Spring Data JPA
  5. 超大规模团队:MyBatis-Plus 的社区支持更稳

十、总结:让数据库操作真正“柔”起来 🌈

MyBatis-Flex 的核心价值就一句话: “用最少的代码,做最多的事,还跑得飞快!”

它解决了 MyBatis 系列框架的三大痛点:

  1. XML 配置繁琐​ → 全 Java API
  2. 性能不够极致​ → 零反射 + APT
  3. 功能不够灵活​ → 强大的 QueryWrapper

适合人群

  • 被 MyBatis XML 折磨过的开发者
  • 追求极致性能的架构师
  • 需要灵活数据权限的中台系统
  • 厌倦了反射的性能损耗

最后,下次当你又要写 MyBatis XML 时,不妨试试 MyBatis-Flex。你可能会发现,原来数据库操作可以这么“柔”、这么“顺滑”!🚀

记住:技术选型没有银弹,但 MyBatis-Flex 绝对是你 ORM 武器库中值得拥有的一把“瑞士军刀”!🔪✨