三、MyBatis 与 Spring 整合高级技巧
MyBatis 提供了强大的动态 SQL 功能,可以在 XML 映射文件中使用各种标签构建动态 SQL 语句。以下是一些高级应用技巧:
3.1.1 条件查询
使用<where>、<if>和<choose>标签可以构建灵活的条件查询:
<select id="selectUsersByCondition" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="username != null and username != ''">
username LIKE CONCAT('%', #{username}, '%')
</when>
<when test="email != null and email != ''">
email = #{email}
</when>
<otherwise>
create_time >= #{startTime}
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
</otherwise>
</choose>
</where>
ORDER BY id DESC
</select>
对应的 Mapper 接口方法:
List<User> selectUsersByCondition(
@Param("username") String username,
@Param("email") String email,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime
);
这种方式可以避免在代码中拼接 SQL 字符串,提高代码的可读性和安全性(7)。
3.1.2 批量操作
使用<foreach>标签可以实现批量插入和更新:
<insert id="batchInsert">
INSERT INTO user(username, email, create_time)
VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email}, #{user.createTime})
</foreach>
</insert>
对应的 Mapper 接口方法:
int batchInsert(@Param("users") List<User> users);
批量操作可以显著提高数据库操作的性能,特别是当需要插入或更新大量数据时(10)。
3.1.3 SQL 片段复用
使用<sql>标签可以定义可复用的 SQL 片段:
<sql id="userColumns">
id, username, email, create_time
</sql>
<select id="selectUserById" resultType="User">
SELECT
<include refid="userColumns"/>
FROM user
WHERE id = #{id}
</select>
这种方式可以减少代码冗余,提高可维护性。
3.2 事务管理
Spring 提供了强大的事务管理机制,可以与 MyBatis 无缝集成。
3.2.1 声明式事务
使用@Transactional注解可以实现声明式事务管理:
@Service
public class UserService {
private final UserMapper userMapper;
private final LogMapper logMapper;
@Autowired
public UserService(UserMapper userMapper, LogMapper logMapper) {
this.userMapper = userMapper;
this.logMapper = logMapper;
}
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
rollbackFor = Exception.class
)
public void createUserWithLog(User user, String operation) {
userMapper.insert(user);
Log log = new Log();
log.setOperation(operation);
log.setCreateTime(LocalDateTime.now());
logMapper.insert(log);
// 如果此处抛出异常,两个插入操作都会回滚
}
}
在这个示例中,createUserWithLog方法上的@Transactional注解表示该方法需要事务管理。如果在方法执行过程中抛出异常,两个插入操作都会回滚,保证了数据的一致性。
3.2.2 编程式事务
除了声明式事务,Spring 还支持编程式事务管理:
@Service
public class UserService {
private final UserMapper userMapper;
private final TransactionTemplate transactionTemplate;
@Autowired
public UserService(UserMapper userMapper, TransactionTemplate transactionTemplate) {
this.userMapper = userMapper;
this.transactionTemplate = transactionTemplate;
}
public void batchUpdate(List<User> users) {
transactionTemplate.execute(status -> {
users.forEach(user -> {
userMapper.update(user);
});
return true;
});
}
}
编程式事务允许更细粒度的事务控制,适用于复杂的事务场景。
3.2.3 事务传播行为
Spring 支持多种事务传播行为,常见的有:
- REQUIRED(默认):如果当前存在事务,加入该事务;否则创建新事务。
- REQUIRES_NEW:创建新事务,如果当前存在事务,挂起当前事务。
- SUPPORTS:如果当前存在事务,加入该事务;否则以非事务方式执行。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,挂起当前事务。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
合理设置事务传播行为可以避免事务嵌套问题,提高系统性能。
3.3 分页查询
分页查询是数据库操作中的常见需求,MyBatis 提供了多种分页解决方案。
3.3.1 使用 PageHelper 插件
PageHelper 是一个流行的 MyBatis 分页插件,可以方便地实现分页查询:
1.添加 PageHelper 依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
2.配置 PageHelper:
@Configuration
public class PageHelperConfig {
@Bean
public PageInterceptor pageInterceptor() {
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("helperDialect", "mysql");
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
pageInterceptor.setProperties(properties);
return pageInterceptor;
}
}
3.使用 PageHelper 进行分页查询:
public PageInfo<User> getUsers(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
return new PageInfo<>(users);
}
PageHelper 会自动拦截查询语句并添加分页参数,返回的PageInfo对象包含了分页信息和查询结果。
3.3.2 自定义分页
如果不想使用插件,也可以手动实现分页:
<select id="selectByPage" resultType="User">
SELECT * FROM user
ORDER BY id DESC
LIMIT #{offset}, #{pageSize}
</select>
对应的 Mapper 接口方法:
List<User> selectByPage(@Param("offset") int offset, @Param("pageSize") int pageSize);
然后在 Service 层计算偏移量:
public List<User> getUsers(int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
return userMapper.selectByPage(offset, pageSize);
}
这种方式需要手动计算偏移量,不如 PageHelper 方便,但可以避免引入额外的依赖。
3.4 多数据源配置
在大型项目中,可能需要连接多个数据库,这时需要配置多数据源。
3.4.1 基本多数据源配置
配置两个数据源的示例:
@Configuration
@MapperScan(basePackages = "com.example.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
return factoryBean.getObject();
}
@Bean
@Primary
public DataSourceTransactionManager primaryTransactionManager(
@Qualifier("primaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@Configuration
@MapperScan(basePackages = "com.example.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory")
public class SecondaryDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory secondarySqlSessionFactory(@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/secondary/*.xml"));
return factoryBean.getObject();
}
@Bean
public DataSourceTransactionManager secondaryTransactionManager(
@Qualifier("secondaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
在这个配置中,@Primary注解标记了主数据源,两个数据源分别对应不同的 Mapper 接口包和 Mapper XML 文件路径。
3.4.2 动态数据源切换
对于需要在运行时动态切换数据源的场景,可以使用动态数据源路由:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceKey(String dataSourceKey) {
CONTEXT_HOLDER.set(dataSourceKey);
}
public static String getDataSourceKey() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceKey() {
CONTEXT_HOLDER.remove();
}
}
然后在 Service 层或 Controller 层根据业务需求设置数据源键:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void switchDataSource(String dataSourceKey) {
DataSourceContextHolder.setDataSourceKey(dataSourceKey);
// 执行数据库操作
userMapper.selectAll();
DataSourceContextHolder.clearDataSourceKey();
}
}
动态数据源切换可以在运行时根据业务需求灵活选择数据源,但需要注意线程安全问题。
3.5 MyBatis 缓存与性能优化
MyBatis 提供了一级缓存和二级缓存机制,可以显著提高数据库操作的性能。
3.5.1 一级缓存
一级缓存是 SqlSession 级别的缓存,默认开启。在同一个 SqlSession 中,相同的查询语句(包括参数)第二次执行时不会访问数据库,而是从缓存中获取结果。
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional // 这个注解很关键!
public void doSomething() {
// 这两个调用共用一个SqlSession,第二次查询会命中一级缓存
User u1 = userMapper.selectById(1L);
User u2 = userMapper.selectById(1L);
}
}
需要注意的是,在 Spring 管理下的 MyBatis 中,默认每个 Mapper 方法调用都会创建一个新的 SqlSession。如果希望利用一级缓存,需要将多个查询放在同一个事务中,这样它们会共享同一个 SqlSession。
3.5.2 二级缓存
二级缓存是 Mapper 级别的缓存,需要显式开启:
1.在 MyBatis 配置文件中开启二级缓存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
2.在 Mapper 接口或 Mapper XML 文件中配置缓存:
@CacheNamespace(eviction = LRU.class, flushInterval = 60000, size = 512, readOnly = true)
public interface UserMapper {
// Mapper方法定义
}
或者在 XML 文件中:
<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<!-- SQL语句定义 -->
</mapper>
二级缓存跨 SqlSession 生效,可以提高应用的整体性能。但在分布式系统中,可能需要考虑缓存一致性问题。
3.5.3 与 Spring Cache 集成
MyBatis 的二级缓存可以与 Spring 的 Cache 抽象层集成,实现更灵活的缓存管理:
@CacheConfig(cacheNames = "users")
public interface UserMapper {
@Cacheable(key = "#id")
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
@CacheEvict(allEntries = true)
@Update("UPDATE user SET username=#{username} WHERE id=#{id}")
int updateUsername(@Param("id") Long id, @Param("username") String username);
}
这种方式允许使用 Spring 的缓存抽象,支持多种缓存实现,如 Ehcache、Redis 等。
四、常见问题与解决方案
4.1 配置相关问题
4.1.1 Invalid bound statement (not found) 错误
当出现 "Invalid bound statement (not found)" 错误时,通常是由于 MyBatis 无法找到对应的 SQL 语句。可能的原因和解决方案:
Mapper 接口与 XML 文件不匹配:
确保 Mapper 接口的方法名与 XML 文件中的 SQL 语句 id 一致。
确保 XML 文件中的 namespace 与 Mapper 接口的完全限定名一致。
Mapper 接口未被扫描:
检查是否使用了@MapperScan注解或MapperScannerConfigurer配置了正确的扫描路径。
确保 Mapper 接口被正确标记为@Mapper注解。
XML 文件位置错误:
检查sqlSessionFactory的mapperLocations属性是否正确配置了 XML 文件的路径。
在 Spring Boot 项目中,确保 Mapper XML 文件位于src/main/resources/mapper目录下。
XML 文件语法错误:
XML 文件中存在语法错误会导致 MyBatis 无法正确解析,可以使用 XML 验证工具检查语法。
项目编译问题:
有时候编译不完整会导致 Mapper 接口或 XML 文件未被正确打包,可以尝试清理并重新构建项目。
4.1.2 Property'sqlSessionFactory' or'sqlSessionTemplate' are required 错误
当出现 "Property'sqlSessionFactory' or'sqlSessionTemplate' are required" 错误时,通常是由于 MyBatis 的 Mapper 接口或相关组件未正确注入。
可能的原因和解决方案:
1.缺少必要的 Bean 定义:
1.确保在 Spring 配置中正确定义了SqlSessionFactory和SqlSessionTemplate的 Bean。
在 XML 配置中:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg ref="sqlSessionFactory"/>
</bean>
2.依赖注入错误:
确保 Mapper 接口或相关组件正确注入了SqlSessionFactory或SqlSessionTemplate。
在使用@Autowired注入时,确保被注入的 Bean 已经正确定义。
3.配置类未被扫描:
如果使用 Java 配置类定义 Bean,确保配置类被 Spring 扫描到。
可以通过在主类上添加@ComponentScan注解或在配置类上添加@Configuration注解来解决。
4.1.3 Spring Boot 与 MyBatis 版本不兼容
当使用 Spring Boot 与 MyBatis 整合时,版本兼容性问题可能导致应用启动失败或功能异常。
可能的原因和解决方案:
1.版本不匹配:
确保使用的 Spring Boot 版本与 MyBatis 和 MyBatis-Spring 版本兼容。
根据 MyBatis 官方文档,MyBatis-Spring 3.0 版本需要 Spring 6.0 + 和 MyBatis 3.5 + 的支持。
2.依赖冲突:
检查项目依赖中是否存在版本冲突,可以通过 Maven 的dependency:tree命令查看依赖树。
使用exclusions标签排除冲突的依赖。
3.Spring Boot 自动配置问题:
Spring Boot 的自动配置可能与手动配置冲突,可以通过spring.autoconfigure.exclude属性排除特定的自动配置类。
4.MyBatis-Plus 版本问题:
如果使用 MyBatis-Plus,确保其版本与 Spring Boot 兼容。例如,Spring Boot 3.5.0 可能需要 MyBatis-Plus 3.5.3 或更高版本。
4.2 数据库操作问题
4.2.1 事务未生效
当使用@Transactional注解但事务未生效时,可能的原因和解决方案:
1.方法不是 public:
@Transactional注解只能应用于 public 方法,否则事务不会生效。
2.类未被 Spring 管理:
确保使用@Transactional注解的类是 Spring Bean,即被@Component、@Service等注解标记。
3.异常类型不匹配:
默认情况下,@Transactional只会回滚 RuntimeException 和 Error。如果需要回滚检查异常,需要显式设置rollbackFor属性:
4.同一类中方法调用:
如果在同一类中,一个未被@Transactional注解的方法调用另一个被@Transactional注解的方法,事务不会生效。因为 Spring 的 AOP 代理是基于接口或子类的,同一类中的方法调用不会触发代理对象的调用。
5.数据库引擎不支持事务:
确保使用的数据库引擎支持事务,如 MySQL 的 InnoDB 引擎,而不是 MyISAM 引擎。
4.2.2 N+1 查询问题
N+1 查询问题通常出现在使用关联查询或延迟加载时,表现为执行一个主查询后,又执行了多个子查询。
可能的解决方案:
1.使用联表查询:
将多个独立的查询合并为一个联表查询,减少数据库交互次数:
<select id="selectPostsWithComments" resultMap="PostResultMap">
SELECT p.*, c.*
FROM post p
LEFT JOIN comment c ON p.id = c.post_id
</select>
<resultMap id="PostResultMap" type="Post">
<id property="id" column="id"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
</resultMap>
2.合理使用延迟加载:
使用 MyBatis 的延迟加载特性,只有在真正需要关联数据时才执行查询:
<resultMap id="UserResultMap" type="User">
<association
property="department"
javaType="Department"
select="com.example.mapper.DepartmentMapper.selectById"
column="dept_id"
fetchType="lazy"
/>
</resultMap>
3.批量查询优化:
如果必须执行多个独立查询,可以使用批量查询或缓存来减少数据库交互次数:
List<Long> userIds = ...;
List<User> users = userMapper.selectBatchIds(userIds);
4.使用二级缓存:
对频繁查询且不经常变化的数据使用二级缓存,减少数据库查询次数(10)。
4.2.3 数据更新未立即反映在查询中
在 MyBatis 与 Spring 整合中,有时会出现数据更新后立即查询却看不到更新结果的情况,这通常与 MyBatis 的缓存机制有关。
可能的原因和解决方案:
1.一级缓存未清除:
执行更新操作后,MyBatis 的一级缓存不会自动清除,导致后续查询仍从缓存中获取旧数据。解决方法是在更新操作后手动清除缓存:
2.事务未提交:
如果更新操作在事务中,而事务尚未提交,查询操作可能在另一个事务中执行,无法看到未提交的数据。确保更新操作所在的事务已经提交。
3.使用不同的 SqlSession:
如果更新和查询操作使用不同的 SqlSession,可能无法立即看到更新结果。在 Spring 中,可以通过将更新和查询放在同一个事务中来确保使用同一个 SqlSession。
4.2.4 分页查询性能问题
分页查询在处理大数据量时可能会遇到性能问题,特别是当偏移量很大时。
可能的解决方案:
1.使用索引:
确保分页查询的字段上有索引,特别是ORDER BY子句中的字段。
** 避免 SELECT ***:
只查询需要的字段,减少数据传输和处理的开销。
2.使用更高效的分页方法:
对于大偏移量的分页,可以考虑使用基于索引的分页方法,如:
SELECT * FROM user
WHERE id > #{lastId}
ORDER BY id
LIMIT #{pageSize}
这种方法需要记录上次查询的最大 id,适用于有序数据的分页。
3.限制分页深度:
在业务层面限制用户可以查看的最大页数,避免处理过大的偏移量。
4.使用缓存:
对于不经常变化的数据,可以使用缓存来减少数据库查询次数
4.3 其他常见问题
4.3.1 连接泄漏
连接泄漏是指数据库连接使用后未正确关闭,导致资源耗尽。
可能的解决方案:
1.使用连接池:
使用连接池数据源如 HikariCP,它会自动管理连接的创建和释放。
2.配置连接泄漏检测:
可以配置 HikariCP 的leakDetectionThreshold属性来检测连接泄漏:
@Bean
public DataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setLeakDetectionThreshold(30000); // 30秒泄漏检测
return ds;
}
3.确保资源正确关闭:
在使用SqlSession或ResultSet等资源后,确保调用close()方法。在 Spring 管理下,SqlSession会在事务结束时自动关闭,但在手动使用时需要注意。
4.使用 try-with-resources:
使用 Java 7 的 try-with-resources 语句自动关闭资源:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 使用session
}
5.监控数据库连接:
定期监控数据库连接的使用情况,及时发现和解决连接泄漏问题。
4.3.2 延迟加载失效
当 MyBatis 的延迟加载失效时,可能的原因和解决方案:
1.配置错误:
检查 MyBatis 的配置是否正确开启了延迟加载:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
2.代理对象问题:
确保使用的是 MyBatis 生成的代理对象,而不是原始对象。延迟加载是通过代理对象实现的。
3.事务未正确管理:
延迟加载需要在同一个数据库会话中执行,如果事务已经提交或回滚,延迟加载将无法执行。确保在事务结束前访问延迟加载的数据。
4.使用 CGLIB 代理:
如果 MyBatis 的默认代理工厂不支持延迟加载,可以强制使用 CGLIB 代理:
<setting name="proxyFactory" value="CGLIB"/>
5.避免在事务外访问延迟加载数据:
确保在事务范围内访问延迟加载的数据,否则会抛出LazyInitializationException异常。
4.3.3 数据类型转换问题
当数据库字段类型与 Java 对象属性类型不匹配时,可能导致数据类型转换错误。
可能的解决方案:
1.使用类型处理器:
MyBatis 提供了TypeHandler接口,可以自定义数据类型转换:
在 MyBatis 配置中注册类型处理器:
2.使用 JdbcType 属性:
在映射文件中显式指定 JDBC 类型:
3.数据库字段类型与 Java 类型匹配:
尽量使用数据库字段类型与 Java 类型直接匹配的数据类型,如使用TIMESTAMP对应 Java 的LocalDateTime。
4.使用 MyBatis 的内置类型处理器:
MyBatis 提供了多种内置类型处理器,如LocalDateTimeTypeHandler、EnumTypeHandler等,可以直接使用。
最佳实践与优化建议参考MyBatis基础到高级实践:全方位指南(下)