什么是 MyBatis
MyBatis 是一个半自动化的持久层框架,它将 SQL 语句从 Java 代码中解耦出来,放在 XML 配置文件或注解中。与 Hibernate 等全自动 ORM 框架不同,MyBatis 不会自动生成 SQL,而是让开发者直接编写 SQL,从而获得更高的灵活性和性能控制。
MyBatis 的核心组件
| 组件 | 作用 | 生命周期 |
|---|
SqlSessionFactoryBuilder | 构建 SqlSessionFactory | 方法局部变量,用完即弃 |
SqlSessionFactory | 生产 SqlSession 的工厂 | 应用整个生命周期 |
SqlSession | 执行 SQL 的核心接口 | 请求级别,用完必须关闭 |
Mapper 接口 | 定义 SQL 操作的接口 | 应用整个生命周期 |
MyBatis 的工作流程
配置文件 → SqlSessionFactory → SqlSession → Mapper → SQL 执行 → 结果映射
- 配置文件:
mybatis-config.xml 或 Spring Boot 的 application.yml - SqlSessionFactory:根据配置创建,线程安全
- SqlSession:每次数据库操作创建一个,非线程安全
- Mapper:接口与 XML 映射文件绑定,定义 SQL 语句
- SQL 执行:预编译、参数设置、结果集处理
- 结果映射:将
ResultSet 映射为 Java 对象
MyBatis 与 Spring Boot 整合
依赖配置
1 2 3 4 5 6 7 8 9 10 11
| <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>
|
配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
|
自动配置原理
mybatis-spring-boot-starter 通过 Spring Boot 的自动配置机制,完成了以下工作:
- 数据源配置:根据
spring.datasource 配置创建 DataSource - SqlSessionFactory:创建并注册
SqlSessionFactoryBean - SqlSessionTemplate:创建线程安全的
SqlSessionTemplate - Mapper 扫描:根据
@MapperScan 或 @Mapper 注解扫描 Mapper 接口 - 配置应用:应用
mybatis.configuration 中的设置
使用示例
实体类
1 2 3 4 5 6 7 8 9 10 11
| 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 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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); List<User> findAll(); List<User> findByCondition(@Param("username") String username, @Param("email") String email); }
|
XML 映射文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <?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> <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 层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| 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); } }
|
最佳实践
使用 @MapperScan 代替 @Mapper
1 2 3 4 5 6 7
| @MapperScan("com.example.demo.mapper") @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
|
使用 ResultMap 而不是别名
1 2 3 4 5 6 7 8 9 10
| <resultMap id="UserResultMap" type="User"> <id column="id" property="id"/> <result column="user_name" property="username"/> </resultMap>
<select id="findById" resultType="User"> SELECT * FROM users WHERE id = #{id} </select>
|
使用 <sql> 片段复用
1 2 3 4 5 6 7
| <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>
|
批量操作
1 2 3 4 5 6 7 8 9 10 11 12
| int batchInsert(@Param("list") List<User> users);
int batchUpdate(@Param("list") List<User> users);
<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>
|
分页查询
使用 PageHelper 插件:
1 2 3 4 5
| <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>
|
1 2 3 4 5 6
| public PageInfo<User> findAll(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<User> users = userMapper.findAll(); return new PageInfo<>(users); }
|
事务管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @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); } }
|
连接池配置
Spring Boot 2.x 以后默认使用 HikariCP 作为连接池,它是目前性能最好的 JDBC 连接池之一。
为什么需要连接池
数据库连接是稀缺资源,创建和销毁连接的代价很高:
- TCP 三次握手建立连接
- 数据库认证验证用户权限
- JDBC 对象初始化(
Statement、ResultSet 等)
连接池通过维护一组可复用的连接,避免频繁创建/销毁连接的开销。
HikariCP 核心配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 idle-timeout: 300000 max-lifetime: 1800000 connection-timeout: 30000 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 设置 |
监控连接池状态
1 2 3 4 5 6 7 8 9 10
| @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()); }
|
数据库密码安全配置
上述配置中密码是明文的,生产环境推荐以下方案:
1 2 3
| spring: datasource: password: ${DB_PASSWORD}
|
启动时指定:
1 2 3
| java -jar app.jar --DB_PASSWORD=your_password
export DB_PASSWORD=your_password && java -jar app.jar
|
常见问题排查
1. 连接泄漏 - 如果 activeConnections 持续增长且不下降,可能是连接未正确关闭:
1 2 3 4 5 6 7 8 9
| SqlSession session = sqlSessionFactory.openSession();
try (SqlSession session = sqlSessionFactory.openSession()) { }
|
2. 连接超时 - connection-timeout 设置过短或连接数不足时:
1
| 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 小时会被服务器关闭:
1 2 3 4 5 6 7
| spring: datasource: hikari: connection-test-query: SELECT 1 max-lifetime: 28700000
|