MyBatis基础到高级实践:全方位指南(中)

发布于:2025-09-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

上期内容:  MyBatis基础到高级实践:全方位指南(上)

三、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 &gt;= #{startTime}
                <if test="endTime != null">
                    AND create_time &lt;= #{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 文件位置错误

检查sqlSessionFactorymapperLocations属性是否正确配置了 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 配置中正确定义了SqlSessionFactorySqlSessionTemplate的 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 接口或相关组件正确注入了SqlSessionFactorySqlSessionTemplate

在使用@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.确保资源正确关闭

    在使用SqlSessionResultSet等资源后,确保调用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 提供了多种内置类型处理器,如LocalDateTimeTypeHandlerEnumTypeHandler等,可以直接使用。

    最佳实践与优化建议参考MyBatis基础到高级实践:全方位指南(下)


    网站公告

    今日签到

    点亮在社区的每一天
    去签到