1. 什么是 MyBatis

MyBatis 是一个半自动化的持久层框架,它将 SQL 语句从 Java 代码中解耦出来,放在 XML 配置文件或注解中。与 Hibernate 等全自动 ORM 框架不同,MyBatis 不会自动生成 SQL,而是让开发者直接编写 SQL,从而获得更高的灵活性和性能控制。

MyBatis 的核心组件

组件作用生命周期
SqlSessionFactoryBuilder构建 SqlSessionFactory方法局部变量,用完即弃
SqlSessionFactory生产 SqlSession 的工厂应用整个生命周期
SqlSession执行 SQL 的核心接口请求级别,用完必须关闭
Mapper 接口定义 SQL 操作的接口应用整个生命周期

MyBatis 的工作流程

配置文件 → SqlSessionFactory → SqlSession → Mapper → SQL 执行 → 结果映射

  1. 配置文件mybatis-config.xml 或 Spring Boot 的 application.yml
  2. SqlSessionFactory:根据配置创建,线程安全
  3. SqlSession:每次数据库操作创建一个,非线程安全
  4. Mapper:接口与 XML 映射文件绑定,定义 SQL 语句
  5. SQL 执行:预编译、参数设置、结果集处理
  6. 结果映射:将 ResultSet 映射为 Java 对象

2. MyBatis 与 Spring Boot 整合

依赖配置

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

配置文件

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.demo.entity
  configuration:
    map-underscore-to-camel-case: true  # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 控制台打印 SQL

自动配置原理

mybatis-spring-boot-starter 通过 Spring Boot 的自动配置机制,完成了以下工作:

  1. 数据源配置:根据 spring.datasource 配置创建 DataSource
  2. SqlSessionFactory:创建并注册 SqlSessionFactoryBean
  3. SqlSessionTemplate:创建线程安全的 SqlSessionTemplate
  4. Mapper 扫描:根据 @MapperScan@Mapper 注解扫描 Mapper 接口
  5. 配置应用:应用 mybatis.configuration 中的设置

3. 使用示例

实体类

package com.example.demo.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createdAt;
}

Mapper 接口

package com.example.demo.mapper;

import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;

@Mapper
public interface UserMapper {
    
    // 注解方式
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findById(@Param("id") Long id);
    
    @Insert("INSERT INTO users (username, email, created_at) VALUES (#{username}, #{email}, #{createdAt})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);
    
    @Update("UPDATE users SET username = #{username}, email = #{email} WHERE id = #{id}")
    int update(User user);
    
    @Delete("DELETE FROM users WHERE id = #{id}")
    int deleteById(@Param("id") Long id);
    
    // XML 方式(复杂 SQL 推荐)
    List<User> findAll();
    
    List<User> findByCondition(@Param("username") String username, 
                               @Param("email") String email);
}

XML 映射文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
    
    <!-- 结果映射 -->
    <resultMap id="UserResultMap" type="com.example.demo.entity.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="email" property="email"/>
        <result column="created_at" property="createdAt"/>
    </resultMap>
    
    <!-- 动态 SQL -->
    <select id="findByCondition" resultMap="UserResultMap">
        SELECT * FROM users
        <where>
            <if test="username != null and username != ''">
                AND username LIKE CONCAT('%', #{username}, '%')
            </if>
            <if test="email != null and email != ''">
                AND email = #{email}
            </if>
        </where>
        ORDER BY created_at DESC
    </select>
    
    <select id="findAll" resultMap="UserResultMap">
        SELECT * FROM users ORDER BY created_at DESC
    </select>
</mapper>

Service 层

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserMapper userMapper;
    
    public User findById(Long id) {
        return userMapper.findById(id);
    }
    
    public List<User> findAll() {
        return userMapper.findAll();
    }
    
    public int save(User user) {
        user.setCreatedAt(LocalDateTime.now());
        return userMapper.insert(user);
    }
    
    public int update(User user) {
        return userMapper.update(user);
    }
    
    public int delete(Long id) {
        return userMapper.deleteById(id);
    }
}

4. 最佳实践

1. 使用 @MapperScan 代替 @Mapper

@MapperScan("com.example.demo.mapper")
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2. 使用 ResultMap 而不是别名

<!-- 推荐 -->
<resultMap id="UserResultMap" type="User">
    <id column="id" property="id"/>
    <result column="user_name" property="username"/>
</resultMap>

<!-- 不推荐:依赖 map-underscore-to-camel-case -->
<select id="findById" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>

3. 使用 <sql> 片段复用

<sql id="Base_Column_List">
    id, username, email, created_at
</sql>

<select id="findAll" resultMap="UserResultMap">
    SELECT <include refid="Base_Column_List"/> FROM users
</select>

4. 批量操作

// Mapper 接口
int batchInsert(@Param("list") List<User> users);

int batchUpdate(@Param("list") List<User> users);

// XML 配置
<insert id="batchInsert">
    INSERT INTO users (username, email, created_at) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.username}, #{user.email}, #{user.createdAt})
    </foreach>
