数据访问:JPA 与 MyBatis
数据访问:JPA 与 MyBatis
数据持久层是任何业务系统的核心。Spring Boot 支持多种持久化方案,其中 Spring Data JPA 和 MyBatis-Plus 是最流行的两种选择。本文将系统讲解两者的用法、优劣对比,以及事务管理的精髓。
数据源配置
Spring Boot 自动配置 DataSource,只需在 application.yml 中声明连接信息:
spring:
datasource:
url: jdbc:mysql://localhost:3306/blog?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: secret
driver-class-name: com.mysql.cj.jdbc.Driver
# HikariCP 连接池(默认已自动配置,性能最优)
hikari:
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000Spring Boot 3.x 默认使用 HikariCP 连接池(性能优于 Tomcat JDBC 和 DBCP2),无需额外引入依赖。
Spring Data JPA
Spring Data JPA 是 Spring Data 家族的一员,它通过接口方法名推导机制大幅简化了 DAO 层的代码量。
依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Boot 3 使用 Jakarta Persistence API -->
<!-- 不再使用 javax.persistence.*,而是 jakarta.persistence.* -->实体映射
package com.example.demo.entity;
import jakarta.persistence.*; // 注意:Spring Boot 3 使用 jakarta.* 而非 javax.*
@Entity
@Table(name = "t_user", indexes = {
@Index(name = "idx_email", columnList = "email"),
@Index(name = "idx_status", columnList = "status")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(length = 100)
private String email;
@Column(length = 20)
private String phone;
@Enumerated(EnumType.STRING) // 存储为字符串而非数字
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime updatedAt;
@Version // 乐观锁版本字段,防止并发更新冲突
private Long version;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// getters and setters (或使用 record)
}
public enum UserStatus {
ACTIVE, INACTIVE, BANNED
}Repository 接口
Spring Data JPA 的核心魔力在于只需要声明方法签名,框架自动实现:
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// ==================== 方法名推导(自动实现)====================
// SELECT * FROM t_user WHERE email = ?
Optional<User> findByEmail(String email);
// SELECT * FROM t_user WHERE username LIKE ? AND status = ?
List<User> findByUsernameContainingAndStatus(String username, UserStatus status);
// SELECT * FROM t_user WHERE status = ? ORDER BY created_at DESC
List<User> findByStatusOrderByCreatedAtDesc(UserStatus status);
// SELECT * FROM t_user WHERE status IN (?, ?)
List<User> findByStatusIn(List<UserStatus> statuses);
// SELECT COUNT(*) FROM t_user WHERE status = ?
long countByStatus(UserStatus status);
// SELECT EXISTS(SELECT 1 FROM t_user WHERE email = ?)
boolean existsByEmail(String email);
// SELECT * FROM t_user WHERE age BETWEEN ? AND ?
List<User> findByAgeBetween(int minAge, int maxAge);
// DELETE FROM t_user WHERE status = ?
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteByStatus(@Param("status") UserStatus status);
// ==================== @Query 自定义查询 ====================
// JPQL(操作实体而非表)
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findByEmailAndStatus(
@Param("email") String email,
@Param("status") UserStatus status);
// 原生 SQL
@Query(value = """
SELECT u.* FROM t_user u
LEFT JOIN t_order o ON u.id = o.user_id
WHERE o.amount > :amount
""",
countQuery = "SELECT COUNT(*) FROM t_user u LEFT JOIN t_order o ON u.id = o.user_id WHERE o.amount > :amount",
nativeQuery = true)
Page<User> findActiveBuyers(@Param("amount") BigDecimal amount, Pageable pageable);
// @Query + @Modifying 执行更新操作
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.lastLoginAt < :cutoff")
int deactivateStaleUsers(
@Param("status") UserStatus status,
@Param("cutoff") LocalDateTime cutoff);
// 分页查询(自动 COUNT)
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
Page<User> search(@Param("keyword") String keyword, Pageable pageable);
// 投影查询:只查部分字段,减少数据传输
@Query("SELECT new com.example.demo.dto.UserBrief(u.id, u.username, u.email) FROM User u WHERE u.status = :status")
List<UserBrief> findBriefByStatus(@Param("status") UserStatus status);
}JpaSpecificationExecutor 动态查询
当查询条件需要动态组合时,JpaSpecificationExecutor 提供了构建 Specification 的能力:
@Service
public class UserSearchService {
@Autowired
private UserRepository userRepository;
public Page<User> search(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (criteria.getUsername() != null) {
predicates.add(cb.like(root.get("username"), "%" + criteria.getUsername() + "%"));
}
if (criteria.getEmail() != null) {
predicates.add(cb.equal(root.get("email"), criteria.getEmail()));
}
if (criteria.getStatus() != null) {
predicates.add(cb.equal(root.get("status"), criteria.getStatus()));
}
if (criteria.getCreatedAfter() != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("createdAt"), criteria.getCreatedAfter()));
}
query.where(predicates.toArray(new Predicate[0]));
query.orderBy(cb.desc(root.get("createdAt")));
return query.getRestriction();
};
return userRepository.findAll(spec, pageable);
}
}MyBatis-Plus
MyBatis-Plus 是 MyBatis 的增强工具,提供无 XML 的 CRUD 接口和强大的条件构造器。
依赖与配置
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true实体映射
@MapperScan("com.example.demo.mapper") // 启动类或配置类上标注
@TableName("t_article")
public class Article {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("title")
private String title;
@TableField("content")
private String content;
@TableField("author_id")
private Long authorId;
@TableField("status")
private String status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic // 逻辑删除:执行 UPDATE SET deleted = 1 而非 DELETE
private Integer deleted;
}Mapper 接口
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
// MyBatis-Plus 已提供 CRUD 方法(save/saveBatch/updateById/removeById 等)
// 可直接注入 ArticleService 使用,或注入 BaseMapper<Article>
// 自定义查询:XML 或注解
List<Article> findByAuthorId(@Param("authorId") Long authorId);
@Select("SELECT * FROM t_article WHERE status = #{status} ORDER BY created_at DESC LIMIT #{limit}")
List<Article> findRecentByStatus(@Param("status") String status, @Param("limit") int limit);
}Lambda 条件构造器
MyBatis-Plus 的 LambdaQueryWrapper 让你用方法引用而非字符串字段名写查询条件:
@Service
public class ArticleServiceImpl {
@Autowired
private ArticleMapper articleMapper;
public IPage<Article> search(String keyword, String status, long page, long size) {
return articleMapper.selectPage(
new Page<>(page, size),
new LambdaQueryWrapper<Article>()
.like(keyword != null, Article::getTitle, keyword)
.or()
.like(keyword != null, Article::getContent, keyword)
.eq(status != null, Article::getStatus, status)
.orderByDesc(Article::getCreatedAt)
);
}
public void publish(Long id, Long authorId) {
// 链式更新
articleMapper.update(null, new LambdaUpdateWrapper<Article>()
.eq(Article::getId, id)
.eq(Article::getAuthorId, authorId)
.set(Article::getStatus, "PUBLISHED")
.set(Article::getUpdatedAt, LocalDateTime.now())
);
}
}分页插件
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}JdbcTemplate 原始查询
对于简单场景,直接使用 JdbcTemplate 更轻量:
@Service
public class StatService {
private final JdbcTemplate jdbcTemplate;
public StatService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public long countUsers() {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM t_user", Long.class);
}
public List<User> findActiveUsers() {
return jdbcTemplate.query(
"SELECT * FROM t_user WHERE status = 'ACTIVE'",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("username"),
rs.getString("email")
)
);
}
public int updateStatus(Long id, String status) {
return jdbcTemplate.update(
"UPDATE t_user SET status = ? WHERE id = ?",
status, id
);
}
}事务管理
Spring 的声明式事务通过 @Transactional 注解实现,是最常用的方式。
@Transactional 基础
@Service
public class TransferService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountRepository.deduct(fromId, amount); // 扣款
accountRepository.deposit(toId, amount); // 存款
notificationService.notify(fromId, toId, amount); // 发送通知
// 若任意一步抛异常,整个事务自动回滚
}
}事务传播行为
事务传播行为定义了当一个事务方法被另一个事务方法调用时,事务如何传播:
| 传播行为 | 说明 |
|---|---|
REQUIRED(默认) | 加入现有事务,若无则创建新事务 |
REQUIRES_NEW | 每次都创建新事务,挂起现有事务 |
SUPPORTS | 有事务则加入,无则非事务执行 |
MANDATORY | 必须在现有事务中运行,否则抛异常 |
NOT_SUPPORTED | 非事务执行,挂起现有事务 |
NEVER | 非事务执行,若有事务则抛异常 |
NESTED | 嵌套事务(MySQL Savepoint) |
@Service
public class OrderService {
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
inventoryService.reserve(order.getItems()); // REQUIRES_NEW:独立事务
paymentService.process(order.getPayment()); // 加入当前事务
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reserve(List<OrderItem> items) {
// 无论 OrderService 事务是否成功,此事务独立提交或回滚
items.forEach(item -> itemRepository.decrementStock(item));
}
}事务隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// READ_UNCOMMITTED:读未提交(可能有脏读)
// READ_COMMITTED:读已提交(Oracle 默认)
// REPEATABLE_READ:可重复读(MySQL InnoDB 默认)
// SERIALIZABLE:串行化(最高隔离,性能最差)
}事务失效场景
@Transactional 在以下场景会失效:
@Service
public class BadExample {
// ❌ private 方法:Spring AOP 无法代理 private 方法
@Transactional
private void doSomething() { ... }
// ❌ 自调用:同类内部方法调用不走代理
@Transactional
public void methodA() {
methodB(); // this.methodB(),不走 Spring 代理,事务不生效
}
@Transactional
public void methodB() { ... }
// ❌ 异常被 catch 吞掉:不会触发回滚
@Transactional
public void methodC() {
try {
doSomething();
} catch (Exception e) {
// 异常被吞掉,事务不会回滚
}
}
// ✅ 解决方案1:注入自身(代理自调用)
@Autowired
private BadExample self;
public void fixedMethodA() {
self.methodB(); // 通过代理调用,事务生效
}
// ✅ 解决方案2:抛出原异常
@Transactional(rollbackFor = Exception.class)
public void fixedMethodC() throws Exception {
try {
doSomething();
} catch (Exception e) {
throw new RuntimeException(e); // 原异常被重新抛出
}
}
}JPA vs MyBatis-Plus 选型
| 维度 | Spring Data JPA | MyBatis-Plus |
|---|---|---|
| SQL 编写 | 自动推导,JPAQL | 手写 SQL,条件构造器辅助 |
| 学习曲线 | 较陡(需理解 JPA 规范) | 平缓(熟悉 SQL 即可) |
| 灵活性 | 一般 | 高(完全掌控 SQL) |
| 动态 SQL | 较弱 | 强大(XML / 注解 / Lambda) |
| 生态 | Spring 官方 | 社区活跃,国产友好 |
| 适用场景 | 复杂对象模型,变更频繁 | 复杂查询,性能敏感场景 |
小结
- Spring Boot 自动配置 HikariCP 连接池,只需声明
spring.datasource配置 - Spring Data JPA 通过方法名推导和
@Query注解大幅简化 DAO 层 @Enumerated(EnumType.STRING)避免枚举存储为数字导致可读性问题@Version字段实现乐观锁,防止并发更新覆盖- MyBatis-Plus 的
LambdaQueryWrapper用方法引用替代字符串字段名,防误写 @Transactional默认传播行为是REQUIRED,默认隔离级别是数据库默认值- 事务失效的三大场景:private 方法、自调用、异常被吞
下一篇文章我们将学习 多环境配置与外部化配置,掌握 Spring Boot 配置体系的精髓。
评论
Written by
AI-Writer
Related Articles
依赖注入与 Bean 管理
深入理解 Spring IoC 容器核心概念、@Bean/@Component/@Configuration 注解使用、构造器注入与 Setter 注入、@Autowired 与 @Qualifier 精确注入、Bean 作用域与优先级控制
Read More数据访问:JPA 与 MyBatis
Spring Data JPA 实体映射与 Repository 接口、MyBatis-Plus 增强用法、JdbcTemplate 原始查询、事务管理(@Transactional 传播行为与隔离级别)完整指南
Read MoreSpring Security 安全认证
Spring Security 6.x 核心架构、认证与授权概念、基于 JWT 的无状态登录实现、OAuth 2.0 资源服务器入门、@PreAuthorize 方法级安全注解
Read More