Spring Data JPA详解
本章导读
Spring Data JPA 基于 JPA 规范,通过 Repository 抽象实现了"零代码"数据访问,极大简化了 CRUD 操作。本章系统讲解 JPA 的实体映射、Repository 接口、JPQL 查询和 Specification 动态查询,帮助你掌握 ORM 框架的核心思想和最佳实践。
学习目标:
- 目标1:掌握 JPA 核心注解和实体关联映射的正确配置
- 目标2:熟练使用 Repository 方法命名查询和 @Query 注解
- 目标3:理解 Specification 动态查询原理,解决 N+1 问题并优化性能
前置知识:JDBC 基础、SQL 语法、面向对象思想
阅读时长:约 45 分钟
一、知识概述
Spring Data JPA 是 Spring Data 家族的重要组成部分,它基于 JPA(Java Persistence API)规范,通过极其简化的编程模型,大大减少了数据访问层的样板代码。Spring Data JPA 的核心理念是"Repository 抽象",开发者只需定义接口,框架自动提供实现,实现"零代码"数据访问。
本文将深入讲解 Spring Data JPA 的核心概念、实体映射、Repository 接口、JPQL 查询、Specification 动态查询等内容,帮助你掌握这个强大的 ORM 框架。
二、快速入门
2.1 引入依赖
<!-- Spring Boot 项目引入 Spring Data JPA -->
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.2 配置文件
# application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/jpa_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# HikariCP 连接池配置
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 120000
connection-timeout: 30000
# JPA 配置
jpa:
# 数据库类型
database: mysql
# 显示 SQL
show-sql: true
# 格式化 SQL
properties:
hibernate:
format_sql: true
# 二级缓存
cache:
use_second_level_cache: false
# 统计信息
generate_statistics: true
# DDL 自动生成策略
hibernate:
ddl-auto: update
# 可选值:
# - none: 不做任何操作
# - validate: 验证表结构,不匹配则报错
# - update: 更新表结构(不会删除已存在的列)
# - create: 每次启动创建表(会删除旧表)
# - create-drop: 启动创建,关闭删除
# Open Session In View(生产环境建议关闭)
open-in-view: false
2.3 快速使用
/**
* Spring Data JPA 快速入门
*/
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.example.repository")
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
}
}
/**
* 实体类
*/
@Entity
@Table(name = "sys_user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", length = 50, nullable = false, unique = true)
private String username;
@Column(name = "email", length = 100)
private String email;
@Column(name = "age")
private Integer age;
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 20)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
// 生命周期回调
@PrePersist
public void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updateTime = LocalDateTime.now();
}
}
/**
* 用户状态枚举
*/
public enum UserStatus {
ACTIVE, INACTIVE, DELETED
}
/**
* Repository 接口 - 继承 JpaRepository 即可获得 CRUD 能力
*/
public interface UserRepository extends JpaRepository<User, Long> {
// JpaRepository 已提供:
// - save(S entity) / saveAll(Iterable<S> entities)
// - findById(ID id) / findAll() / findAllById(Iterable<ID> ids)
// - count() / existsById(ID id)
// - deleteById(ID id) / delete(T entity) / deleteAll()
// - findAll(Sort sort) / findAll(Pageable pageable)
// 自定义查询方法
User findByUsername(String username);
List<User> findByStatus(UserStatus status);
List<User> findByAgeBetween(Integer min, Integer max);
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain")
List<User> findByEmailDomain(@Param("domain") String domain);
}
/**
* Service 层
*/
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public User create(User user) {
return userRepository.save(user);
}
public User update(User user) {
return userRepository.save(user);
}
public void delete(Long id) {
userRepository.deleteById(id);
}
public User getById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found"));
}
public List<User> listAll() {
return userRepository.findAll();
}
public Page<User> listPage(int page, int size) {
return userRepository.findAll(PageRequest.of(page, size));
}
}
/**
* 测试使用
*/
@SpringBootTest
public class QuickStartTest {
@Autowired
private UserRepository userRepository;
@Test
public void testInsert() {
User user = new User();
user.setUsername("zhangsan");
user.setEmail("zhangsan@example.com");
user.setAge(25);
user.setStatus(UserStatus.ACTIVE);
User saved = userRepository.save(user);
System.out.println("保存成功: " + saved);
}
@Test
public void testSelect() {
// 根据 ID 查询
Optional<User> optional = userRepository.findById(1L);
optional.ifPresent(System.out::println);
// 查询所有
List<User> users = userRepository.findAll();
users.forEach(System.out::println);
// 分页查询
Page<User> page = userRepository.findAll(PageRequest.of(0, 10));
System.out.println("总记录数: " + page.getTotalElements());
System.out.println("总页数: " + page.getTotalPages());
System.out.println("当前页数据: " + page.getContent());
}
@Test
public void testUpdate() {
User user = userRepository.findById(1L).orElse(null);
if (user != null) {
user.setAge(26);
userRepository.save(user);
}
}
@Test
public void testDelete() {
userRepository.deleteById(1L);
}
}
三、实体映射详解
3.1 基础注解
/**
* JPA 实体映射基础注解
*/
@Entity
@Table(name = "sys_user",
schema = "mydb",
indexes = {
@Index(name = "idx_username", columnList = "username", unique = true),
@Index(name = "idx_create_time", columnList = "create_time")
},
uniqueConstraints = {
@UniqueConstraint(name = "uk_email", columnNames = {"email"})
})
@Data
public class User {
/**
* @Id - 主键
* @GeneratedValue - 主键生成策略
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// GenerationType 可选值:
// - IDENTITY: 数据库自增(MySQL)
// - SEQUENCE: 序列(Oracle)
// - TABLE: 使用单独的表生成主键
// - AUTO: JPA 自动选择(默认)
private Long id;
/**
* @Column - 字段映射
*/
@Column(name = "username", // 数据库列名
length = 50, // 长度
nullable = false, // 是否允许 null
unique = true, // 是否唯一
insertable = true, // 是否参与 insert
updatable = true, // 是否参与 update
columnDefinition = "VARCHAR(50) COMMENT '用户名'") // DDL 定义
private String username;
/**
* @Enumerated - 枚举映射
*/
@Enumerated(EnumType.STRING) // 存储枚举名称
// EnumType.ORDINAL: 存储枚举序号(默认,不推荐)
// EnumType.STRING: 存储枚举名称
@Column(name = "status")
private UserStatus status;
/**
* @Temporal - 日期时间映射
*/
@Temporal(TemporalType.TIMESTAMP)
// TemporalType.DATE: 只存日期
// TemporalType.TIME: 只存时间
// TemporalType.TIMESTAMP: 日期时间
@Column(name = "create_time")
private Date createTime;
/**
* @Lob - 大对象
*/
@Lob
@Column(name = "avatar", columnDefinition = "LONGBLOB")
private byte[] avatar;
@Lob
@Column(name = "description", columnDefinition = "LONGTEXT")
private String description;
/**
* @Transient - 忽略映射
*/
@Transient
private String extraField;
/**
* @Basic - 基本配置
*/
@Basic(fetch = FetchType.LAZY) // 延迟加载
@Column(name = "large_content")
private String largeContent;
}
/**
* 复合主键
*/
@Data
@Embeddable
public class OrderItemId implements Serializable {
@Column(name = "order_id")
private Long orderId;
@Column(name = "product_id")
private Long productId;
}
@Entity
@Table(name = "order_item")
@Data
public class OrderItem {
@EmbeddedId
private OrderItemId id;
@Column(name = "quantity")
private Integer quantity;
@Column(name = "price")
private BigDecimal price;
}
// 另一种方式:@IdClass
@Entity
@Table(name = "order_item2")
@IdClass(OrderItemId.class)
@Data
public class OrderItem2 {
@Id
@Column(name = "order_id")
private Long orderId;
@Id
@Column(name = "product_id")
private Long productId;
@Column(name = "quantity")
private Integer quantity;
}
3.2 关联映射
/**
* 一对一关联
*/
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
/**
* 一对一 - 主表方
*/
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", referencedColumnName = "id")
// name: 外键列名(在当前表)
// referencedColumnName: 关联列名(在关联表)
private UserProfile profile;
}
@Entity
@Table(name = "user_profile")
@Data
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String realName;
private String phone;
/**
* 一对一 - 从表方(可选)
*/
@OneToOne(mappedBy = "profile", fetch = FetchType.LAZY)
private User user;
}
/**
* 一对多关联
*/
@Entity
@Table(name = "dept")
@Data
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
/**
* 一对多 - 部门方
*/
@OneToMany(mappedBy = "department",
fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
orphanRemoval = true) // 孤儿删除
private List<User> users = new ArrayList<>();
}
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
/**
* 多对一 - 用户方
*/
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
}
/**
* 多对多关联
*/
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
private String username;
/**
* 多对多 - User 方(维护方)
*/
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
@Entity
@Table(name = "role")
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Include
private Long id;
private String name;
/**
* 多对多 - Role 方(被维护方)
*/
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private Set<User> users = new HashSet<>();
}
/**
* 关联映射最佳实践
*/
@SpringBootTest
public class AssociationMappingTest {
@Autowired
private UserRepository userRepository;
@Autowired
private DepartmentRepository departmentRepository;
/**
* 级联操作
*/
@Test
public void testCascade() {
// 创建部门
Department dept = new Department();
dept.setName("技术部");
// 创建用户
User user1 = new User();
user1.setUsername("张三");
user1.setDepartment(dept);
User user2 = new User();
user2.setUsername("李四");
user2.setDepartment(dept);
dept.setUsers(Arrays.asList(user1, user2));
// 保存部门(级联保存用户)
departmentRepository.save(dept);
}
/**
* 多对多操作
*/
@Test
public void testManyToMany() {
User user = userRepository.findById(1L).orElse(null);
Role role = new Role();
role.setId(1L); // 假设角色已存在
// 添加角色
user.getRoles().add(role);
userRepository.save(user);
// 移除角色
user.getRoles().remove(role);
userRepository.save(user);
}
/**
* N+1 问题与解决
*/
// 问题代码
@Test
public void testNPlusOneProblem() {
List<User> users = userRepository.findAll();
for (User user : users) {
// 每次 getDepartment() 都会发起一次查询
System.out.println(user.getDepartment().getName());
}
}
// 解决方案1:JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.department")
List<User> findAllWithDepartment();
// 解决方案2:EntityGraph
@EntityGraph(attributePaths = {"department"})
List<User> findAll();
// 解决方案3:批量抓取
@BatchSize(size = 100)
@OneToMany(mappedBy = "department")
private List<User> users;
}
3.3 继承映射
/**
* JPA 继承映射策略
*/
/**
* 1. SINGLE_TABLE - 单表继承(默认)
* 所有子类存储在同一张表,使用区分列
*/
@Entity
@Table(name = "payment")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "payment_type",
discriminatorType = DiscriminatorType.STRING)
@Data
public abstract class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal amount;
private LocalDateTime payTime;
}
@Entity
@DiscriminatorValue("ALIPAY")
@Data
@EqualsAndHashCode(callSuper = true)
public class AlipayPayment extends Payment {
private String alipayAccount;
}
@Entity
@DiscriminatorValue("WECHAT")
@Data
@EqualsAndHashCode(callSuper = true)
public class WechatPayment extends Payment {
private String wechatOpenId;
}
/**
* 2. JOINED - 联合表继承
* 父类和子类各一张表,子类表只存储特有字段
*/
@Entity
@Table(name = "vehicle")
@Inheritance(strategy = InheritanceType.JOINED)
@Data
public abstract class Vehicle {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String brand;
private Double price;
}
@Entity
@Table(name = "car")
@PrimaryKeyJoinColumn(name = "vehicle_id")
@Data
@EqualsAndHashCode(callSuper = true)
public class Car extends Vehicle {
private Integer seatCount;
}
@Entity
@Table(name = "truck")
@PrimaryKeyJoinColumn(name = "vehicle_id")
@Data
@EqualsAndHashCode(callSuper = true)
public class Truck extends Vehicle {
private Double loadCapacity;
}
/**
* 3. TABLE_PER_CLASS - 每个类一张表
* 每个具体类一张表,包含所有字段(包括父类字段)
*/
@Entity
@Table(name = "document")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Data
public abstract class Document {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // 不能使用 IDENTITY
private Long id;
private String title;
private String content;
}
@Entity
@Table(name = "pdf_document")
@Data
@EqualsAndHashCode(callSuper = true)
public class PdfDocument extends Document {
private Integer pageCount;
}
@Entity
@Table(name = "word_document")
@Data
@EqualsAndHashCode(callSuper = true)
public class WordDocument extends Document {
private String author;
}
/**
* 4. MappedSuperclass - 映射父类(非实体)
* 父类不映射为表,只用于属性继承
*/
@MappedSuperclass
@Data
public abstract class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
@Version
private Integer version;
@PrePersist
protected void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
@PreUpdate
protected void preUpdate() {
this.updateTime = LocalDateTime.now();
}
}
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends BaseEntity {
private String username;
private String email;
}
@Entity
@Table(name = "product")
@Data
@EqualsAndHashCode(callSuper = true)
public class Product extends BaseEntity {
private String name;
private BigDecimal price;
}
四、Repository 接口
4.1 Repository 层次结构
/**
* Spring Data JPA Repository 层次结构
*
* Repository (标记接口)
* └── CrudRepository (CRUD 操作)
* └── PagingAndSortingRepository (分页排序)
* └── JpaRepository (JPA 扩展)
*/
/**
* Repository 接口定义
*/
// 标记接口
public interface Repository<T, ID> { }
// CRUD 操作
public interface CrudRepository<T, ID> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
Optional<T> findById(ID id);
boolean existsById(ID id);
Iterable<T> findAll();
Iterable<T> findAllById(Iterable<ID> ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAllById(Iterable<? extends ID> ids);
void deleteAll(Iterable<? extends T> entities);
void deleteAll();
}
// 分页排序
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
}
// JPA 扩展
public interface JpaRepository<T, ID> extends
PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
// 批量操作
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities);
void deleteAllInBatch();
void deleteAllByIdInBatch(Iterable<ID> ids);
// 获取引用(延迟加载)
T getReferenceById(ID id);
// 刷新
void flush();
<S extends T> S saveAndFlush(S entity);
// 其他
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
}
/**
* 自定义 Repository
*/
// 自定义接口
public interface CustomUserRepository {
List<User> findActiveUsers();
void updateUserStatusBatch(List<Long> userIds, UserStatus status);
}
// 自定义实现
public class CustomUserRepositoryImpl implements CustomUserRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<User> findActiveUsers() {
return entityManager.createQuery(
"SELECT u FROM User u WHERE u.status = 'ACTIVE'", User.class)
.getResultList();
}
@Override
@Transactional
public void updateUserStatusBatch(List<Long> userIds, UserStatus status) {
entityManager.createQuery(
"UPDATE User u SET u.status = :status WHERE u.id IN :ids")
.setParameter("status", status)
.setParameter("ids", userIds)
.executeUpdate();
}
}
// 继承自定义接口
public interface UserRepository extends JpaRepository<User, Long>, CustomUserRepository {
// 现在可以使用 CustomUserRepository 中定义的方法
}
4.2 方法命名查询
/**
* 方法命名查询规则
*
* 格式:findBy + 属性名 + 关键字 + [And/Or] + 属性名 + 关键字
*/
public interface UserRepository extends JpaRepository<User, Long> {
// ========== 比较关键字 ==========
// 等于
User findByUsername(String username);
User findByUsernameEquals(String username);
// WHERE username = ?
// 不等于
List<User> findByStatusNot(UserStatus status);
// WHERE status <> ?
// Like
List<User> findByUsernameLike(String username);
// WHERE username LIKE ?
List<User> findByUsernameContaining(String username);
// WHERE username LIKE %?%
List<User> findByUsernameStartingWith(String prefix);
// WHERE username LIKE ?%
List<User> findByUsernameEndingWith(String suffix);
// WHERE username LIKE %?
// Between
List<User> findByAgeBetween(Integer min, Integer max);
// WHERE age BETWEEN ? AND ?
List<User> findByCreateTimeBetween(LocalDateTime start, LocalDateTime end);
// WHERE create_time BETWEEN ? AND ?
// LessThan / LessThanEqual
List<User> findByAgeLessThan(Integer age);
// WHERE age < ?
List<User> findByAgeLessThanEqual(Integer age);
// WHERE age <= ?
// GreaterThan / GreaterThanEqual
List<User> findByAgeGreaterThan(Integer age);
// WHERE age > ?
List<User> findByAgeGreaterThanEqual(Integer age);
// WHERE age >= ?
// In
List<User> findByStatusIn(List<UserStatus> statuses);
// WHERE status IN (?)
List<User> findByIdIn(List<Long> ids);
// WHERE id IN (?)
// NotIn
List<User> findByStatusNotIn(List<UserStatus> statuses);
// WHERE status NOT IN (?)
// Null / NotNull
List<User> findByEmailIsNull();
// WHERE email IS NULL
List<User> findByEmailNotNull();
// WHERE email IS NOT NULL
// True / False(适用于 boolean)
List<User> findByActiveTrue();
// WHERE active = true
List<User> findByActiveFalse();
// WHERE active = false
// ========== 排序 ==========
List<User> findByStatusOrderByCreateTimeDesc(UserStatus status);
// WHERE status = ? ORDER BY create_time DESC
List<User> findByStatusOrderByCreateTimeDescUsernameAsc(UserStatus status);
// WHERE status = ? ORDER BY create_time DESC, username ASC
// ========== 限制结果 ==========
User findFirstByUsername(String username);
// LIMIT 1
User findTopByUsername(String username);
// LIMIT 1
List<User> findFirst10ByStatus(UserStatus status);
// LIMIT 10
List<User> findTop5ByStatusOrderByCreateTimeDesc(UserStatus status);
// LIMIT 5
// ========== Distinct ==========
List<User> findDistinctByUsername(String username);
// SELECT DISTINCT ...
// ========== 逻辑操作符 ==========
List<User> findByUsernameAndStatus(String username, UserStatus status);
// WHERE username = ? AND status = ?
List<User> findByUsernameOrEmail(String username, String email);
// WHERE username = ? OR email = ?
// ========== 嵌套属性 ==========
List<User> findByDepartmentName(String deptName);
// WHERE department.name = ?
List<User> findByDepartmentId(Long deptId);
// WHERE department.id = ?
// ========== 分页和排序参数 ==========
List<User> findByStatus(UserStatus status, Sort sort);
Page<User> findByStatus(UserStatus status, Pageable pageable);
Slice<User> findByUsernameLike(String username, Pageable pageable);
// Slice 只查询当前页,不查 count
}
/**
* 使用示例
*/
@SpringBootTest
public class MethodQueryTest {
@Autowired
private UserRepository userRepository;
@Test
public void testMethodQuery() {
// 简单查询
User user = userRepository.findByUsername("zhangsan");
// Like 查询
List<User> users = userRepository.findByUsernameContaining("张");
// Between 查询
List<User> ageUsers = userRepository.findByAgeBetween(20, 30);
// In 查询
List<User> statusUsers = userRepository.findByStatusIn(
Arrays.asList(UserStatus.ACTIVE, UserStatus.INACTIVE));
// 分页查询
Page<User> page = userRepository.findByStatus(
UserStatus.ACTIVE,
PageRequest.of(0, 10, Sort.by("createTime").descending())
);
// 排序查询
List<User> sorted = userRepository.findByStatus(
UserStatus.ACTIVE,
Sort.by(Sort.Direction.DESC, "createTime")
.and(Sort.by(Sort.Direction.ASC, "username"))
);
}
}
4.3 @Query 查询
/**
* @Query 注解查询
*/
public interface UserRepository extends JpaRepository<User, Long> {
// ========== JPQL 查询 ==========
/**
* 基本 JPQL
*/
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsernameJPQL(@Param("username") String username);
/**
* 多条件查询
*/
@Query("SELECT u FROM User u WHERE u.status = :status AND u.age >= :minAge")
List<User> findActiveAdults(@Param("status") UserStatus status,
@Param("minAge") Integer minAge);
/**
* Like 查询
*/
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword%")
List<User> searchByUsername(@Param("keyword") String keyword);
/**
* 排序
*/
@Query("SELECT u FROM User u WHERE u.status = :status ORDER BY u.createTime DESC")
List<User> findByStatusSorted(@Param("status") UserStatus status);
/**
* 分页
*/
@Query("SELECT u FROM User u WHERE u.status = :status")
Page<User> findByStatusPaged(@Param("status") UserStatus status,
Pageable pageable);
/**
* 关联查询
*/
@Query("SELECT u FROM User u JOIN FETCH u.department WHERE u.id = :id")
User findByIdWithDepartment(@Param("id") Long id);
/**
* 统计
*/
@Query("SELECT COUNT(u) FROM User u WHERE u.status = :status")
long countByStatus(@Param("status") UserStatus status);
/**
* 聚合
*/
@Query("SELECT AVG(u.age) FROM User u WHERE u.status = :status")
Double avgAgeByStatus(@Param("status") UserStatus status);
/**
* 分组
*/
@Query("SELECT u.status, COUNT(u) FROM User u GROUP BY u.status")
List<Object[]> countGroupByStatus();
/**
* DTO 投影
*/
@Query("SELECT new com.example.dto.UserDTO(u.id, u.username, u.email) " +
"FROM User u WHERE u.status = :status")
List<UserDTO> findUserDTOs(@Param("status") UserStatus status);
// ========== 原生 SQL 查询 ==========
/**
* 原生 SQL
*/
@Query(value = "SELECT * FROM user WHERE username = :username",
nativeQuery = true)
User findByUsernameNative(@Param("username") String username);
/**
* 原生 SQL 分页
*/
@Query(value = "SELECT * FROM user WHERE status = :status",
countQuery = "SELECT COUNT(*) FROM user WHERE status = :status",
nativeQuery = true)
Page<User> findByStatusNative(@Param("status") String status,
Pageable pageable);
/**
* 原生 SQL 更新
*/
@Modifying
@Query(value = "UPDATE user SET status = :status WHERE id = :id",
nativeQuery = true)
int updateStatusNative(@Param("id") Long id, @Param("status") String status);
// ========== 更新查询 ==========
/**
* 更新
*/
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
/**
* 批量更新
*/
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
int batchUpdateStatus(@Param("ids") List<Long> ids,
@Param("status") UserStatus status);
/**
* 删除
*/
@Modifying
@Query("DELETE FROM User u WHERE u.status = :status")
int deleteByStatus(@Param("status") UserStatus status);
// ========== SpEL 表达式 ==========
/**
* 使用实体名称
*/
@Query("SELECT u FROM #{#entityName} u WHERE u.username = :username")
User findByUsernameSpEL(@Param("username") String username);
/**
* 使用 SpEL 动态条件
*/
@Query("SELECT u FROM User u WHERE " +
"(:username IS NULL OR u.username = :username) AND " +
"(:status IS NULL OR u.status = :status)")
List<User> findByConditions(@Param("username") String username,
@Param("status") UserStatus status);
}
/**
* 更新查询使用
*/
@SpringBootTest
public class QueryTest {
@Autowired
private UserRepository userRepository;
/**
* 更新操作需要 @Transactional
*/
@Test
@Transactional
public void testUpdate() {
int rows = userRepository.updateStatus(1L, UserStatus.INACTIVE);
System.out.println("更新行数: " + rows);
}
/**
* 批量更新
*/
@Test
@Transactional
public void testBatchUpdate() {
int rows = userRepository.batchUpdateStatus(
Arrays.asList(1L, 2L, 3L),
UserStatus.DELETED
);
System.out.println("更新行数: " + rows);
}
/**
* 清除持久化上下文(更新后刷新)
*/
@Test
@Transactional
@Modifying(clearAutomatically = true, flushAutomatically = true)
public void testUpdateWithClear() {
userRepository.updateStatus(1L, UserStatus.INACTIVE);
}
}
4.4 Specification 动态查询
/**
* Specification 动态查询
*/
public interface UserSpecificationRepository
extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
// JpaSpecificationExecutor 提供以下方法:
// Optional<T> findOne(@Nullable Specification<T> spec);
// List<T> findAll(@Nullable Specification<T> spec);
// Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
// List<T> findAll(@Nullable Specification<T> spec, Sort sort);
// long count(@Nullable Specification<T> spec);
// boolean exists(@Nullable Specification<T> spec);
}
/**
* Specification 工具类
*/
public class UserSpecifications {
/**
* 用户名模糊查询
*/
public static Specification<User> usernameLike(String username) {
return (root, query, cb) -> {
if (StringUtils.hasText(username)) {
return cb.like(root.get("username"), "%" + username + "%");
}
return null;
};
}
/**
* 状态等于
*/
public static Specification<User> statusEq(UserStatus status) {
return (root, query, cb) -> {
if (status != null) {
return cb.equal(root.get("status"), status);
}
return null;
};
}
/**
* 年龄范围
*/
public static Specification<User> ageBetween(Integer minAge, Integer maxAge) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (minAge != null) {
predicates.add(cb.ge(root.get("age"), minAge));
}
if (maxAge != null) {
predicates.add(cb.le(root.get("age"), maxAge));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
/**
* 部门 ID
*/
public static Specification<User> departmentIdEq(Long deptId) {
return (root, query, cb) -> {
if (deptId != null) {
return cb.equal(root.get("department").get("id"), deptId);
}
return null;
};
}
/**
* 创建时间范围
*/
public static Specification<User> createTimeBetween(
LocalDateTime start, LocalDateTime end) {
return (root, query, cb) -> {
if (start != null && end != null) {
return cb.between(root.get("createTime"), start, end);
}
return null;
};
}
/**
* 动态组合条件
*/
public static Specification<User> buildQuery(UserQuery query) {
return Specification
.where(usernameLike(query.getUsername()))
.and(statusEq(query.getStatus()))
.and(ageBetween(query.getMinAge(), query.getMaxAge()))
.and(departmentIdEq(query.getDeptId()))
.and(createTimeBetween(query.getStartTime(), query.getEndTime()));
}
}
/**
* 查询条件对象
*/
@Data
public class UserQuery {
private String username;
private UserStatus status;
private Integer minAge;
private Integer maxAge;
private Long deptId;
private LocalDateTime startTime;
private LocalDateTime endTime;
}
/**
* 使用示例
*/
@SpringBootTest
public class SpecificationTest {
@Autowired
private UserSpecificationRepository userRepository;
/**
* 基本使用
*/
@Test
public void testSpecification() {
Specification<User> spec = (root, query, cb) -> {
return cb.and(
cb.equal(root.get("status"), UserStatus.ACTIVE),
cb.like(root.get("username"), "%张%")
);
};
List<User> users = userRepository.findAll(spec);
}
/**
* 使用工具类
*/
@Test
public void testSpecificationUtils() {
UserQuery query = new UserQuery();
query.setUsername("张");
query.setStatus(UserStatus.ACTIVE);
query.setMinAge(20);
query.setMaxAge(30);
Specification<User> spec = UserSpecifications.buildQuery(query);
List<User> users = userRepository.findAll(spec);
}
/**
* 分页查询
*/
@Test
public void testSpecificationPage() {
UserQuery query = new UserQuery();
query.setStatus(UserStatus.ACTIVE);
Specification<User> spec = UserSpecifications.buildQuery(query);
Page<User> page = userRepository.findAll(spec,
PageRequest.of(0, 10, Sort.by("createTime").descending()));
System.out.println("总数: " + page.getTotalElements());
System.out.println("数据: " + page.getContent());
}
/**
* 复杂查询示例
*/
@Test
public void testComplexQuery() {
Specification<User> spec = (root, query, cb) -> {
// 多表关联
Join<User, Department> deptJoin = root.join("department", JoinType.LEFT);
// 多条件
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(root.get("status"), UserStatus.ACTIVE));
predicates.add(cb.isNotNull(root.get("email")));
predicates.add(cb.like(deptJoin.get("name"), "%技术%"));
// OR 条件
Predicate orPredicate = cb.or(
cb.like(root.get("username"), "%张%"),
cb.like(root.get("email"), "%test%")
);
predicates.add(orPredicate);
return cb.and(predicates.toArray(new Predicate[0]));
};
List<User> users = userRepository.findAll(spec);
}
}
五、实体监听器与审计
5.1 实体生命周期回调
/**
* JPA 实体生命周期回调
*/
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private String createBy;
private String updateBy;
// ========== 生命周期回调方法 ==========
/**
* @PrePersist - 持久化前
*/
@PrePersist
public void prePersist() {
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
this.createBy = getCurrentUser();
this.updateBy = getCurrentUser();
}
/**
* @PostPersist - 持久化后
*/
@PostPersist
public void postPersist() {
System.out.println("用户已创建: " + this.id);
}
/**
* @PreUpdate - 更新前
*/
@PreUpdate
public void preUpdate() {
this.updateTime = LocalDateTime.now();
this.updateBy = getCurrentUser();
}
/**
* @PostUpdate - 更新后
*/
@PostUpdate
public void postUpdate() {
System.out.println("用户已更新: " + this.id);
}
/**
* @PreRemove - 删除前
*/
@PreRemove
public void preRemove() {
System.out.println("即将删除用户: " + this.id);
}
/**
* @PostRemove - 删除后
*/
@PostRemove
public void postRemove() {
System.out.println("用户已删除: " + this.id);
}
/**
* @PostLoad - 加载后
*/
@PostLoad
public void postLoad() {
// 可以在此进行数据初始化
}
private String getCurrentUser() {
return SecurityContextHolder.getContext()
.getAuthentication().getName();
}
}
/**
* 外部实体监听器
*/
public class UserEntityListener {
@PrePersist
public void onPrePersist(User user) {
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
}
@PreUpdate
public void onPreUpdate(User user) {
user.setUpdateTime(LocalDateTime.now());
}
}
@Entity
@Table(name = "user")
@EntityListeners(UserEntityListener.class) // 注册监听器
@Data
public class UserWithListener {
// ...
}
/**
* 全局实体监听器配置
*/
// orm.xml
/*
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
version="2.2">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="com.example.listener.GlobalEntityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
*/
5.2 Spring Data JPA 审计
/**
* Spring Data JPA 审计功能
*/
// 1. 启用审计
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
// 获取当前用户
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
if (auth != null && auth.isAuthenticated()) {
return Optional.of(auth.getName());
}
return Optional.of("system");
};
}
}
// 2. 基础实体类
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@LastModifiedDate
@Column(name = "update_time")
private LocalDateTime updateTime;
@CreatedBy
@Column(name = "create_by", length = 50, updatable = false)
private String createBy;
@LastModifiedBy
@Column(name = "update_by", length = 50)
private String updateBy;
}
// 3. 实体类继承
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
}
@Entity
@Table(name = "product")
@Data
@EqualsAndHashCode(callSuper = true)
public class Product extends AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private BigDecimal price;
}
六、性能优化
6.1 延迟加载与 N+1 问题
/**
* 延迟加载与 N+1 问题
*/
@Entity
@Table(name = "user")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
// 延迟加载部门
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
// 延迟加载角色
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
/**
* 解决 N+1 问题
*/
public interface UserRepository extends JpaRepository<User, Long> {
// 方式1:JPQL JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.department WHERE u.id = :id")
User findByIdWithDepartment(@Param("id") Long id);
@Query("SELECT u FROM User u LEFT JOIN FETCH u.department")
List<User> findAllWithDepartment();
@Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles")
List<User> findAllWithRoles();
// 方式2:EntityGraph
@EntityGraph(attributePaths = {"department"})
@Query("SELECT u FROM User u")
List<User> findAllWithDeptGraph();
@EntityGraph(attributePaths = {"department", "roles"})
List<User> findAll();
// 方式3:NamedEntityGraph(在实体上定义)
// @NamedEntityGraphs({
// @NamedEntityGraph(name = "User.withDepartment",
// attributeNodes = @NamedAttributeNode("department"))
// })
@EntityGraph(value = "User.withDepartment", type = EntityGraphType.FETCH)
List<User> findAll();
}
/**
* 批量抓取配置
*/
@Entity
@Table(name = "department")
@Data
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 配置批量抓取
@BatchSize(size = 50)
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
private List<User> users = new ArrayList<>();
}
/**
* application.yml 配置
*/
/*
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
batch_fetch_style: PADDED
*/
6.2 批量操作优化
/**
* 批量插入优化
*/
@Configuration
public class JpaBatchConfig {
// 配置批量插入
// spring.jpa.properties.hibernate.jdbc.batch_size=50
// spring.jpa.properties.hibernate.order_inserts=true
// spring.jpa.properties.hibernate.order_updates=true
}
@Service
@Transactional
public class UserBatchService {
@PersistenceContext
private EntityManager entityManager;
@Autowired
private UserRepository userRepository;
/**
* 批量插入 - saveAll
*/
public void batchInsertWithSaveAll(List<User> users) {
userRepository.saveAll(users);
}
/**
* 批量插入 - 分批处理
*/
public void batchInsertWithBatch(List<User> users, int batchSize) {
List<User> batch = new ArrayList<>(batchSize);
for (int i = 0; i < users.size(); i++) {
batch.add(users.get(i));
if (batch.size() >= batchSize) {
userRepository.saveAll(batch);
entityManager.flush();
entityManager.clear(); // 清除持久化上下文
batch.clear();
}
}
if (!batch.isEmpty()) {
userRepository.saveAll(batch);
}
}
/**
* 批量更新
*/
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id IN :ids")
int batchUpdateStatus(@Param("ids") List<Long> ids,
@Param("status") UserStatus status);
}
/**
* 原生 SQL 批量操作
*/
public interface UserRepository extends JpaRepository<User, Long> {
/**
* 批量插入(原生 SQL)
*/
@Modifying
@Query(value = "INSERT INTO user(username, email, age) VALUES " +
"(:username1, :email1, :age1), " +
"(:username2, :email2, :age2)",
nativeQuery = true)
void batchInsertNative(@Param("username1") String username1,
@Param("email1") String email1,
@Param("age1") Integer age1,
@Param("username2") String username2,
@Param("email2") String email2,
@Param("age2") Integer age2);
}
6.3 缓存配置
/**
* JPA 二级缓存配置
*/
// 1. 引入依赖
/*
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
*/
// 2. 配置文件
/*
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
*/
// 3. 实体配置
@Entity
@Table(name = "user")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "userCache")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
// 缓存关联对象
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private Department department;
}
// 4. 查询缓存
public interface UserRepository extends JpaRepository<User, Long> {
@QueryHints(@QueryHint(name = org.hibernate.annotations.QueryHints.CACHEABLE,
value = "true"))
List<User> findByStatus(UserStatus status);
}
// 5. ehcache.xml 配置
/*
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="false"
/>
<cache name="userCache"
maxElementsInMemory="500"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
/>
</ehcache>
*/
七、总结与最佳实践
7.1 JPA vs MyBatis 对比
┌─────────────────────────────────────────────────────────────────────────┐
│ JPA vs MyBatis 对比 │
├──────────────────────┬──────────────────────┬───────────────────────────┤
│ 特性 │ JPA │ MyBatis │
├──────────────────────┼──────────────────────┼───────────────────────────┤
│ 学习曲线 │ 较陡峭 │ 较平缓 │
│ SQL 控制 │ 弱(自动生成) │ 强(完全控制) │
│ 跨数据库 │ 强(方言支持) │ 弱(需适配) │
│ 复杂查询 │ 较难(Specification)│ 简单(XML/注解) │
│ 性能优化 │ 较难 │ 简单 │
│ 开发效率 │ 高(少写代码) │ 中等 │
│ 动态 SQL │ 弱 │ 强 │
│ 缓存 │ 内置 │ 需配置 │
│ 适用场景 │ 标准 CRUD、简单查询 │ 复杂查询、性能敏感 │
└──────────────────────┴──────────────────────┴───────────────────────────┘
7.2 最佳实践
-
实体设计
- 使用
@MappedSuperclass提取公共字段 - 谨慎使用关联映射,避免过度关联
- 优先使用延迟加载
- 使用
-
Repository 设计
- 方法命名遵循规范
- 复杂查询使用
@Query或 Specification - 分离读操作和写操作
-
性能优化
- 解决 N+1 问题(JOIN FETCH、EntityGraph)
- 合理使用批量操作
- 考虑缓存策略
-
事务管理
- 合理设置事务边界
- 避免长事务
- 只读操作使用
@Transactional(readOnly = true)
参考资料:
- Spring Data JPA 官方文档:spring.io/projects/sp…
- Hibernate 文档:hibernate.org/orm/documen…
- 《Java Persistence with Hibernate》
六、思考与练习
思考题
-
基础题:JPA 的 Repository 接口继承体系是怎样的?JpaRepository 相比 CrudRepository 提供了哪些额外功能?
-
进阶题:什么是 N+1 问题?在 JPA 中如何通过 JOIN FETCH 和 EntityGraph 解决这个问题?
-
实战题:在一个内容管理系统中,文章(Article)和标签(Tag)是多对多关系,如何设计实体映射?如何避免循环引用导致的 JSON 序列化问题?
编程练习
练习:实现一个在线商城的商品管理模块,要求:
- 设计商品(Product)、分类(Category)、品牌(Brand)三个实体及其关联关系
- 使用方法命名查询实现:按分类查询、按品牌查询、价格区间查询
- 使用 @Query 注解实现:查询热门商品(销量前 10)、查询新品(创建时间最近 7 天)
- 使用 Specification 实现多条件动态查询(分类、品牌、价格区间、关键词)
- 实现分页查询,并使用 @EntityGraph 解决关联查询的 N+1 问题
章节关联
- 前置章节:MyBatis-Plus 详解
- 后续章节:多数据源配置详解
- 扩展阅读:
- Hibernate 性能优化实战
- JPA 与 MyBatis 混合使用指南
- 领域驱动设计(DDD)中的 Repository 模式
📝 下一章预告
在实际企业项目中,往往需要同时访问多个数据库,如读写分离、分库分表、异构数据库等。下一章将深入讲解多数据源的配置方案,包括静态配置、动态切换、读写分离以及分布式事务处理。
本章完