1. 项目背景与核心需求
在日常开发中,我们经常遇到两种典型的数据处理场景:一是数据库一对多关系的映射,二是逗号分隔字符串与Java集合类型的转换。MyBatisPlus作为MyBatis的增强工具,虽然简化了很多CRUD操作,但在处理这两种场景时仍需要开发者掌握特定的实现技巧。
最近在重构一个老项目时,我遇到了一个典型的案例:用户表(user)和订单表(order)存在一对多关系,同时用户有一个tags字段以逗号分隔存储标签。前端需要获取用户信息时,期望tags字段能自动转换为List
2. MyBatisPlus一对多关系实现
2.1 基础表结构设计
先来看数据库表结构设计。以用户-订单为例:
sql复制CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
tags VARCHAR(255) -- 逗号分隔的标签字符串
);
CREATE TABLE order (
id BIGINT PRIMARY KEY,
user_id BIGINT,
order_no VARCHAR(50),
FOREIGN KEY (user_id) REFERENCES user(id)
);
2.2 实体类映射
在Java实体类中,我们使用@TableField注解处理一对多关系:
java复制@Data
@TableName("user")
public class User {
private Long id;
private String username;
@TableField(exist = false)
private List<Order> orders;
private String tags; // 原始字符串字段
@TableField(exist = false)
private List<String> tagList; // 转换后的集合
}
2.3 查询方式实现
MyBatisPlus提供了几种实现一对多查询的方法:
2.3.1 注解方式
java复制public interface UserMapper extends BaseMapper<User> {
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "orders", column = "id",
many = @Many(select = "com.example.mapper.OrderMapper.findByUserId"))
})
User findUserWithOrders(Long id);
}
2.3.2 XML方式
xml复制<resultMap id="userWithOrders" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<collection property="orders" ofType="Order"
select="com.example.mapper.OrderMapper.findByUserId" column="id"/>
</resultMap>
2.3.3 服务层组装
java复制public User getUserWithOrders(Long userId) {
User user = userMapper.selectById(userId);
if(user != null) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
user.setOrders(orderMapper.selectList(wrapper));
}
return user;
}
提示:对于大数据量场景,建议使用分页查询避免内存溢出。MyBatisPlus 3.4.0+版本支持的分页插件能很好地处理这种情况。
3. 逗号分隔字符串转Java集合
3.1 基础转换方法
最简单的处理方式是在业务层手动转换:
java复制public List<String> convertStringToList(String str) {
if(StringUtils.isBlank(str)) {
return Collections.emptyList();
}
return Arrays.asList(str.split(","));
}
3.2 使用TypeHandler自动转换
更优雅的方式是实现自定义TypeHandler:
java复制@MappedTypes(List.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringToListTypeHandler extends BaseTypeHandler<List<String>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
List<String> parameter, JdbcType jdbcType) {
ps.setString(i, String.join(",", parameter));
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) {
String value = rs.getString(columnName);
return convert(value);
}
// 其他重载方法...
private List<String> convert(String value) {
if(value == null || value.trim().isEmpty()) {
return new ArrayList<>();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
3.3 配置TypeHandler
在MyBatisPlus中配置TypeHandler有多种方式:
3.3.1 字段级别配置
java复制@TableField(typeHandler = StringToListTypeHandler.class)
private List<String> tagList;
3.3.2 全局配置
在application.yml中:
yaml复制mybatis-plus:
type-handlers-package: com.example.handler
3.4 处理空值和去重
实际项目中还需要考虑更多细节:
java复制private List<String> convert(String value) {
if(value == null || value.trim().isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.distinct() // 去重
.collect(Collectors.toCollection(LinkedList::new)); // 保持顺序
}
4. 性能优化与常见问题
4.1 一对多查询的N+1问题
使用MyBatis的延迟加载可以缓解这个问题:
yaml复制mybatis-plus:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: false
或者在XML映射中使用collection的fetchType="lazy"属性。
4.2 字符串分割性能
对于高频调用且可能很长的字符串,可以考虑预编译正则表达式:
java复制private static final Pattern SPLIT_PATTERN = Pattern.compile(",");
public List<String> fastSplit(String input) {
return SPLIT_PATTERN.splitAsStream(input)
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
4.3 事务边界问题
当一对多操作涉及多个表的修改时,务必确保在服务层添加事务注解:
java复制@Transactional
public void updateUserWithOrders(User user, List<Order> orders) {
// 更新用户
userMapper.updateById(user);
// 先删除旧订单
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", user.getId());
orderMapper.delete(wrapper);
// 插入新订单
orders.forEach(order -> {
order.setUserId(user.getId());
orderMapper.insert(order);
});
}
4.4 缓存一致性
当使用TypeHandler转换字段时,需要注意MyBatis二级缓存的问题。如果修改了TypeHandler的逻辑,可能需要手动清除缓存:
java复制@CacheNamespace(flushInterval = 60000)
public interface UserMapper extends BaseMapper<User> {
@Override
@Options(flushCache = Options.FlushCachePolicy.TRUE)
int updateById(@Param(Constants.ENTITY) User entity);
}
5. 实际应用案例
5.1 用户标签管理系统
假设我们需要实现一个用户标签管理系统,用户可以拥有多个标签,标签以逗号分隔存储在数据库中:
java复制public void addUserTag(Long userId, String newTag) {
User user = userMapper.selectById(userId);
List<String> tags = stringToListTypeHandler.convert(user.getTags());
if(!tags.contains(newTag)) {
tags.add(newTag);
user.setTags(String.join(",", tags));
userMapper.updateById(user);
}
}
5.2 订单批量查询优化
对于需要查询用户及其所有订单的场景,可以使用MyBatisPlus的LambdaQueryWrapper:
java复制public User getUserWithOrders(Long userId) {
User user = userMapper.selectById(userId);
if(user != null) {
List<Order> orders = orderMapper.selectList(
new LambdaQueryWrapper<Order>()
.eq(Order::getUserId, userId)
.orderByDesc(Order::getCreateTime)
);
user.setOrders(orders);
}
return user;
}
5.3 复杂条件查询
结合一对多关系和字符串转换,实现复杂查询:
java复制public List<User> findUsersByTag(String tag) {
return userMapper.selectList(new LambdaQueryWrapper<User>()
.apply("FIND_IN_SET({0}, tags)", tag)
.last("LIMIT 100"));
}
6. 测试与验证
6.1 TypeHandler单元测试
java复制@Test
public void testStringToListTypeHandler() {
StringToListTypeHandler handler = new StringToListTypeHandler();
// 测试正常情况
List<String> result = handler.convert("java,spring,mybatis");
assertEquals(3, result.size());
// 测试空值
assertTrue(handler.convert(null).isEmpty());
assertTrue(handler.convert("").isEmpty());
// 测试前后空格
result = handler.convert(" a , b , c ");
assertEquals("a", result.get(0));
}
6.2 一对多查询集成测试
java复制@Test
@Transactional
public void testUserWithOrders() {
// 准备测试数据
User user = new User();
user.setUsername("test");
userMapper.insert(user);
Order order1 = new Order();
order1.setUserId(user.getId());
order1.setOrderNo("NO001");
orderMapper.insert(order1);
// 执行查询
User result = userService.getUserWithOrders(user.getId());
// 验证结果
assertNotNull(result.getOrders());
assertEquals(1, result.getOrders().size());
assertEquals("NO001", result.getOrders().get(0).getOrderNo());
}
7. 扩展思考
7.1 JSON格式存储方案
除了逗号分隔,也可以考虑使用JSON格式存储集合数据。MyBatisPlus提供了通用的JacksonTypeHandler:
java复制@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> tags;
7.2 多对多关系处理
对于更复杂的多对多关系,可以通过中间表实现:
sql复制CREATE TABLE user_tag (
user_id BIGINT,
tag_id BIGINT,
PRIMARY KEY (user_id, tag_id)
);
在Java实体中使用@TableField(exist = false)标记关联集合。
7.3 自定义注解简化配置
可以创建自定义注解来简化TypeHandler的配置:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@TableField(typeHandler = StringToListTypeHandler.class)
public @interface CommaSeparatedList {
}
然后在实体类中使用:
java复制@CommaSeparatedList
private List<String> tags;
8. 最佳实践总结
经过多个项目的实践,我总结了以下几点经验:
- 对于简单的一对多关系,服务层组装是最灵活的方式
- 当关联数据量大时,务必使用分页查询避免内存问题
- 字符串分割要考虑空值、去重和性能问题
- TypeHandler的实现要考虑线程安全性
- 事务边界要明确,避免部分成功的情况
- 对于频繁变更的标签系统,考虑使用专门的关联表而非字符串存储
在最近的一个电商项目中,我们最初使用逗号分隔存储用户权限,随着业务复杂度的增加,最终重构为专门的权限关联表。这个经验告诉我们:在项目初期就要根据业务发展预期选择合适的技术方案。
