1. 初识Spring Boot与MyBatis联姻

老张最近在重构项目的用户模块时,发现原来的Hibernate用起来总有点水土不服。比如说,复杂的多表联查要写HQL总觉得不够直观,调整SQL性能优化时又像戴着脚镣跳舞。在技术选型会上,架构师小王力荐MyBatis与Spring Boot的黄金组合,于是我们开启了这段探索之旅。

MyBatis这个数据持久层框架最大的魅力在于它既保持SQL灵活性,又能让Java代码和SQL优雅解耦。配合Spring Boot的自动化配置,就像咖啡遇上伴侣,让原本繁琐的ORM配置变得清爽可口。举个例子,原本需要手动配置的数据源、事务管理现在都可以通过几行配置搞定。

2. 环境搭建与基础配置

2.1 Maven依赖准备

首先用Spring Initializr初始化项目时,除了Web模块外,特别注意勾选这些依赖:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

2.2 配置文件之美

在application.yml中配置数据源和MyBatis参数时,我们推荐采用阿里云Druid数据源:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user_db?useSSL=false
    username: root
    password: root123
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
  configuration:
    map-underscore-to-camel-case: true

这个配置实现了三个魔法效果:自动驼峰命名转换、实体类别名自动注册、XML映射文件自动扫描。

3. 领域模型与Mapper构建

3.1 实体类设计

先定义一个用户实体类:

public class User {
    private Long id;
    private String username;
    private Integer age;
    private LocalDateTime createTime;
    // 此处省略getter/setter,推荐使用Lombok注解
}

3.2 Mapper接口的写法

方式1:XML映射法(推荐)

@Mapper
public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{userId}")
    User selectById(@Param("userId") Long id);

    @Insert("INSERT INTO users(username,age) VALUES(#{username},#{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);
    
    // 复杂查询采用XML方式
    List<User> selectByCondition(Map<String, Object> params);
}

对应的XML文件UserMapper.xml应该存放在resources/mapper目录:

<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectByCondition" resultType="User">
        SELECT * FROM users
        <where>
            <if test="username != null">
                AND username LIKE CONCAT('%',#{username},'%')
            </if>
            <if test="minAge != null">
                AND age >= #{minAge}
            </if>
            <if test="maxAge != null">
                AND age &lt;= #{maxAge}
            </if>
        </where>
        ORDER BY create_time DESC
    </select>
</mapper>

方式2:注解动态SQL(需@SelectProvider)

@Mapper
public interface UserAnnotationMapper {
    @SelectProvider(type = UserSqlBuilder.class, method = "buildSelectByCondition")
    List<User> selectByCondition(Map<String, Object> params);

    class UserSqlBuilder {
        public static String buildSelectByCondition(Map<String, Object> params) {
            return new SQL() {{
                SELECT("*");
                FROM("users");
                if (params.get("username") != null) {
                    WHERE("username LIKE CONCAT('%',#{username},'%')");
                }
                if (params.get("minAge") != null) {
                    WHERE("age >= #{minAge}");
                }
                if (params.get("maxAge") != null) {
                    WHERE("age <= #{maxAge}");
                }
                ORDER_BY("create_time DESC");
            }}.toString();
        }
    }
}

这种写法把SQL构建逻辑封装在内部类中,适合不喜欢XML的开发团队。

4. 服务层实现技巧

在Service层进行事务管理时,要注意@Transactional注解的生效条件:

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserMapper userMapper;

    @Transactional(rollbackFor = Exception.class)
    public void batchInsert(List<User> users) {
        users.forEach(user -> {
            if (user.getUsername() == null) {
                throw new IllegalArgumentException("用户名不能为空");
            }
            userMapper.insert(user);
        });
    }

    public PageInfo<User> paginationQuery(int pageNum, int pageSize) {
        PageHelper.startPage(pageNum, pageSize);
        List<User> users = userMapper.selectByCondition(Collections.emptyMap());
        return new PageInfo<>(users);
    }
}

这里展示了分页插件的典型用法,需要在pom中添加pagehelper依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>

5. 实践中的疑难解答

5.1 懒加载陷阱

当我们使用嵌套查询时,MyBatis的延迟加载功能可能会引发NPE异常。例如在用户信息中包含订单列表的场景:

<resultMap id="userWithOrders" type="User">
    <collection property="orders" column="id" 
               select="com.example.mapper.OrderMapper.selectByUserId"
               fetchType="lazy"/>
</resultMap>

此时需要在配置文件中启用延迟加载并指定代理方式:

mybatis:
  configuration:
    lazy-loading-enabled: true
    aggressive-lazy-loading: false
    proxy-target-class: true

5.2 二级缓存雪崩

配置缓存时要特别注意缓存策略,避免大量缓存同时失效导致的雪崩效应:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

建议生产环境配合Redis等分布式缓存使用,可以通过实现Cache接口来自定义缓存策略。

6. 技术选型深度分析

6.1 适用场景

  • 复杂查询主导的系统:适合需要精细控制SQL的场景
  • 遗留系统改造:兼容已有复杂SQL的平稳迁移
  • 混合数据库环境:同一系统需要操作多种数据库
  • 需要动态SQL的场景:查询条件动态组合的情况

6.2 优劣势对比

优势方面:

  • SQL可视化调试:直接看到最终执行的SQL语句
  • 学习曲线平缓:熟悉SQL的开发者快速上手
  • 性能调优便捷:直接优化原生SQL语句
  • 灵活的结果映射:支持复杂对象结构的映射

不足之处:

  • 需要维护XML文件:项目庞大时文件数量较多
  • 简单CRUD略啰嗦:相比Spring Data JPA不够简洁
  • 缓存管理复杂:二级缓存需要谨慎处理

7. 架构师的私房建议

  1. 分页规范:统一使用PageHelper分页插件,避免各写各的分页逻辑
  2. SQL审查:建立SQL代码审查机制,防止性能杀手SQL进入生产环境
  3. 安全防御:定期扫描XML文件,防范SQL注入漏洞
  4. 监控预警:集成Druid监控台,实时观察数据库连接池状态
  5. 版本规范:锁定MyBatis和Spring Boot的版本号,避免兼容性问题

8. 项目实战进阶

最后通过一个完整的接口案例展示全流程实现:

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @GetMapping("/search")
    public ResponseResult<PageInfo<User>> search(
            @RequestParam(required = false) String keyword,
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "10") Integer size) {
        
        Map<String, Object> params = new HashMap<>();
        params.put("username", keyword);
        return ResponseResult.success(userService.paginationQuery(params, page, size));
    }

    @PostMapping("/batch")
    public ResponseResult<Integer> batchCreate(@RequestBody List<UserDTO> dtos) {
        // 此处省略DTO转换逻辑
        return ResponseResult.success(userService.batchInsert(users));
    }
}

对应的统一返回封装:

public class ResponseResult<T> {
    private Integer code;
    private String message;
    private T data;
    // 成功静态方法等实现略
}

9. 应用场景总结

从电商系统的商品搜索模块到金融系统的交易流水查询,从社交平台的动态信息流到物联网设备的数据分析,MyBatis+Spring Boot的组合几乎覆盖了所有需要灵活操作关系型数据库的场景。特别是在需要处理复杂报表查询、历史数据归档、多维度数据分析等场景下,其优势更为明显。