数据访问
本章将介绍 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
小结
本章我们学习了:
- 数据源配置:单数据源和多数据源配置
- Spring Data JPA:实体映射、Repository 接口、分页查询
- MyBatis 集成:Mapper 接口、XML 配置、注解方式
- 事务管理:声明式事务、传播行为、隔离级别
- 数据库迁移:Flyway 和 Liquibase 的使用
练习
- 配置一个 MySQL 数据源并使用 JPA 创建实体类
- 实现一个完整的 CRUD Repository
- 使用 MyBatis 实现动态 SQL 查询
- 实现一个涉及多表操作的事务方法
- 使用 Flyway 管理数据库版本