1. 为什么选择Mybatis-Flex?
在Java持久层框架的选择上,MyBatis一直以其灵活性和对SQL的精准控制著称。但原生MyBatis在面对复杂业务场景时,往往需要开发者编写大量重复的XML或注解SQL。这正是Mybatis-Flex诞生的背景——它在保留MyBatis所有优点的同时,通过创新的APT(Annotation Processing Tool)技术,在编译期生成样板代码,让开发者获得接近JPA的开发体验,同时不损失MyBatis的执行效率。
我最初接触Mybatis-Flex是在一个需要快速迭代的电商项目中。当时团队在MyBatis-Plus和JPA之间犹豫不决:MyBatis-Plus的Wrapper虽然强大但学习曲线陡峭,JPA的Hibernate在复杂查询时又显得力不从心。Mybatis-Flex的"QueryWrapper"设计让我眼前一亮——它既保持了类似MyBatis-Plus的链式调用风格,又通过更智能的代码生成减少了80%以上的重复CRUD代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与基础配置
2.1 依赖引入与初始化
在Spring Boot项目中集成Mybatis-Flex只需两步。首先在pom.xml中添加核心依赖(注意要同时引入mybatis-spring-boot-starter):
xml复制<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
然后配置数据源和Mybatis-Flex的全局设置。这里有个容易被忽略的关键点:需要在application.yml中显式启用APT代码生成:
yaml复制mybatis-flex:
codegen:
enable: true
base-package: com.example.entity # 实体类所在包
警告:如果不开启codegen.enable,后续的@Table注解和QueryWrapper将无法正常工作。这是新手最常踩的坑之一。
2.2 实体类与Mapper设计
与传统MyBatis不同,Mybatis-Flex要求实体类必须使用@Table注解标记。以下是一个用户实体的完整定义:
java复制@Table("sys_user")
public class User {
@Id(keyType = KeyType.Auto)
private Long id;
@Column("username")
private String name;
@Column(ignore = true) // 该字段不参与数据库操作
private String tempToken;
// Getter/Setter省略
}
Mapper接口可以继承FlexBaseMapper获得基础CRUD能力:
java复制public interface UserMapper extends FlexBaseMapper<User> {
// 自定义SQL方法
@Select("SELECT * FROM sys_user WHERE status = #{status}")
List<User> selectByStatus(@Param("status") int status);
}
3. 核心功能实战解析
3.1 QueryWrapper的魔法
Mybatis-Flex最强大的特性莫过于其QueryWrapper。与MyBatis-Plus的Wrapper相比,它的链式调用更加符合直觉。比如这个多条件分页查询:
java复制QueryWrapper query = QueryWrapper.create()
.select(USER.ALL_COLUMNS)
.from(USER)
.where(USER.AGE.ge(18))
.and(USER.USERNAME.like("张%"))
.orderBy(USER.CREATE_TIME.desc())
.limit(10, 20); // 第2页,每页20条
List<User> users = userMapper.selectListByQuery(query);
这里有几个值得注意的技术细节:
USER是编译期生成的常量类,完全类型安全- 条件方法如
ge()(greater equals)、like()等直观易用 - 不需要手动写
WHERE 1=1,框架会自动处理空条件
3.2 关联查询的优雅实现
复杂关联查询一直是ORM的痛点。Mybatis-Flex提供了两种解决方案。第一种是通过QueryWrapper实现:
java复制QueryWrapper query = QueryWrapper.create()
.select(USER.USERNAME, ORDER.ORDER_NO)
.from(USER.as("u"))
.leftJoin(ORDER).on(USER.ID.eq(ORDER.USER_ID))
.where(USER.STATUS.eq(1));
List<Map<String, Object>> list = userMapper.selectMapsByQuery(query);
第二种更推荐的方式是使用Relation注解定义实体关联:
java复制@Table("sys_order")
public class Order {
@Column("user_id")
private Long userId;
@Relation(
target = User.class,
targetField = "id",
value = "user_id"
)
private User user;
}
查询时通过withRelations()方法自动加载关联对象:
java复制Order order = orderMapper.selectOneWithRelationsById(1);
System.out.println(order.getUser().getName()); // 自动关联查询
4. 高级特性与性能优化
4.1 多租户实现方案
在SAAS系统中,Mybatis-Flex的多租户支持堪称优雅。只需实现TenantFactory接口:
java复制@Component
public class MyTenantFactory implements TenantFactory {
@Override
public String getTenantId() {
return SecurityUtils.getCurrentTenantId();
}
}
然后在配置中启用租户过滤:
yaml复制mybatis-flex:
tenant:
enable: true
tables: sys_user, sys_order # 需要过滤的表
所有相关查询会自动附加tenant_id = ?条件,完全无侵入。
4.2 逻辑删除的陷阱与规避
逻辑删除是另一个常用功能,但有些细节需要注意:
java复制@Table("sys_user")
public class User {
@Column(isLogicDelete = true)
private Boolean deleted;
}
默认情况下,查询会自动过滤deleted=true的记录。但在以下场景需要特别注意:
- 使用原生SQL时需要手动添加条件
- 联表查询时每个表都需要处理逻辑删除字段
- 更新操作不会自动过滤已删除记录
建议在配置中添加全局检查:
yaml复制mybatis-flex:
global-config:
logic-delete:
check-update: true # 更新时检查记录是否已被删除
4.3 性能调优实战
在大数据量场景下,我总结出几个关键优化点:
- 批量插入优化:
java复制List<User> users = ...;
userMapper.insertBatch(users); // 默认每批1000条
// 更优方案:开启rewriteBatchedStatements
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://...?rewriteBatchedStatements=true");
return ds;
}
- 查询字段精简:
java复制// 反例:查询所有字段
QueryWrapper.create().select(USER.ALL_COLUMNS)...
// 正例:只查询必要字段
QueryWrapper.create().select(USER.ID, USER.NAME)...
- 避免N+1查询:
java复制// 反例:循环中查询关联数据
List<Order> orders = orderMapper.selectAll();
orders.forEach(o -> {
User u = userMapper.selectById(o.getUserId());
o.setUser(u);
});
// 正例:一次性加载关联
List<Order> orders = orderMapper.selectAllWithRelations();
5. 生产环境踩坑记录
5.1 版本升级的兼容性问题
在从1.1.x升级到1.2.x时,我们遇到了QueryWrapper的API变更。原来的eq()方法被拆分为eqValue()和eqCondition(),导致大量代码报错。解决方案是:
- 先在测试环境运行代码扫描工具检测不兼容API
- 使用IDE的全局替换功能批量修改:
eq(...)→eqValue(...)eq(condition, ...)→eqCondition(condition, ...)
- 特别注意动态SQL中的条件判断
5.2 分布式ID冲突问题
在使用@Id(keyType = KeyType.Auto)时,多个实例同时插入可能导致ID冲突。我们的最终方案是:
java复制@Id(keyType = KeyType.Generator, value = "snowFlakeId")
private Long id;
// 配置ID生成器
@Bean
public IdGenerator snowFlakeId() {
return new SnowFlakeIdGenerator(1L); // workerId根据实例区分
}
5.3 复杂事务下的异常处理
在涉及多表操作的事务中,我们发现某些异常没有被正确回滚。根本原因是Mybatis-Flex的部分操作会创建新SqlSession。解决方案是:
- 显式指定事务管理器:
java复制@Transactional(transactionManager = "transactionManager")
public void complexOperation() {
// ...
}
- 或者在配置中强制使用同一事务:
yaml复制mybatis-flex:
global-config:
keep-alive-sql-session: true
6. 监控与扩展开发
6.1 SQL监控方案
我们基于Mybatis的Interceptor实现了慢SQL监控:
java复制@Intercepts(@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class SlowSqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long cost = System.currentTimeMillis() - start;
if (cost > 1000) { // 超过1秒视为慢查询
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
log.warn("Slow SQL detected: {}ms - {}", cost, ms.getBoundSql());
}
return result;
}
}
6.2 自定义类型处理器
处理JSON字段的典型场景:
java复制public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private final Class<T> type;
public JsonTypeHandler(Class<T> type) {
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, JSON.toJSONString(parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return JSON.parseObject(json, type);
}
}
在实体字段上使用:
java复制@Column(typeHandler = JsonTypeHandler.class)
private List<String> tags;
6.3 动态表名策略
在分表场景下,可以通过实现TableNameProcessor接口实现动态表名:
java复制@Component
public class MonthTableProcessor implements TableNameProcessor {
@Override
public String process(String tableName, Object entity) {
if (entity instanceof LogEntity) {
return tableName + "_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
}
return tableName;
}
}
7. 测试策略与持续集成
7.1 单元测试最佳实践
我们采用H2内存数据库进行快速测试:
java复制@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
"spring.datasource.driver-class-name=org.h2.Driver"
})
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
@Transactional
@Rollback
public void testInsert() {
User user = new User();
user.setName("test");
assertThat(userMapper.insert(user)).isEqualTo(1);
assertThat(user.getId()).isNotNull();
}
}
7.2 集成测试数据准备
使用DBUnit准备测试数据:
java复制@DataSet(value = "user.yml", cleanBefore = true)
public class UserServiceIT {
@Test
public void testComplexQuery() {
// 测试逻辑
}
}
user.yml示例:
yaml复制sys_user:
- id: 1
username: "user1"
age: 20
- id: 2
username: "user2"
age: 25
8. 架构设计建议
8.1 分层架构实践
我们推荐的分层方案:
code复制- controller
- dto (请求/响应对象)
- service
- bo (业务对象)
- repository
- entity (持久化实体)
- mapper (Mybatis-Flex接口)
- converter (实体转换器)
转换器示例:
java复制public class UserConverter {
public static UserBO toBO(User entity) {
if (entity == null) return null;
UserBO bo = new UserBO();
BeanUtils.copyProperties(entity, bo);
return bo;
}
}
8.2 复杂查询的治理方案
对于复杂查询,我们建立了一套规范:
- 简单查询:使用QueryWrapper链式调用
- 中等复杂度查询:@Select注解SQL
- 复杂查询:XML映射文件+动态SQL
- 特别复杂的报表查询:直接使用JdbcTemplate
每个查询方法必须添加@Query注解说明:
java复制@Query(type = QueryType.READ, desc = "根据状态分页查询用户")
List<User> selectByStatus(@Param("status") int status);
9. 未来演进方向
虽然Mybatis-Flex已经非常强大,但在实际项目中我们发现几个可以进一步优化的方向:
- 更智能的APT生成:目前对复杂继承结构的实体类支持有限
- 增强的分布式事务支持:与Seata等框架的深度集成
- 更丰富的监控指标:比如连接池使用情况、缓存命中率等
- Kotlin DSL支持:提供更符合Kotlin风格的查询API
我们团队已经向社区贡献了部分改进,这也是开源项目的魅力所在——每个使用者都可以成为贡献者。
