数据访问
本章将介绍 Spring Boot 中数据访问的核心技术,包括 Spring Data JPA、MyBatis 集成和事务管理。
数据源配置
添加依赖
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JDBC 起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置(Spring Boot 默认)
hikari:
minimum-idle: 5 # 最小空闲连接数
maximum-pool-size: 20 # 最大连接数
idle-timeout: 30000 # 空闲连接超时时间(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
connection-timeout: 30000 # 连接超时时间(毫秒)
pool-name: MyHikariCP # 连接池名称
多数据源配置
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.primary")
@Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
}
Spring Data JPA
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
JPA 配置
spring:
jpa:
hibernate:
ddl-auto: update # 自动更新表结构
show-sql: true # 显示 SQL
properties:
hibernate:
format_sql: true # 格式化 SQL
dialect: org.hibernate.dialect.MySQLDialect
ddl-auto 选项说明:
| 值 | 说明 |
|---|---|
none | 不做任何操作 |
validate | 验证表结构与实体是否一致 |
update | 自动更新表结构(保留数据) |
create | 每次启动创建新表(删除旧数据) |
create-drop | 启动创建,关闭删除 |
实体类
@Entity
@Table(name = "t_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(unique = true, nullable = false, length = 100)
private String email;
@Column(name = "phone_number", length = 20)
private String phone;
private Integer age;
@Enumerated(EnumType.STRING)
private Gender gender;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_at", updatable = false)
private Date createdAt;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "updated_at")
private Date updatedAt;
@PrePersist
public void prePersist() {
this.createdAt = new Date();
this.updatedAt = new Date();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = new Date();
}
}
public enum Gender {
MALE, FEMALE, OTHER
}
常用 JPA 注解
| 注解 | 作用 |
|---|---|
@Entity | 标识为实体类 |
@Table | 指定表名 |
@Id | 标识主键 |
@GeneratedValue | 主键生成策略 |
@Column | 字段映射 |
@Enumerated | 枚举类型映射 |
@Temporal | 日期时间类型映射 |
@Transient | 忽略字段(不映射) |
@OneToOne | 一对一关联 |
@OneToMany | 一对多关联 |
@ManyToOne | 多对一关联 |
@ManyToMany | 多对多关联 |
Repository 接口
// 基础 Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 根据方法名自动生成查询
User findByName(String name);
List<User> findByAgeGreaterThan(Integer age);
List<User> findByAgeBetween(Integer min, Integer max);
List<User> findByNameContaining(String name);
User findByEmail(String email);
// 使用 @Query 自定义查询
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
// 原生 SQL 查询
@Query(value = "SELECT * FROM t_user WHERE age > :age", nativeQuery = true)
List<User> findByAgeGreaterThanNative(@Param("age") Integer age);
// 更新操作
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateName(@Param("id") Long id, @Param("name") String name);
// 分页查询
Page<User> findByAgeGreaterThan(Integer age, Pageable pageable);
}
关键字查询
| 关键字 | 示例 | SQL 片段 |
|---|---|---|
And | findByNameAndAge | WHERE name = ? AND age = ? |
Or | findByNameOrAge | WHERE name = ? OR age = ? |
Between | findByAgeBetween | WHERE age BETWEEN ? AND ? |
LessThan | findByAgeLessThan | WHERE age < ? |
GreaterThan | findByAgeGreaterThan | WHERE age > ? |
Like | findByNameLike | WHERE name LIKE ? |
Containing | findByNameContaining | WHERE name LIKE %?% |
StartingWith | findByNameStartingWith | WHERE name LIKE ?% |
EndingWith | findByNameEndingWith | WHERE name LIKE %? |
In | findByAgeIn | WHERE age IN (?) |
IsNull | findByNameIsNull | WHERE name IS NULL |
NotNull | findByNameNotNull | WHERE name IS NOT NULL |
OrderBy | findByAgeOrderByName | WHERE age = ? ORDER BY name |
Not | findByNameNot | WHERE name <> ? |
分页查询
// Service 层
public Page<User> findByPage(int page, int size, String sort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sort).descending());
return userRepository.findAll(pageable);
}
// Controller 层
@GetMapping
public Result<Page<User>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "createdAt") String sort
) {
return Result.success(userService.findByPage(page, size, sort));
}
分页响应结构:
{
"content": [...],
"totalElements": 100,
"totalPages": 10,
"size": 10,
"number": 0,
"first": true,
"last": false
}
MyBatis 集成
添加依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/**/*.xml # Mapper XML 文件位置
type-aliases-package: com.example.entity # 实体类包路径
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
Mapper 接口
@Mapper
public interface UserMapper {
/**
* 查询所有用户
*/
List<User> findAll();
/**
* 根据 ID 查询
*/
User findById(Long id);
/**
* 插入用户
*/
int insert(User user);
/**
* 更新用户
*/
int update(User user);
/**
* 删除用户
*/
int deleteById(Long id);
/**
* 条件查询
*/
List<User> findByCondition(@Param("name") String name, @Param("age") Integer age);
}
Mapper 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.mapper.UserMapper">
<!-- 结果映射 -->
<resultMap id="UserResultMap" type="com.example.entity.User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="email" column="email"/>
<result property="phone" column="phone_number"/>
<result property="age" column="age"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<!-- 查询所有 -->
<select id="findAll" resultMap="UserResultMap">
SELECT * FROM t_user ORDER BY created_at DESC
</select>
<!-- 根据 ID 查询 -->
<select id="findById" resultMap="UserResultMap">
SELECT * FROM t_user WHERE id = #{id}
</select>
<!-- 插入 -->
<insert id="insert" parameterType="com.example.entity.User"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user (name, email, phone_number, age, created_at, updated_at)
VALUES (#{name}, #{email}, #{phone}, #{age}, NOW(), NOW())
</insert>
<!-- 更新 -->
<update id="update" parameterType="com.example.entity.User">
UPDATE t_user
SET name = #{name},
email = #{email},
phone_number = #{phone},
age = #{age},
updated_at = NOW()
WHERE id = #{id}
</update>
<!-- 删除 -->
<delete id="deleteById">
DELETE FROM t_user WHERE id = #{id}
</delete>
<!-- 条件查询 -->
<select id="findByCondition" resultMap="UserResultMap">
SELECT * FROM t_user
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
</mapper>
注解方式
@Mapper
public interface UserMapper {
@Select("SELECT * FROM t_user WHERE id = #{id}")
User findById(Long id);
@Select("SELECT * FROM t_user ORDER BY created_at DESC")
List<User> findAll();
@Insert("INSERT INTO t_user (name, email, phone_number, age, created_at, updated_at) " +
"VALUES (#{name}, #{email}, #{phone}, #{age}, NOW(), NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE t_user SET name = #{name}, email = #{email}, " +
"phone_number = #{phone}, age = #{age}, updated_at = NOW() WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM t_user WHERE id = #{id}")
int deleteById(Long id);
}
事务管理
声明式事务
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
/**
* 创建用户并初始化订单
* 整个方法在事务中执行
*/
public User createUserWithOrder(UserDTO userDTO) {
User user = userRepository.save(convertToEntity(userDTO));
// 创建初始订单
Order order = new Order();
order.setUserId(user.getId());
orderRepository.save(order);
return user;
}
/**
* 只读事务
*/
@Transactional(readOnly = true)
public List<User> findAll() {
return userRepository.findAll();
}
/**
* 指定回滚异常
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
userRepository.deleteById(id);
}
/**
* 不回滚特定异常
*/
@Transactional(noRollbackFor = BusinessException.class)
public void update(User user) {
userRepository.save(user);
}
}
事务传播行为
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) | 有事务则加入,无则新建 |
SUPPORTS | 有事务则加入,无则非事务执行 |
MANDATORY | 必须在事务中,否则抛异常 |
REQUIRES_NEW | 总是新建事务,暂停当前事务 |
NOT_SUPPORTED | 非事务执行,暂停当前事务 |
NEVER | 非事务执行,有事务则抛异常 |
NESTED | 嵌套事务(保存点) |
@Service
public class OrderService {
/**
* 新事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderLog(Order order) {
// 即使外层事务回滚,此日志也会保存
orderLogRepository.save(new OrderLog(order));
}
}
事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
READ_UNCOMMITTED | √ | √ | √ |
READ_COMMITTED | × | √ | √ |
REPEATABLE_READ(MySQL 默认) | × | × | √ |
SERIALIZABLE | × | × | × |
@Transactional(isolation = Isolation.READ_COMMITTED)
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
数据库迁移
Flyway
添加依赖
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
</dependency>
配置
spring:
flyway:
enabled: true
locations: classpath:db/migration # 迁移脚本位置
baseline-on-migrate: true # 首次迁移时创建基线
validate-on-migrate: true # 迁移前验证
迁移脚本命名规则
V{版本号}__{描述}.sql
示例:
V1__init_schema.sql
V2__add_user_table.sql
V3__add_order_table.sql
迁移脚本示例
-- V1__init_schema.sql
CREATE TABLE t_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
phone_number VARCHAR(20),
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Liquibase
添加依赖
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
配置
spring:
liquibase:
enabled: true
change-log: classpath:db/changelog/db.changelog-master.yaml
变更日志
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- include:
file: db/changelog/changes/001-init-schema.yaml
# db/changelog/changes/001-init-schema.yaml
databaseChangeLog:
- changeSet:
id: 1
author: author
changes:
- createTable:
tableName: t_user
columns:
- column:
name: id
type: BIGINT
autoIncrement: true
constraints:
primaryKey: true
- column:
name: name
type: VARCHAR(50)
constraints:
nullable: false
HikariCP 连接池深度优化
HikariCP 是 Spring Boot 默认的数据库连接池,以其高性能和低延迟著称。正确配置连接池对应用性能至关重要。
连接池工作原理
┌─────────────────────────────────────────────────────────────────────┐
│ 连接池工作原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用请求连接 ──> 从池中获取空闲连接 ──> 使用连接执行SQL │
│ │ │ │
│ 连接池管理 归还连接 │
│ ┌─────┴─────┐ │ │
│ ▼ ▼ ▼ │
│ 空闲连接池 活跃连接池 重新放入空闲池 │
│ (等待使用) (正在使用) │
│ │
│ 连接池优势: │
│ - 避免频繁创建/销毁连接的开销 │
│ - 控制数据库连接数量,防止连接耗尽 │
│ - 连接复用,提高性能 │
│ │
└─────────────────────────────────────────────────────────────────────┘
核心配置参数详解
spring:
datasource:
hikari:
# 连接池大小配置
maximum-pool-size: 20 # 最大连接数
minimum-idle: 5 # 最小空闲连接数
# 超时配置
connection-timeout: 30000 # 获取连接超时时间(毫秒)
idle-timeout: 600000 # 空闲连接超时时间(毫秒)
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
# 连接验证配置
validation-timeout: 5000 # 连接验证超时时间(毫秒)
leak-detection-threshold: 60000 # 连接泄露检测阈值(毫秒)
# 连接池名称
pool-name: MyHikariCP
# 初始化配置
initialization-fail-timeout: 1 # 初始化失败超时
connection-init-sql: SELECT 1 # 连接初始化SQL
参数详解:
| 参数 | 默认值 | 说明 |
|---|---|---|
maximum-pool-size | 10 | 连接池最大连接数,超过此数量的请求将阻塞等待 |
minimum-idle | 与maximum相同 | 最小空闲连接数,建议设置为与最大值相同以避免动态调整开销 |
connection-timeout | 30000ms | 客户端等待连接的最大毫秒数,超时将抛出异常 |
idle-timeout | 600000ms | 空闲连接在池中保留的最长时间,仅在minimum-idle < maximum-pool-size时生效 |
max-lifetime | 1800000ms | 连接的最大存活时间,建议比数据库的wait_timeout小几秒 |
leak-detection-threshold | 0(禁用) | 连接泄露检测阈值,如果连接未归还超过此时间,将记录警告日志 |
连接池大小计算
连接池大小不是越大越好,需要根据实际场景计算:
公式:
实践经验:
# 低并发场景(内部系统)
spring:
datasource:
hikari:
maximum-pool-size: 5-10
minimum-idle: 2-5
# 中等并发场景(Web应用)
spring:
datasource:
hikari:
maximum-pool-size: 10-20
minimum-idle: 5-10
# 高并发场景(互联网应用)
spring:
datasource:
hikari:
maximum-pool-size: 20-50
minimum-idle: 10-20
注意:连接池大小需要根据数据库服务器的最大连接数限制来设置,确保所有应用实例的总连接数不超过数据库的限制。
连接泄露检测
连接泄露是指应用获取连接后忘记归还,会导致连接池耗尽:
spring:
datasource:
hikari:
# 启用连接泄露检测(开发环境推荐)
leak-detection-threshold: 60000 # 60秒未归还则记录警告
日志示例:
WARN - Connection leak detection triggered for connection on thread http-nio-8080-exec-1
常见泄露场景:
// 错误:忘记关闭连接
public void badExample() throws SQLException {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
// 异常发生时连接未归还
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
}
// 正确:使用 try-with-resources
public void goodExample() throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
// 自动关闭资源
}
}
JMX 监控
启用 JMX 监控可以实时查看连接池状态:
spring:
datasource:
hikari:
register-mbeans: true # 注册 JMX MBean
使用 JConsole 或 VisualVM 连接后,可以监控:
| 指标 | 说明 |
|---|---|
ActiveConnections | 当前活跃连接数 |
IdleConnections | 当前空闲连接数 |
TotalConnections | 总连接数 |
ThreadsAwaitingConnection | 等待获取连接的线程数 |
ConnectionCreationMillis | 创建连接的平均耗时 |
生产环境最佳配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# 连接池大小(根据实际情况调整)
maximum-pool-size: 20
minimum-idle: 10
# 超时配置
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 连接验证
validation-timeout: 5000
leak-detection-threshold: 60000
# 连接池名称(方便监控识别)
pool-name: ProductionPool
# 初始化SQL(验证连接有效性)
connection-init-sql: SELECT 1
# 注册JMX监控
register-mbeans: true
JPA 性能优化
JPA 虽然简化了数据访问,但不当使用会导致严重的性能问题。本节介绍常见的性能陷阱和优化方案。
N+1 查询问题
N+1 问题是 JPA 最常见的性能陷阱,会导致大量不必要的数据库查询。
问题示例:
@Entity
public class Author {
@Id
private Long id;
private String name;
// 一对多关系,默认懒加载
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
private List<Book> books;
}
@Entity
public class Book {
@Id
private Long id;
private String title;
@ManyToOne
@JoinColumn(name = "author_id")
private Author author;
}
触发 N+1 的代码:
// 查询所有作者(1次查询)
List<Author> authors = authorRepository.findAll();
for (Author author : authors) {
// 每个作者访问 books 集合时,各执行一次查询(N次查询)
System.out.println(author.getName() + ": " + author.getBooks().size());
}
// 总共执行:1 + N 次查询(N = 作者数量)
执行过程分析:
-- 第1次查询:获取所有作者
SELECT * FROM author;
-- 第2次查询:获取第1个作者的书籍
SELECT * FROM book WHERE author_id = 1;
-- 第3次查询:获取第2个作者的书籍
SELECT * FROM book WHERE author_id = 2;
-- ... 继续 N 次
解决方案一:JOIN FETCH
使用 JPQL 的 JOIN FETCH 一次性加载关联数据:
public interface AuthorRepository extends JpaRepository<Author, Long> {
/**
* 使用 JOIN FETCH 同时加载作者和书籍
*/
@Query("SELECT DISTINCT a FROM Author a LEFT JOIN FETCH a.books")
List<Author> findAllWithBooks();
/**
* 按需加载指定作者的书籍
*/
@Query("SELECT a FROM Author a LEFT JOIN FETCH a.books WHERE a.id = :id")
Optional<Author> findByIdWithBooks(@Param("id") Long id);
}
生成的 SQL:
SELECT DISTINCT a.*, b.*
FROM author a
LEFT JOIN book b ON a.id = b.author_id;
使用示例:
// 只执行1次查询,获取所有作者和书籍
List<Author> authors = authorRepository.findAllWithBooks();
for (Author author : authors) {
// 不会再执行额外查询
System.out.println(author.getName() + ": " + author.getBooks().size());
}
解决方案二:EntityGraph
@EntityGraph 提供了更灵活的关联加载方式,可以在运行时决定加载策略:
@Entity
@NamedEntityGraph(
name = "Author.withBooks",
attributeNodes = @NamedAttributeNode("books")
)
public class Author {
// ... 实体定义
}
public interface AuthorRepository extends JpaRepository<Author, Long> {
/**
* 使用 EntityGraph 加载关联数据
*/
@EntityGraph(value = "Author.withBooks", type = EntityGraph.EntityGraphType.LOAD)
List<Author> findAll();
/**
* 动态 EntityGraph
*/
@EntityGraph(attributePaths = {"books"})
Optional<Author> findById(Long id);
}
EntityGraph 类型说明:
| 类型 | 说明 |
|---|---|
LOAD | 懒加载的属性变为急加载,其他保持原样 |
FETCH | 只加载指定的属性,其他懒加载属性保持懒加载 |
解决方案三:批量获取
配置批量获取可以减少查询次数:
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100 # 批量获取大小
batch_fetch_style: PADDED # 批量获取风格
@Entity
public class Author {
@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
@BatchSize(size = 20) // 每次批量获取20个作者的书籍
private List<Book> books;
}
解决方案四:使用 DTO 投影
对于只需要部分字段的场景,使用 DTO 投影避免加载整个实体:
/**
* 接口投影
*/
public interface AuthorBookCount {
Long getId();
String getName();
Long getBookCount();
}
public interface AuthorRepository extends JpaRepository<Author, Long> {
@Query("SELECT a.id as id, a.name as name, COUNT(b) as bookCount " +
"FROM Author a LEFT JOIN a.books b " +
"GROUP BY a.id, a.name")
List<AuthorBookCount> findAuthorBookCounts();
}
/**
* 类投影(推荐)
*/
public record AuthorDTO(Long id, String name, List<String> bookTitles) {}
@Query("SELECT new com.example.dto.AuthorDTO(a.id, a.name, " +
"(SELECT GROUP_CONCAT(b.title) FROM Book b WHERE b.author = a)) " +
"FROM Author a")
List<AuthorDTO> findAuthorDTOs();
懒加载最佳实践
原则:默认使用懒加载,按需急加载。
@Entity
public class Order {
@Id
private Long id;
// 默认懒加载
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// 默认懒加载
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
// 少量数据且总是需要,可以急加载
@OneToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "shipping_address_id")
private ShippingAddress shippingAddress;
}
避免懒加载初始化异常:
// 方式一:在事务内加载
@Transactional(readOnly = true)
public OrderDTO getOrderDetail(Long id) {
Order order = orderRepository.findByIdWithCustomerAndItems(id);
return convertToDTO(order);
}
// 方式二:使用 JOIN FETCH
@Query("SELECT o FROM Order o " +
"LEFT JOIN FETCH o.customer " +
"LEFT JOIN FETCH o.items " +
"WHERE o.id = :id")
Optional<Order> findByIdWithDetails(@Param("id") Long id);
查询优化技巧
1. 只查询需要的字段:
// 不推荐:查询所有字段
@Query("SELECT u FROM User u WHERE u.status = :status")
List<User> findByStatus(@Param("status") String status);
// 推荐:只查询需要的字段
@Query("SELECT new com.example.dto.UserDTO(u.id, u.name, u.email) " +
"FROM User u WHERE u.status = :status")
List<UserDTO> findDTOByStatus(@Param("status") String status);
2. 使用分页避免大量数据加载:
// 推荐:使用分页
Page<User> findByStatus(String status, Pageable pageable);
// 使用示例
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<User> page = userRepository.findByStatus("ACTIVE", pageable);
3. 避免使用 SELECT *:
// 不推荐
@Query("SELECT p FROM Product p")
List<Product> findAllProducts();
// 推荐:明确指定字段
@Query("SELECT p.id, p.name, p.price FROM Product p WHERE p.active = true")
List<Object[]> findActiveProducts();
多数据源完整配置
在企业应用中,经常需要同时访问多个数据库。Spring Boot 支持灵活的多数据源配置。
场景分析
┌─────────────────────────────────────────────────────────────────────┐
│ 多数据源常见场景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 读写分离:主库写入,从库读取 │
│ │
│ 应用 ──写入──> 主库(Master) │
│ │ │
│ └──读取──> 从库(Slave1, Slave2...) │
│ │
│ 2. 业务分库:不同业务使用不同数据库 │
│ │
│ 订单模块 ──> 订单数据库 │
│ 用户模块 ──> 用户数据库 │
│ 日志模块 ──> 日志数据库 │
│ │
│ 3. 多租户:不同租户使用不同数据库 │
│ │
│ 租户A ──> 租户A数据库 │
│ 租户B ──> 租户B数据库 │
│ │
└─────────────────────────────────────────────────────────────────────┘
基础多数据源配置
步骤一:配置文件
spring:
datasource:
primary:
jdbc-url: jdbc:mysql://localhost:3306/primary_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
secondary:
jdbc-url: jdbc:mysql://localhost:3306/secondary_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 2
jpa:
hibernate:
ddl-auto: update
show-sql: true
步骤二:主数据源配置
package com.example.config;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
/**
* 主数据源配置
* 管理用户相关的实体和 Repository
*/
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary", // Repository 所在包
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {
/**
* 配置主数据源
*/
@Bean
@Primary // 标记为主数据源
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 配置主数据源的 EntityManager
*/
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
DataSource primaryDataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(primaryDataSource);
em.setPackagesToScan("com.example.entity.primary"); // 实体类所在包
em.setPersistenceUnitName("primary");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
// JPA 属性配置
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", true);
properties.put("hibernate.format_sql", true);
em.setJpaPropertyMap(properties);
return em;
}
/**
* 配置主数据源的事务管理器
*/
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(
LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(primaryEntityManagerFactory.getObject());
return transactionManager;
}
}
步骤三:次数据源配置
package com.example.config;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
/**
* 次数据源配置
* 管理订单相关的实体和 Repository
*/
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.secondary",
entityManagerFactoryRef = "secondaryEntityManagerFactory",
transactionManagerRef = "secondaryTransactionManager"
)
public class SecondaryDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory(
DataSource secondaryDataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(secondaryDataSource);
em.setPackagesToScan("com.example.entity.secondary");
em.setPersistenceUnitName("secondary");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", "update");
properties.put("hibernate.show_sql", true);
em.setJpaPropertyMap(properties);
return em;
}
@Bean
public PlatformTransactionManager secondaryTransactionManager(
LocalContainerEntityManagerFactoryBean secondaryEntityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(secondaryEntityManagerFactory.getObject());
return transactionManager;
}
}
步骤四:实体类组织
src/main/java/com/example/
├── entity/
│ ├── primary/ # 主数据源实体
│ │ └── User.java
│ └── secondary/ # 次数据源实体
│ └── Order.java
└── repository/
├── primary/ # 主数据源 Repository
│ └── UserRepository.java
└── secondary/ # 次数据源 Repository
└── OrderRepository.java
跨数据源事务
当需要在一个事务中操作多个数据源时,需要使用分布式事务或链式事务:
方式一:JTA 分布式事务
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
import org.springframework.transaction.annotation.Transactional;
import com.example.service.primary.UserService;
import com.example.service.secondary.OrderService;
@Service
public class BusinessService {
private final UserService userService;
private final OrderService orderService;
/**
* JTA 分布式事务
* 保证跨数据源操作的原子性
*/
@Transactional
public void createUserAndOrder(UserDTO userDTO, OrderDTO orderDTO) {
// 操作主数据源
userService.createUser(userDTO);
// 操作次数据源
orderService.createOrder(orderDTO);
// 任何一步失败,两个数据源都会回滚
}
}
方式二:编程式事务
@Service
public class BusinessService {
private final PlatformTransactionManager primaryTransactionManager;
private final PlatformTransactionManager secondaryTransactionManager;
private final UserService userService;
private final OrderService orderService;
/**
* 使用 TransactionTemplate 手动管理事务
*/
public void createUserAndOrder(UserDTO userDTO, OrderDTO orderDTO) {
TransactionTemplate primaryTx = new TransactionTemplate(primaryTransactionManager);
TransactionTemplate secondaryTx = new TransactionTemplate(secondaryTransactionManager);
primaryTx.execute(primaryStatus -> {
try {
userService.createUser(userDTO);
secondaryTx.execute(secondaryStatus -> {
orderService.createOrder(orderDTO);
return null;
});
return null;
} catch (Exception e) {
primaryStatus.setRollbackOnly();
throw e;
}
});
}
}
读写分离实现
读写分离是提升数据库性能的常见方案:
/**
* 动态数据源路由
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
/**
* 数据源上下文持有者
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static final String MASTER = "master";
public static final String SLAVE = "slave";
public static void setDataSourceType(String type) {
contextHolder.set(type);
}
public static String getDataSourceType() {
return contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
/**
* 自定义注解标记读写操作
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
/**
* AOP 切面自动切换数据源
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {
@Before("@annotation(readOnly)")
public void beforeReadOnly(ReadOnly readOnly) {
DataSourceContextHolder.setDataSourceType(DataSourceContextHolder.SLAVE);
}
@After("@annotation(readOnly)")
public void afterReadOnly(ReadOnly readOnly) {
DataSourceContextHolder.clearDataSourceType();
}
}
/**
* 配置读写分离数据源
*/
@Configuration
public class ReadWriteDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
}
使用示例:
@Service
public class UserService {
// 读操作使用从库
@ReadOnly
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
@ReadOnly
public List<User> findAll() {
return userRepository.findAll();
}
// 写操作使用主库(默认)
public User save(User user) {
return userRepository.save(user);
}
}
数据库性能优化最佳实践
1. 索引优化
@Entity
@Table(name = "t_user", indexes = {
@Index(name = "idx_user_email", columnList = "email"),
@Index(name = "idx_user_status_created", columnList = "status, created_at")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String status;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
索引设计原则:
- 为经常用于查询条件的字段创建索引
- 为排序和分组字段创建索引
- 避免为低选择性字段(如性别)创建索引
- 复合索引注意字段顺序(最左前缀原则)
2. 批量操作优化
配置批量操作:
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50 # 批量大小
batch_versioned_data: true
order_inserts: true # 排序插入
order_updates: true # 排序更新
批量插入示例:
@Transactional
public void batchInsert(List<User> users) {
for (int i = 0; i < users.size(); i++) {
entityManager.persist(users.get(i));
// 每50条刷新一次
if (i % 50 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
3. 连接池监控与调优
@Component
@Slf4j
public class DataSourceMonitor {
@Autowired
private DataSource dataSource;
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void monitorConnectionPool() {
if (dataSource instanceof HikariDataSource) {
HikariPoolMXBean pool = ((HikariDataSource) dataSource).getHikariPoolMXBean();
log.info("连接池状态 - 活跃: {}, 空闲: {}, 等待: {}, 总数: {}",
pool.getActiveConnections(),
pool.getIdleConnections(),
pool.getThreadsAwaitingConnection(),
pool.getTotalConnections());
// 告警:如果活跃连接超过80%
double usageRate = (double) pool.getActiveConnections() / pool.getTotalConnections();
if (usageRate > 0.8) {
log.warn("连接池使用率过高: {}%", usageRate * 100);
}
}
}
}
4. 查询超时配置
全局配置:
spring:
jpa:
properties:
hibernate:
query:
timeout: 30 # 查询超时秒数
单查询配置:
@QueryHints(@QueryHint(name = "javax.persistence.query.timeout", value = "30000"))
List<User> findActiveUsers();
5. 二级缓存配置
启用二级缓存:
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
region:
factory_class: org.hibernate.cache.jcache.JCacheRegionFactory
javax:
persistence:
sharedCache:
mode: ALL
实体类缓存配置:
@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Dictionary {
// 字典等很少变化的数据适合缓存
}
6. 数据库连接健康检查
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try (Connection conn = dataSource.getConnection()) {
// 执行简单查询验证连接
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1")) {
if (rs.next()) {
return Health.up()
.withDetail("database", "MySQL")
.withDetail("validationQuery", "SELECT 1")
.build();
}
}
} catch (SQLException e) {
return Health.down()
.withException(e)
.withDetail("error", e.getMessage())
.build();
}
return Health.unknown().build();
}
}
小结
本章我们学习了:
- 数据源配置:单数据源和多数据源配置
- Spring Data JPA:实体映射、Repository 接口、分页查询
- MyBatis 集成:Mapper 接口、XML 配置、注解方式
- 事务管理:声明式事务、传播行为、隔离级别
- 数据库迁移:Flyway 和 Liquibase 的使用
- HikariCP 连接池优化:核心参数、连接泄露检测、JMX 监控
- JPA 性能优化:N+1 问题解决、懒加载最佳实践、EntityGraph 使用
- 多数据源配置:完整的多数据源配置、跨数据源事务、读写分离
- 数据库性能优化:索引优化、批量操作、缓存配置
练习
- 配置一个 MySQL 数据源并使用 JPA 创建实体类
- 实现一个完整的 CRUD Repository
- 使用 MyBatis 实现动态 SQL 查询
- 实现一个涉及多表操作的事务方法
- 使用 Flyway 管理数据库版本
- 配置多数据源并实现跨数据源事务
- 解决 JPA 的 N+1 查询问题
- 实现读写分离配置