Spring Data JPA 完全指南
# 一、前言
在Java开发中,数据库操作是绕不开的话题。你是否曾为写大量的JDBC模板代码而烦恼?是否在处理复杂的对象关系映射时感到困惑?JPA(Java Persistence API)就是为了解决这些问题而诞生的。
JPA是Java官方提供的ORM(对象关系映射)规范,它让你可以用面向对象的方式来操作数据库,而不用写繁琐的SQL语句。Spring Data JPA则是Spring对JPA的再次封装,让数据访问变得更加简单。
本文将带你从零开始,全面掌握JPA和Spring Data JPA的使用。无论你是初学者还是有一定经验的开发者,相信都能从中获益。
# 二、JPA基础概念
# 1、什么是JPA
JPA(Java Persistence API)是Java EE的一个标准规范,用于对象关系映射(ORM)。它定义了一套标准的API,让Java对象可以方便地映射到数据库表。
简单来说,JPA让你可以:
- 用Java类表示数据库表:不用手动创建表结构
- 用对象操作代替SQL:增删改查都是对象操作
- 自动处理关系映射:一对一、一对多、多对多关系自动维护
- 跨数据库移植:换数据库不用改代码
# 2、JPA vs Hibernate vs MyBatis
很多人会混淆这三者的关系,我们来理清一下:
- JPA:是一个规范、标准,类似于JDBC
- Hibernate:是JPA规范的一个实现,也是最流行的实现
- MyBatis:不是JPA实现,是另一种持久层框架,更偏向SQL
三者对比:
| 特性 | JPA/Hibernate | MyBatis |
|---|---|---|
| 编程方式 | 面向对象 | 面向SQL |
| 学习曲线 | 较陡 | 较平缓 |
| SQL控制 | 自动生成 | 完全控制 |
| 复杂查询 | JPQL/Criteria API | XML/注解映射 |
| 适用场景 | 标准CRUD、领域模型复杂 | 复杂SQL、动态查询 |
# 3、Spring Data JPA是什么
Spring Data JPA是Spring对JPA的进一步封装,它提供了:
- Repository接口:只需定义接口,Spring自动实现
- 方法名查询:根据方法名自动生成查询
- 自定义查询:支持
@Query注解 - 分页排序:内置分页和排序支持
- 审计功能:自动记录创建时间、修改时间等
可以说,Spring Data JPA = JPA + Spring的便利性,让数据访问层的开发效率大大提升。
# 三、快速上手
# 1、环境搭建
首先在pom.xml中添加依赖:
<dependencies>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2数据库(用于测试) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后在application.yml中配置数据源:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
# 是否显示SQL语句
show-sql: true
# Hibernate配置
hibernate:
# ddl-auto: create-drop # 测试时使用,会删除并重建表
# ddl-auto: update # 开发时使用,会更新表结构
ddl-auto: validate # 生产环境使用,只验证不修改
# 数据库方言
database-platform: org.hibernate.dialect.MySQLDialect
# 格式化SQL输出
properties:
hibernate:
format_sql: true
ddl-auto配置说明:
create:每次启动都删除并创建表(危险)create-drop:启动时创建,关闭时删除(测试用)update:自动更新表结构,不删除数据(开发用)validate:只验证表结构,不做修改(生产用)none:不做任何操作
# 2、第一个实体类
创建一个用户实体:
package com.example.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
/**
* 用户实体类
*/
@Entity
@Table(name = "t_user")
public class User {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 用户名(唯一)
*/
@Column(nullable = false, unique = true, length = 50)
private String username;
/**
* 邮箱
*/
@Column(nullable = false, length = 100)
private String email;
/**
* 年龄
*/
@Column
private Integer age;
/**
* 创建时间
*/
@Column(name = "create_time", nullable = false, updatable = false)
private LocalDateTime createTime;
/**
* 更新时间
*/
@Column(name = "update_time")
private LocalDateTime updateTime;
// 在插入前自动设置创建时间
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
updateTime = LocalDateTime.now();
}
// 在更新前自动设置更新时间
@PreUpdate
protected void onUpdate() {
updateTime = LocalDateTime.now();
}
// Getter和Setter方法
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
}
注解说明:
@Entity:标记这是一个JPA实体类@Table:指定对应的数据库表名@Id:标记主键字段@GeneratedValue:主键生成策略@Column:指定列的属性(名称、长度、是否可空等)@PrePersist:插入前执行的方法@PreUpdate:更新前执行的方法
# 3、创建Repository
只需定义一个接口继承JpaRepository:
package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 用户数据访问接口
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA会根据方法名自动生成查询
/**
* 根据用户名查询用户
*/
Optional<User> findByUsername(String username);
/**
* 根据邮箱查询用户
*/
Optional<User> findByEmail(String email);
/**
* 查询年龄大于指定值的用户
*/
List<User> findByAgeGreaterThan(Integer age);
/**
* 查询用户名包含指定字符串的用户
*/
List<User> findByUsernameContaining(String keyword);
}
没错,就这么简单!你不需要写任何实现代码,Spring Data JPA会自动帮你实现这些方法。
# 4、使用示例
在Service中使用Repository:
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* 用户服务类
*/
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 创建用户
*/
@Transactional
public User createUser(String username, String email, Integer age) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setAge(age);
return userRepository.save(user);
}
/**
* 根据ID查询用户
*/
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
/**
* 根据用户名查询用户
*/
public Optional<User> getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
/**
* 查询所有用户
*/
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 更新用户
*/
@Transactional
public User updateUser(Long id, String email, Integer age) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在"));
user.setEmail(email);
user.setAge(age);
return userRepository.save(user);
}
/**
* 删除用户
*/
@Transactional
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
/**
* 查询年龄大于指定值的用户
*/
public List<User> getUsersOlderThan(Integer age) {
return userRepository.findByAgeGreaterThan(age);
}
}
是不是很简单?不需要写一行SQL,就完成了基本的增删改查功能。
# 四、实体映射详解
# 1、基本注解
# 1.1、@Entity和@Table
@Entity // 标记为JPA实体
@Table(
name = "t_user", // 表名
schema = "mydb", // 数据库schema
catalog = "catalog_name", // catalog名称
uniqueConstraints = { // 唯一约束
@UniqueConstraint(
name = "uk_username_email",
columnNames = {"username", "email"}
)
},
indexes = { // 索引
@Index(name = "idx_age", columnList = "age")
}
)
public class User {
// ...
}
# 1.2、@Id和主键生成策略
public class User {
// 策略1:自增(最常用,适合MySQL)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 策略2:序列(适合Oracle、PostgreSQL)
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_sequence", allocationSize = 1)
private Long id;
// 策略3:UUID
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
// 策略4:自定义生成器
@Id
@GeneratedValue(generator = "custom-generator")
@GenericGenerator(name = "custom-generator", strategy = "com.example.CustomIdGenerator")
private String id;
}
主键生成策略说明:
IDENTITY:数据库自增,适合MySQL、SQL ServerSEQUENCE:使用数据库序列,适合Oracle、PostgreSQLTABLE:使用表来模拟序列,性能较差,不推荐AUTO:自动选择策略,不推荐(不确定性高)UUID:使用UUID,适合分布式系统
# 1.3、@Column详解
public class User {
@Column(
name = "user_name", // 列名
nullable = false, // 是否可为空
unique = true, // 是否唯一
length = 50, // 字符串长度
precision = 10, // 数字总位数
scale = 2, // 小数位数
insertable = true, // 是否可插入
updatable = true, // 是否可更新
columnDefinition = "VARCHAR(50) COMMENT '用户名'" // 自定义列定义
)
private String username;
// 对于枚举类型
@Enumerated(EnumType.STRING) // 存储枚举名称(推荐)
private UserStatus status;
// 对于日期类型
@Temporal(TemporalType.TIMESTAMP) // JPA 2.x中使用
private Date createTime;
// JPA 3.x推荐使用Java 8时间API
private LocalDateTime updateTime; // 不需要@Temporal
// 对于大字段
@Lob // Large Object
@Column(columnDefinition = "TEXT")
private String content;
// 不需要持久化的字段
@Transient
private String tempData;
}
# 2、关系映射
# 2.1、一对一(@OneToOne)
示例:用户与用户详情
/**
* 用户实体
*/
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
// 一对一关系(主控方)
@OneToOne(
cascade = CascadeType.ALL, // 级联操作
fetch = FetchType.LAZY, // 懒加载
orphanRemoval = true // 删除孤儿记录
)
@JoinColumn(name = "profile_id", unique = true) // 外键列
private UserProfile profile;
// getter/setter...
}
/**
* 用户详情实体
*/
@Entity
@Table(name = "t_user_profile")
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String address;
private String phone;
// 一对一关系(被控方)
@OneToOne(mappedBy = "profile") // mappedBy指向主控方的属性名
private User user;
// getter/setter...
}
使用示例:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void createUserWithProfile() {
// 创建用户详情
UserProfile profile = new UserProfile();
profile.setAddress("北京市朝阳区");
profile.setPhone("13800138000");
// 创建用户
User user = new User();
user.setUsername("zhangsan");
user.setProfile(profile); // 关联详情
// 保存用户(级联保存详情)
userRepository.save(user);
}
}
# 2.2、一对多(@OneToMany)和多对一(@ManyToOne)
示例:部门与员工
/**
* 部门实体(一方)
*/
@Entity
@Table(name = "t_department")
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 一对多关系
@OneToMany(
mappedBy = "department", // 指向Employee中的department属性
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
private List<Employee> employees = new ArrayList<>();
// 便捷方法:添加员工
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this); // 维护双向关系
}
// 便捷方法:移除员工
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
// getter/setter...
}
/**
* 员工实体(多方)
*/
@Entity
@Table(name = "t_employee")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 多对一关系
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id") // 外键列
private Department department;
// getter/setter...
}
使用示例:
@Service
public class DepartmentService {
@Autowired
private DepartmentRepository departmentRepository;
@Transactional
public void createDepartmentWithEmployees() {
// 创建部门
Department dept = new Department();
dept.setName("研发部");
// 创建员工
Employee emp1 = new Employee();
emp1.setName("张三");
Employee emp2 = new Employee();
emp2.setName("李四");
// 添加员工到部门
dept.addEmployee(emp1);
dept.addEmployee(emp2);
// 保存部门(级联保存员工)
departmentRepository.save(dept);
}
}
# 2.3、多对多(@ManyToMany)
示例:学生与课程
/**
* 学生实体
*/
@Entity
@Table(name = "t_student")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 多对多关系(主控方)
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinTable(
name = "t_student_course", // 中间表名
joinColumns = @JoinColumn(name = "student_id"), // 当前实体的外键
inverseJoinColumns = @JoinColumn(name = "course_id") // 关联实体的外键
)
private Set<Course> courses = new HashSet<>();
// 便捷方法:选课
public void enrollCourse(Course course) {
courses.add(course);
course.getStudents().add(this);
}
// 便捷方法:退课
public void dropCourse(Course course) {
courses.remove(course);
course.getStudents().remove(this);
}
// getter/setter...
}
/**
* 课程实体
*/
@Entity
@Table(name = "t_course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 多对多关系(被控方)
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
// getter/setter...
}
使用示例:
@Service
public class StudentService {
@Autowired
private StudentRepository studentRepository;
@Autowired
private CourseRepository courseRepository;
@Transactional
public void enrollStudentToCourses() {
// 创建学生
Student student = new Student();
student.setName("王五");
// 查询已存在的课程
Course javaCourse = courseRepository.findByName("Java编程").orElseThrow();
Course springCourse = courseRepository.findByName("Spring框架").orElseThrow();
// 学生选课
student.enrollCourse(javaCourse);
student.enrollCourse(springCourse);
// 保存学生
studentRepository.save(student);
}
}
# 3、级联操作(Cascade)
级联操作决定了对主实体的操作是否会传播到关联实体:
public enum CascadeType {
PERSIST, // 保存主实体时,也保存关联实体
MERGE, // 更新主实体时,也更新关联实体
REMOVE, // 删除主实体时,也删除关联实体
REFRESH, // 刷新主实体时,也刷新关联实体
DETACH, // 分离主实体时,也分离关联实体
ALL // 以上全部
}
使用建议:
- 一对一:通常使用
CascadeType.ALL - 一对多:父实体使用
CascadeType.ALL,子实体谨慎使用REMOVE - 多对多:只使用
PERSIST和MERGE,避免误删
# 4、加载策略(Fetch)
public enum FetchType {
LAZY, // 懒加载:访问时才加载(推荐)
EAGER // 急加载:立即加载
}
默认加载策略:
@OneToOne:EAGER@ManyToOne:EAGER@OneToMany:LAZY@ManyToMany:LAZY
最佳实践:除非必要,都使用LAZY,避免N+1查询问题。
# 五、Repository详解
# 1、Repository继承体系
Spring Data JPA提供了一系列Repository接口:
Repository (标记接口)
├─ CrudRepository (基本增删改查)
│ ├─ PagingAndSortingRepository (分页和排序)
│ │ └─ JpaRepository (JPA扩展,最常用)
│ └─ ListCrudRepository (返回List而非Iterable)
└─ QueryByExampleExecutor (按示例查询)
# 1.1、CrudRepository
提供基本的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); // 根据ID查询
boolean existsById(ID id); // 是否存在
Iterable<T> findAll(); // 查询所有
Iterable<T> findAllById(Iterable<ID> ids); // 批量查询
long count(); // 统计数量
void deleteById(ID id); // 根据ID删除
void delete(T entity); // 删除实体
void deleteAll(Iterable<? extends T> entities); // 批量删除
void deleteAll(); // 删除所有
}
# 1.2、PagingAndSortingRepository
增加了分页和排序功能:
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
Iterable<T> findAll(Sort sort); // 排序查询
Page<T> findAll(Pageable pageable); // 分页查询
}
# 1.3、JpaRepository(推荐使用)
这是我们最常用的接口,它整合了上面所有功能,并增加了JPA特有的方法:
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>,
ListPagingAndSortingRepository<T, ID>,
QueryByExampleExecutor<T> {
void flush(); // 刷新持久化上下文
<S extends T> S saveAndFlush(S entity); // 保存并刷新
<S extends T> List<S> saveAllAndFlush(Iterable<S> entities); // 批量保存并刷新
void deleteAllInBatch(Iterable<T> entities); // 批量删除(一条SQL)
void deleteAllByIdInBatch(Iterable<ID> ids); // 批量删除(一条SQL)
void deleteAllInBatch(); // 删除所有(一条SQL)
T getOne(ID id); // 获取引用(懒加载)
T getReferenceById(ID id); // 获取引用(推荐)
<S extends T> List<S> findAll(Example<S> example); // 按示例查询
<S extends T> List<S> findAll(Example<S> example, Sort sort); // 按示例排序查询
}
# 2、方法命名查询
Spring Data JPA最强大的功能之一就是根据方法名自动生成查询。
# 2.1、基本规则
方法名由以下部分组成:
- 前缀:
findBy、getBy、queryBy、readBy、countBy、deleteBy等 - 属性名:实体类的属性名(首字母大写)
- 条件关键字:
And、Or、Between等 - 返回类型:
Optional<T>、List<T>、Stream<T>等
# 2.2、常用关键字
public interface UserRepository extends JpaRepository<User, Long> {
// 等于(Equals,可省略)
User findByUsername(String username);
// 不等于(Not)
List<User> findByUsernameNot(String username);
// Like(模糊查询)
List<User> findByUsernameLike(String pattern); // 需要传 "%pattern%"
List<User> findByUsernameContaining(String keyword); // 自动加 %%
List<User> findByUsernameStartingWith(String prefix); // 前缀匹配
List<User> findByUsernameEndingWith(String suffix); // 后缀匹配
// 大小比较
List<User> findByAgeGreaterThan(Integer age); // >
List<User> findByAgeGreaterThanEqual(Integer age); // >=
List<User> findByAgeLessThan(Integer age); // <
List<User> findByAgeLessThanEqual(Integer age); // <=
List<User> findByAgeBetween(Integer start, Integer end); // BETWEEN
// In和NotIn
List<User> findByAgeIn(Collection<Integer> ages);
List<User> findByAgeNotIn(Collection<Integer> ages);
// IsNull和IsNotNull
List<User> findByEmailIsNull();
List<User> findByEmailIsNotNull();
// True和False(针对Boolean类型)
List<User> findByActiveTrue();
List<User> findByActiveFalse();
// IgnoreCase(忽略大小写)
User findByUsernameIgnoreCase(String username);
// OrderBy(排序)
List<User> findByAgeOrderByUsernameAsc(Integer age);
List<User> findByAgeOrderByUsernameDesc(Integer age);
// And和Or(组合条件)
User findByUsernameAndEmail(String username, String email);
List<User> findByUsernameOrEmail(String username, String email);
// Distinct(去重)
List<User> findDistinctByAge(Integer age);
// Top和First(限制结果数量)
User findFirstByOrderByCreateTimeDesc(); // 最新的一条
List<User> findTop10ByOrderByAgeDesc(); // 年龄最大的10个
// Exists(是否存在)
boolean existsByUsername(String username);
// Count(统计数量)
long countByAge(Integer age);
// Delete(删除)
long deleteByAge(Integer age); // 返回删除的数量
List<User> removeByAge(Integer age); // 返回被删除的实体
}
# 2.3、复杂示例
public interface UserRepository extends JpaRepository<User, Long> {
// 查询年龄在18-30之间,用户名包含"zhang",按创建时间倒序
List<User> findByAgeBetweenAndUsernameContainingOrderByCreateTimeDesc(
Integer startAge, Integer endAge, String keyword
);
// 查询邮箱不为空且年龄大于18的用户,返回前20条
List<User> findTop20ByEmailIsNotNullAndAgeGreaterThanOrderByCreateTimeDesc(Integer age);
// 统计用户名包含指定关键字的用户数量
long countByUsernameContaining(String keyword);
}
# 3、@Query自定义查询
当方法名查询无法满足需求时,可以使用@Query注解自定义查询。
# 3.1、JPQL查询
JPQL(Java Persistence Query Language)是面向对象的查询语言:
public interface UserRepository extends JpaRepository<User, Long> {
// 基本JPQL查询
@Query("SELECT u FROM User u WHERE u.age > ?1")
List<User> findUsersOlderThan(Integer age);
// 命名参数(推荐)
@Query("SELECT u FROM User u WHERE u.username = :username AND u.email = :email")
Optional<User> findByUsernameAndEmail(
@Param("username") String username,
@Param("email") String email
);
// 投影查询(只查询部分字段)
@Query("SELECT u.username, u.email FROM User u WHERE u.age > :age")
List<Object[]> findUserInfoByAge(@Param("age") Integer age);
// 使用DTO接收结果
@Query("SELECT new com.example.dto.UserDTO(u.username, u.email) FROM User u WHERE u.age > :age")
List<UserDTO> findUserDTOByAge(@Param("age") Integer age);
// 关联查询
@Query("SELECT u FROM User u LEFT JOIN FETCH u.profile WHERE u.id = :id")
Optional<User> findUserWithProfile(@Param("id") Long id);
// 分组聚合
@Query("SELECT u.age, COUNT(u) FROM User u GROUP BY u.age")
List<Object[]> countByAge();
// 统计查询
@Query("SELECT COUNT(u) FROM User u WHERE u.age BETWEEN :start AND :end")
long countByAgeBetween(@Param("start") Integer start, @Param("end") Integer end);
// 更新操作
@Modifying
@Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
int updateEmailById(@Param("id") Long id, @Param("email") String email);
// 删除操作
@Modifying
@Query("DELETE FROM User u WHERE u.age < :age")
int deleteByAgeLessThan(@Param("age") Integer age);
}
注意:
- 更新和删除操作必须加
@Modifying注解 - 更新和删除操作需要在Service层加
@Transactional注解 - JPQL中的类名是实体类名,不是表名
# 3.2、原生SQL查询
有时候JPQL无法满足需求,可以使用原生SQL:
public interface UserRepository extends JpaRepository<User, Long> {
// 原生SQL查询
@Query(value = "SELECT * FROM t_user WHERE age > ?1", nativeQuery = true)
List<User> findUsersOlderThanNative(Integer age);
// 使用命名参数
@Query(value = "SELECT * FROM t_user WHERE username = :username", nativeQuery = true)
Optional<User> findByUsernameNative(@Param("username") String username);
// 复杂统计查询
@Query(value = """
SELECT age, COUNT(*) as count
FROM t_user
GROUP BY age
HAVING COUNT(*) > 1
ORDER BY age
""", nativeQuery = true)
List<Object[]> findAgeStatistics();
// 原生SQL更新
@Modifying
@Query(value = "UPDATE t_user SET email = :email WHERE id = :id", nativeQuery = true)
int updateEmailByIdNative(@Param("id") Long id, @Param("email") String email);
}
# 4、分页和排序
# 4.1、排序(Sort)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> getUsersSorted() {
// 单字段排序
Sort sort = Sort.by("age").descending();
// 多字段排序
Sort multiSort = Sort.by("age").descending()
.and(Sort.by("username").ascending());
// 使用Order构建
Sort orderSort = Sort.by(
Sort.Order.desc("age"),
Sort.Order.asc("username")
);
return userRepository.findAll(sort);
}
}
# 4.2、分页(Pageable)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> getUsersPaged(int pageNum, int pageSize) {
// 创建分页请求(页码从0开始)
Pageable pageable = PageRequest.of(pageNum, pageSize);
// 分页+排序
Pageable pageableWithSort = PageRequest.of(
pageNum,
pageSize,
Sort.by("createTime").descending()
);
// 执行分页查询
Page<User> page = userRepository.findAll(pageable);
// 获取分页信息
long totalElements = page.getTotalElements(); // 总记录数
int totalPages = page.getTotalPages(); // 总页数
int currentPage = page.getNumber(); // 当前页码
int pageSize = page.getSize(); // 每页大小
List<User> content = page.getContent(); // 当前页数据
boolean hasNext = page.hasNext(); // 是否有下一页
boolean hasPrevious = page.hasPrevious(); // 是否有上一页
return page;
}
// 自定义查询的分页
public Page<User> searchUsers(String keyword, int pageNum, int pageSize) {
Pageable pageable = PageRequest.of(pageNum, pageSize);
return userRepository.findByUsernameContaining(keyword, pageable);
}
}
Repository中的分页方法:
public interface UserRepository extends JpaRepository<User, Long> {
// 方法名查询 + 分页
Page<User> findByUsernameContaining(String keyword, Pageable pageable);
// @Query查询 + 分页
@Query("SELECT u FROM User u WHERE u.age > :age")
Page<User> findUsersOlderThan(@Param("age") Integer age, Pageable pageable);
// 如果只需要列表不需要总数统计,可以使用Slice
Slice<User> findByAge(Integer age, Pageable pageable);
}
Page和Slice的区别:
Page:会执行count查询获取总数,适合需要显示总页数的场景Slice:不执行count查询,只知道是否有下一页,性能更好
# 六、Specifications动态查询
当查询条件是动态的(用户可能选择不同的搜索条件),方法名查询和@Query都不够灵活。这时候就需要使用Specifications。
# 1、启用Specifications
让Repository继承JpaSpecificationExecutor接口:
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
# 2、基本使用
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(String username, Integer minAge, Integer maxAge) {
// 创建Specification
Specification<User> spec = (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
// 用户名模糊查询
if (username != null && !username.isEmpty()) {
predicates.add(
criteriaBuilder.like(root.get("username"), "%" + username + "%")
);
}
// 年龄范围查询
if (minAge != null) {
predicates.add(
criteriaBuilder.greaterThanOrEqualTo(root.get("age"), minAge)
);
}
if (maxAge != null) {
predicates.add(
criteriaBuilder.lessThanOrEqualTo(root.get("age"), maxAge)
);
}
// 组合所有条件(AND)
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
return userRepository.findAll(spec);
}
}
# 3、封装Specification工具类
为了代码复用,可以封装一个工具类:
/**
* Specification工具类
*/
public class SpecificationBuilder<T> {
private List<Specification<T>> specifications = new ArrayList<>();
/**
* 添加等于条件
*/
public SpecificationBuilder<T> equal(String field, Object value) {
if (value != null) {
specifications.add((root, query, cb) ->
cb.equal(root.get(field), value)
);
}
return this;
}
/**
* 添加模糊查询条件
*/
public SpecificationBuilder<T> like(String field, String value) {
if (value != null && !value.isEmpty()) {
specifications.add((root, query, cb) ->
cb.like(root.get(field), "%" + value + "%")
);
}
return this;
}
/**
* 添加大于等于条件
*/
public SpecificationBuilder<T> greaterThanOrEqual(String field, Comparable value) {
if (value != null) {
specifications.add((root, query, cb) ->
cb.greaterThanOrEqualTo(root.get(field), value)
);
}
return this;
}
/**
* 添加小于等于条件
*/
public SpecificationBuilder<T> lessThanOrEqual(String field, Comparable value) {
if (value != null) {
specifications.add((root, query, cb) ->
cb.lessThanOrEqualTo(root.get(field), value)
);
}
return this;
}
/**
* 添加In条件
*/
public SpecificationBuilder<T> in(String field, Collection<?> values) {
if (values != null && !values.isEmpty()) {
specifications.add((root, query, cb) ->
root.get(field).in(values)
);
}
return this;
}
/**
* 添加Between条件
*/
public SpecificationBuilder<T> between(String field, Comparable start, Comparable end) {
if (start != null && end != null) {
specifications.add((root, query, cb) ->
cb.between(root.get(field), start, end)
);
}
return this;
}
/**
* 添加IsNull条件
*/
public SpecificationBuilder<T> isNull(String field) {
specifications.add((root, query, cb) ->
cb.isNull(root.get(field))
);
return this;
}
/**
* 添加IsNotNull条件
*/
public SpecificationBuilder<T> isNotNull(String field) {
specifications.add((root, query, cb) ->
cb.isNotNull(root.get(field))
);
return this;
}
/**
* 构建最终的Specification(AND条件)
*/
public Specification<T> build() {
return (root, query, cb) -> {
if (specifications.isEmpty()) {
return cb.conjunction(); // 返回恒真条件
}
Predicate[] predicates = specifications.stream()
.map(spec -> spec.toPredicate(root, query, cb))
.toArray(Predicate[]::new);
return cb.and(predicates);
};
}
/**
* 构建最终的Specification(OR条件)
*/
public Specification<T> buildOr() {
return (root, query, cb) -> {
if (specifications.isEmpty()) {
return cb.conjunction();
}
Predicate[] predicates = specifications.stream()
.map(spec -> spec.toPredicate(root, query, cb))
.toArray(Predicate[]::new);
return cb.or(predicates);
};
}
}
使用示例:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public List<User> searchUsers(String username, Integer minAge, Integer maxAge) {
Specification<User> spec = new SpecificationBuilder<User>()
.like("username", username)
.greaterThanOrEqual("age", minAge)
.lessThanOrEqual("age", maxAge)
.build();
return userRepository.findAll(spec);
}
// 分页+排序+动态查询
public Page<User> searchUsersPaged(String username, Integer minAge,
int pageNum, int pageSize) {
Specification<User> spec = new SpecificationBuilder<User>()
.like("username", username)
.greaterThanOrEqual("age", minAge)
.build();
Pageable pageable = PageRequest.of(pageNum, pageSize,
Sort.by("createTime").descending());
return userRepository.findAll(spec, pageable);
}
}
# 七、事务管理
# 1、声明式事务
Spring Data JPA的事务管理非常简单,只需在Service方法上加@Transactional注解:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 创建用户(事务)
*/
@Transactional
public User createUser(User user) {
return userRepository.save(user);
}
/**
* 批量创建用户(事务)
*/
@Transactional
public List<User> createUsers(List<User> users) {
return userRepository.saveAll(users);
}
/**
* 转账示例(事务)
*/
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
User from = userRepository.findById(fromId)
.orElseThrow(() -> new RuntimeException("转出用户不存在"));
User to = userRepository.findById(toId)
.orElseThrow(() -> new RuntimeException("转入用户不存在"));
// 扣款
from.setBalance(from.getBalance().subtract(amount));
userRepository.save(from);
// 如果这里抛异常,上面的扣款会回滚
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("转账金额必须大于0");
}
// 加款
to.setBalance(to.getBalance().add(amount));
userRepository.save(to);
}
}
# 2、事务属性
@Transactional注解有很多属性可以配置:
@Transactional(
// 事务传播行为
propagation = Propagation.REQUIRED,
// 事务隔离级别
isolation = Isolation.DEFAULT,
// 超时时间(秒)
timeout = 30,
// 是否只读事务(只读事务性能更好)
readOnly = false,
// 遇到哪些异常回滚(默认RuntimeException和Error)
rollbackFor = {Exception.class},
// 遇到哪些异常不回滚
noRollbackFor = {IllegalArgumentException.class}
)
public void someMethod() {
// ...
}
# 3、事务传播行为
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private LogService logService;
/**
* REQUIRED(默认):如果当前有事务,加入;没有就新建
*/
@Transactional(propagation = Propagation.REQUIRED)
public void createOrder() {
// 保存订单
orderRepository.save(new Order());
// 记录日志(加入当前事务)
logService.logRequired("订单创建");
}
/**
* REQUIRES_NEW:总是新建事务,如果当前有事务,挂起
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createOrderWithNewTransaction() {
orderRepository.save(new Order());
// 日志在新事务中(即使订单创建失败,日志也会保存)
logService.logRequiresNew("订单创建");
}
/**
* NESTED:嵌套事务,可以部分回滚
*/
@Transactional(propagation = Propagation.NESTED)
public void createOrderNested() {
orderRepository.save(new Order());
try {
// 尝试发送通知(失败不影响订单)
logService.logNested("发送通知");
} catch (Exception e) {
// 嵌套事务回滚,但不影响外层事务
}
}
}
@Service
public class LogService {
@Autowired
private LogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRED)
public void logRequired(String message) {
logRepository.save(new Log(message));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logRequiresNew(String message) {
logRepository.save(new Log(message));
}
@Transactional(propagation = Propagation.NESTED)
public void logNested(String message) {
logRepository.save(new Log(message));
}
}
# 4、只读事务
对于查询操作,使用只读事务可以提升性能:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 只读事务
*/
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 分页查询也应该用只读事务
*/
@Transactional(readOnly = true)
public Page<User> getUsersPaged(int pageNum, int pageSize) {
Pageable pageable = PageRequest.of(pageNum, pageSize);
return userRepository.findAll(pageable);
}
}
# 八、审计功能
审计功能可以自动记录创建时间、修改时间、创建人、修改人等信息。
# 1、启用审计
在配置类上加@EnableJpaAuditing注解:
@SpringBootApplication
@EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
# 2、使用审计注解
@Entity
@Table(name = "t_user")
@EntityListeners(AuditingEntityListener.class) // 启用审计监听器
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
/**
* 创建时间(自动设置)
*/
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createTime;
/**
* 修改时间(自动更新)
*/
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updateTime;
/**
* 创建人(自动设置)
*/
@CreatedBy
@Column(nullable = false, updatable = false, length = 50)
private String createdBy;
/**
* 修改人(自动更新)
*/
@LastModifiedBy
@Column(nullable = false, length = 50)
private String lastModifiedBy;
// getter/setter...
}
# 3、提供审计人信息
创建一个AuditorAware实现类,告诉JPA当前用户是谁:
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 从Spring Security获取当前用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("system");
}
return Optional.of(authentication.getName());
}
}
如果项目中没有使用Spring Security,可以简化:
@Component
public class SimpleAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 这里可以从ThreadLocal、Session等地方获取当前用户
// 示例:返回固定值
return Optional.of("admin");
}
}
# 4、抽取基类
为了避免每个实体都重复定义审计字段,可以创建一个基类:
/**
* 审计基类
*/
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createTime;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updateTime;
@CreatedBy
@Column(nullable = false, updatable = false, length = 50)
private String createdBy;
@LastModifiedBy
@Column(nullable = false, length = 50)
private String lastModifiedBy;
// getter/setter...
}
实体类只需继承基类:
@Entity
@Table(name = "t_user")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// getter/setter...
}
# 九、性能优化
# 1、N+1查询问题
这是使用ORM框架最常遇到的性能问题。
问题示例:
// 查询所有部门
List<Department> departments = departmentRepository.findAll();
// 遍历部门,访问员工(会触发N次查询)
for (Department dept : departments) {
System.out.println(dept.getName());
for (Employee emp : dept.getEmployees()) { // 每次都会查询数据库!
System.out.println(emp.getName());
}
}
这会产生1(查部门)+ N(查N个部门的员工)次查询。
解决方案1:使用JOIN FETCH
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
// 使用JOIN FETCH一次性加载部门和员工
@Query("SELECT d FROM Department d LEFT JOIN FETCH d.employees")
List<Department> findAllWithEmployees();
}
解决方案2:使用@EntityGraph
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
@EntityGraph(attributePaths = {"employees"})
List<Department> findAll();
}
解决方案3:使用@NamedEntityGraph
在实体类上定义:
@Entity
@Table(name = "t_department")
@NamedEntityGraph(
name = "Department.withEmployees",
attributeNodes = @NamedAttributeNode("employees")
)
public class Department {
// ...
}
在Repository中使用:
@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {
@EntityGraph(value = "Department.withEmployees")
List<Department> findAll();
}
# 2、批量操作优化
批量插入:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 批量插入(每批1000条)
*/
@Transactional
public void batchInsert(List<User> users) {
int batchSize = 1000;
for (int i = 0; i < users.size(); i++) {
userRepository.save(users.get(i));
if (i % batchSize == 0 && i > 0) {
// 每1000条刷新一次
userRepository.flush();
}
}
}
/**
* 使用saveAll(更推荐)
*/
@Transactional
public void batchInsertSaveAll(List<User> users) {
userRepository.saveAll(users);
userRepository.flush();
}
}
配置Hibernate批量插入:
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 1000 # 批量大小
order_inserts: true # 对插入语句排序
order_updates: true # 对更新语句排序
批量删除:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
/**
* 批量删除(一条SQL)
*/
@Transactional
public void batchDelete(List<Long> ids) {
userRepository.deleteAllByIdInBatch(ids);
}
}
# 3、投影查询
当只需要部分字段时,使用投影查询可以减少数据传输量:
接口投影:
/**
* 投影接口
*/
public interface UserProjection {
String getUsername();
String getEmail();
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 返回投影
List<UserProjection> findAllProjectedBy();
UserProjection findProjectedById(Long id);
}
类投影:
/**
* DTO类
*/
public class UserDTO {
private String username;
private String email;
public UserDTO(String username, String email) {
this.username = username;
this.email = email;
}
// getter/setter...
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserDTO(u.username, u.email) FROM User u")
List<UserDTO> findAllDTO();
}
# 4、缓存
一级缓存(Session缓存):
JPA默认开启,同一个事务内多次查询同一个实体,只会查询一次数据库。
二级缓存(SessionFactory缓存):
需要手动配置,可以跨事务共享。
添加依赖:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
</dependency>
配置:
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
在实体上启用缓存:
@Entity
@Table(name = "t_user")
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
// ...
}
# 5、懒加载优化
问题:在Controller层访问懒加载的关联对象,会抛LazyInitializationException异常。
解决方案1:在Service层处理
@Service
public class DepartmentService {
@Autowired
private DepartmentRepository departmentRepository;
@Transactional(readOnly = true)
public Department getDepartmentWithEmployees(Long id) {
Department dept = departmentRepository.findById(id).orElseThrow();
// 在事务内访问懒加载对象,触发加载
dept.getEmployees().size();
return dept;
}
}
解决方案2:使用DTO
@Service
public class DepartmentService {
@Autowired
private DepartmentRepository departmentRepository;
@Transactional(readOnly = true)
public DepartmentDTO getDepartment(Long id) {
Department dept = departmentRepository.findById(id).orElseThrow();
// 转换为DTO
return new DepartmentDTO(dept);
}
}
解决方案3:使用OSIV(不推荐)
spring:
jpa:
open-in-view: true # 开启OSIV(默认开启)
OSIV会在整个请求周期内保持Session,虽然方便,但可能导致性能问题和意外的查询。
# 十、最佳实践
# 1、实体设计原则
- 使用包装类型:
Long id而不是long id,避免0值的歧义 - 重写equals和hashCode:特别是在Set和Map中使用实体时
- 避免双向关联:除非真的需要,单向关联更简单
- 谨慎使用级联:特别是
CascadeType.REMOVE,容易误删数据 - 使用懒加载:默认使用
LAZY,需要时再用EAGER
# 2、Repository设计原则
- 继承JpaRepository:功能最全面
- 方法命名要规范:遵循Spring Data JPA的命名规则
- 复杂查询用@Query:别让方法名太长
- 动态查询用Specification:灵活且类型安全
# 3、Service设计原则
- 加事务注解:写操作必须加
@Transactional - 只读事务:查询操作加
@Transactional(readOnly = true) - 避免事务太大:拆分成小事务,提高并发性
- 处理懒加载:在事务内访问懒加载对象
# 4、性能优化建议
- 避免N+1查询:使用JOIN FETCH或EntityGraph
- 使用批量操作:批量插入、批量删除
- 使用投影查询:只查询需要的字段
- 合理使用索引:在常用查询字段上建索引
- 监控SQL:开发环境开启
show-sql,生产环境使用慢查询日志
# 5、常见错误
错误1:在循环中save
// 错误示例
for (User user : users) {
userRepository.save(user); // 每次都会发SQL
}
// 正确示例
userRepository.saveAll(users); // 批量保存
错误2:忘记加@Transactional
// 错误示例
public void updateUser(Long id, String email) {
User user = userRepository.findById(id).orElseThrow();
user.setEmail(email);
// 没有save,更新不会生效!
}
// 正确示例
@Transactional
public void updateUser(Long id, String email) {
User user = userRepository.findById(id).orElseThrow();
user.setEmail(email);
// 事务提交时自动更新
}
错误3:LazyInitializationException
// 错误示例
public Department getDepartment(Long id) {
return departmentRepository.findById(id).orElseThrow();
// 在Controller访问employees会报错
}
// 正确示例
@Transactional(readOnly = true)
public Department getDepartment(Long id) {
Department dept = departmentRepository.findById(id).orElseThrow();
dept.getEmployees().size(); // 在事务内触发加载
return dept;
}
# 十一、总结
JPA和Spring Data JPA大大简化了数据访问层的开发,让我们可以用面向对象的方式操作数据库。通过本文,你应该掌握了:
- JPA基础概念:实体映射、关系映射、生命周期等
- Repository使用:方法命名查询、
@Query、Specifications - 分页排序:
Pageable和Sort的使用 - 事务管理:声明式事务、事务传播行为
- 审计功能:自动记录创建时间、修改时间等
- 性能优化:避免N+1查询、批量操作、缓存等
掌握了这些知识,你就可以在项目中游刃有余地使用JPA了。但记住,JPA不是银弹,对于复杂的SQL查询和批量操作,MyBatis可能是更好的选择。选择合适的工具,才能事半功倍。
祝你变得更强!