</insert>

5. 分页查询

使用 PageHelper 插件:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>
// Service 层
public PageInfo<User> findAll(int pageNum, int pageSize) {
    PageHelper.startPage(pageNum, pageSize);
    List<User> users = userMapper.findAll();
    return new PageInfo<>(users);
}

6. 事务管理

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserMapper userMapper;
    
    @Transactional(rollbackFor = Exception.class)
    public void transfer(User from, User to) {
        // 业务逻辑
        userMapper.update(from);
        userMapper.update(to);
        // 如果抛出异常,自动回滚
    }
}

5. 连接池配置

Spring Boot 2.x 以后默认使用 HikariCP 作为连接池,它是目前性能最好的 JDBC 连接池之一。

为什么需要连接池

数据库连接是稀缺资源,创建和销毁连接的代价很高:

  • TCP 三次握手建立连接
  • 数据库认证验证用户权限
  • JDBC 对象初始化StatementResultSet 等)

连接池通过维护一组可复用的连接,避免频繁创建/销毁连接的开销。

HikariCP 核心配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
    username: root
    password: password
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      # 连接池名称
      pool-name: MyHikariPool
      # 最小空闲连接数
      minimum-idle: 5
      # 最大连接数
      maximum-pool-size: 20
      # 连接空闲超时时间(毫秒),默认 600000(10分钟)
      idle-timeout: 300000
      # 连接最大存活时间(毫秒),默认 1800000(30分钟)
      max-lifetime: 1800000
      # 连接超时时间(毫秒),默认 30000(30秒)
      connection-timeout: 30000
      # 连接测试查询(MySQL 推荐设置)
      connection-test-query: SELECT 1
      # 自动提交
      auto-commit: true

关键参数说明

参数说明推荐值
maximum-pool-size最大连接数公式:CPU核数 * 2 + 有效磁盘数,通常 10-20
minimum-idle最小空闲连接maximum-pool-size 相同,保持固定连接池
connection-timeout获取连接等待超时30000ms,根据业务容忍度调整
idle-timeout空闲连接回收时间仅当 minimum-idle < maximum-pool-size 时生效
max-lifetime连接最大存活时间小于数据库的 wait_timeout 设置

监控连接池状态

@Autowired
private HikariDataSource dataSource;

public void printPoolStatus() {
    HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
    System.out.println("活动连接数: " + poolMXBean.getActiveConnections());
    System.out.println("空闲连接数: " + poolMXBean.getIdleConnections());
    System.out.println("等待连接数: " + poolMXBean.getThreadsAwaitingConnection());
    System.out.println("总连接数: " + poolMXBean.getTotalConnections());
}

数据库密码安全配置

上述配置中密码是明文的,生产环境推荐以下方案:

spring:
  datasource:
    password: ${DB_PASSWORD}

启动时指定:

java -jar app.jar --DB_PASSWORD=your_password
# 或
export DB_PASSWORD=your_password && java -jar app.jar

常见问题排查

1. 连接泄漏 - 如果 activeConnections 持续增长且不下降,可能是连接未正确关闭:

// 错误:忘记关闭 SqlSession
SqlSession session = sqlSessionFactory.openSession();
// ... 操作
// session.close(); // 遗漏!

// 正确:使用 try-with-resources
try (SqlSession session = sqlSessionFactory.openSession()) {
    // ... 操作
}

2. 连接超时 - connection-timeout 设置过短或连接数不足时:

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms

解决方案:增加 maximum-pool-size 或优化 SQL 减少连接持有时间。

3. MySQL 8小时问题 - MySQL 默认 wait_timeout=28800(8小时),连接空闲超过 8 小时会被服务器关闭:

spring:
  datasource:
    hikari:
      # 方案1:HikariCP 自动检测(推荐)
      connection-test-query: SELECT 1
      # 方案2:设置连接最大存活时间小于 8 小时
      max-lifetime: 28700000