mybatis
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 执行 → 结果映射
- 配置文件:
mybatis-config.xml或 Spring Boot 的application.yml - SqlSessionFactory:根据配置创建,线程安全
- SqlSession:每次数据库操作创建一个,非线程安全
- Mapper:接口与 XML 映射文件绑定,定义 SQL 语句
- SQL 执行:预编译、参数设置、结果集处理
- 结果映射:将
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 的自动配置机制,完成了以下工作:
- 数据源配置:根据
spring.datasource配置创建DataSource - SqlSessionFactory:创建并注册
SqlSessionFactoryBean - SqlSessionTemplate:创建线程安全的
SqlSessionTemplate - Mapper 扫描:根据
@MapperScan或@Mapper注解扫描 Mapper 接口 - 配置应用:应用
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 对象初始化(
Statement、ResultSet等)
连接池通过维护一组可复用的连接,避免频繁创建/销毁连接的开销。
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