简介
Spring Data JPA 是 Spring Data 家族里专门用来简化 JPA 开发的模块。
它不是一个新的 ORM 规范。
更准确地说:
1JPA 是规范 2Hibernate 是常见实现 3Spring Data JPA 是 Spring 对 JPA Repository 的封装 4
在 Spring Boot 项目里,常见调用链大致是:
1Controller 2 | 3 v 4Service 5 | 6 v 7Repository 8 | 9 v 10Spring Data JPA 11 | 12 v 13Hibernate 14 | 15 v 16JDBC 17 | 18 v 19数据库 20
它的核心目标是:
1用 Repository 接口表达数据访问,让常见 CRUD、分页、排序、简单查询少写很多样板代码。 2
一句话概括:
1Spring Data JPA 适合用实体对象驱动数据库操作,尤其适合标准 CRUD、分页列表、简单条件查询和领域模型比较清楚的项目。 2
JPA、Hibernate、Spring Data JPA 的关系
这几个概念经常一起出现。
JPA
JPA 全称是 Jakarta Persistence API。
它是一套持久化规范。
常见注解有:
@Entity@Table@Id@GeneratedValue@Column@OneToMany@ManyToOne
常见接口有:
EntityManagerQueryTypedQuery
Hibernate
Hibernate 是 JPA 的常见实现。
它负责真正执行对象映射、SQL 生成、脏检查、缓存、关联加载等工作。
Spring Data JPA
Spring Data JPA 在 JPA 之上又封装了一层 Repository。
比如定义一个接口:
1public interface UserRepository extends JpaRepository<User, Long> { 2} 3
就能直接使用:
1userRepository.save(user); 2userRepository.findById(1L); 3userRepository.findAll(); 4userRepository.deleteById(1L); 5
简单理解:
1Hibernate 负责 ORM 2Spring Data JPA 负责 Repository 抽象 3Spring Boot 负责自动配置 4
Maven 依赖
Spring Boot 项目里直接引入 starter:
1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-data-jpa</artifactId> 4</dependency> 5
数据库驱动以 MySQL 为例:
1<dependency> 2 <groupId>com.mysql</groupId> 3 <artifactId>mysql-connector-j</artifactId> 4 <scope>runtime</scope> 5</dependency> 6
如果需要写 Web 接口:
1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-web</artifactId> 4</dependency> 5
Spring Boot 3 以后使用的是 jakarta.persistence 包。
也就是实体注解来自:
1import jakarta.persistence.Entity; 2import jakarta.persistence.Table; 3
不是旧的:
1import javax.persistence.Entity; 2
数据源和 JPA 配置
application.yml 示例:
1spring: 2 datasource: 3 url: jdbc:mysql://localhost:3306/jpa_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 4 username: root 5 password: 123456 6 driver-class-name: com.mysql.cj.jdbc.Driver 7 8 jpa: 9 hibernate: 10 ddl-auto: update 11 show-sql: true 12 properties: 13 hibernate: 14 format_sql: true 15
常见配置说明:
| 配置 | 作用 |
|---|---|
| ddl-auto | 控制 Hibernate 是否自动处理表结构 |
| show-sql | 是否打印 SQL |
| format_sql | 是否格式化 SQL |
ddl-auto 常见取值:
| 值 | 含义 | 常见场景 |
|---|---|---|
| none | 不处理表结构 | 生产环境 |
| validate | 校验实体和表结构 | 稳定环境 |
| update | 根据实体更新表结构 | 开发环境 |
| create | 启动时删除并重建表 | 本地临时测试 |
| create-drop | 启动创建,关闭删除 | 测试场景 |
生产环境通常使用:
1spring: 2 jpa: 3 hibernate: 4 ddl-auto: none 5
表结构更适合交给 Flyway、Liquibase 或数据库变更流程管理。
准备演示表
下面用用户表和订单表做示例。
1DROP TABLE IF EXISTS orders; 2DROP TABLE IF EXISTS users; 3 4CREATE TABLE users ( 5 id BIGINT PRIMARY KEY AUTO_INCREMENT, 6 username VARCHAR(50) NOT NULL, 7 email VARCHAR(100) NOT NULL, 8 age INT NOT NULL, 9 status VARCHAR(20) NOT NULL, 10 version INT NOT NULL DEFAULT 0, 11 created_at DATETIME NOT NULL, 12 updated_at DATETIME NULL, 13 UNIQUE KEY uk_users_email (email) 14); 15 16CREATE TABLE orders ( 17 id BIGINT PRIMARY KEY AUTO_INCREMENT, 18 user_id BIGINT NOT NULL, 19 order_no VARCHAR(50) NOT NULL, 20 amount DECIMAL(10, 2) NOT NULL, 21 status VARCHAR(20) NOT NULL, 22 created_at DATETIME NOT NULL, 23 INDEX idx_orders_user_id (user_id) 24); 25 26INSERT INTO users (username, email, age, status, version, created_at, updated_at) VALUES 27('张三', 'zhangsan@example.com', 20, 'ACTIVE', 0, '2026-01-01 10:00:00', NULL), 28('李四', 'lisi@example.com', 25, 'ACTIVE', 0, '2026-01-02 10:00:00', NULL), 29('王五', 'wangwu@example.com', 17, 'DISABLED', 0, '2026-01-03 10:00:00', NULL); 30 31INSERT INTO orders (user_id, order_no, amount, status, created_at) VALUES 32(1, 'A001', 99.00, 'PAID', '2026-02-01 10:00:00'), 33(1, 'A002', 260.00, 'PAID', '2026-02-02 10:00:00'), 34(2, 'A003', 35.50, 'CANCELLED', '2026-02-03 10:00:00'); 35
实体类
用户实体:
1package com.example.demo.entity; 2 3import jakarta.persistence.Column; 4import jakarta.persistence.Entity; 5import jakarta.persistence.GeneratedValue; 6import jakarta.persistence.GenerationType; 7import jakarta.persistence.Id; 8import jakarta.persistence.OneToMany; 9import jakarta.persistence.Table; 10import jakarta.persistence.Version; 11 12import java.time.LocalDateTime; 13import java.util.ArrayList; 14import java.util.List; 15 16@Entity 17@Table(name = "users") 18public class User { 19 20 @Id 21 @GeneratedValue(strategy = GenerationType.IDENTITY) 22 private Long id; 23 24 @Column(nullable = false, length = 50) 25 private String username; 26 27 @Column(nullable = false, unique = true, length = 100) 28 private String email; 29 30 @Column(nullable = false) 31 private Integer age; 32 33 @Column(nullable = false, length = 20) 34 private String status; 35 36 @Version 37 private Integer version; 38 39 @Column(name = "created_at", nullable = false) 40 private LocalDateTime createdAt; 41 42 @Column(name = "updated_at") 43 private LocalDateTime updatedAt; 44 45 @OneToMany(mappedBy = "user") 46 private List<Order> orders = new ArrayList<>(); 47 48 public Long getId() { 49 return id; 50 } 51 52 public void setId(Long id) { 53 this.id = id; 54 } 55 56 public String getUsername() { 57 return username; 58 } 59 60 public void setUsername(String username) { 61 this.username = username; 62 } 63 64 public String getEmail() { 65 return email; 66 } 67 68 public void setEmail(String email) { 69 this.email = email; 70 } 71 72 public Integer getAge() { 73 return age; 74 } 75 76 public void setAge(Integer age) { 77 this.age = age; 78 } 79 80 public String getStatus() { 81 return status; 82 } 83 84 public void setStatus(String status) { 85 this.status = status; 86 } 87 88 public Integer getVersion() { 89 return version; 90 } 91 92 public void setVersion(Integer version) { 93 this.version = version; 94 } 95 96 public LocalDateTime getCreatedAt() { 97 return createdAt; 98 } 99 100 public void setCreatedAt(LocalDateTime createdAt) { 101 this.createdAt = createdAt; 102 } 103 104 public LocalDateTime getUpdatedAt() { 105 return updatedAt; 106 } 107 108 public void setUpdatedAt(LocalDateTime updatedAt) { 109 this.updatedAt = updatedAt; 110 } 111 112 public List<Order> getOrders() { 113 return orders; 114 } 115 116 public void setOrders(List<Order> orders) { 117 this.orders = orders; 118 } 119} 120
订单实体:
1package com.example.demo.entity; 2 3import jakarta.persistence.Column; 4import jakarta.persistence.Entity; 5import jakarta.persistence.FetchType; 6import jakarta.persistence.GeneratedValue; 7import jakarta.persistence.GenerationType; 8import jakarta.persistence.Id; 9import jakarta.persistence.JoinColumn; 10import jakarta.persistence.ManyToOne; 11import jakarta.persistence.Table; 12 13import java.math.BigDecimal; 14import java.time.LocalDateTime; 15 16@Entity 17@Table(name = "orders") 18public class Order { 19 20 @Id 21 @GeneratedValue(strategy = GenerationType.IDENTITY) 22 private Long id; 23 24 @ManyToOne(fetch = FetchType.LAZY) 25 @JoinColumn(name = "user_id", nullable = false) 26 private User user; 27 28 @Column(name = "order_no", nullable = false, length = 50) 29 private String orderNo; 30 31 @Column(nullable = false) 32 private BigDecimal amount; 33 34 @Column(nullable = false, length = 20) 35 private String status; 36 37 @Column(name = "created_at", nullable = false) 38 private LocalDateTime createdAt; 39 40 // getter setter 41} 42
几个重点:
@Entity:标记 JPA 实体@Table(name = "users"):指定表名@Id:主键字段@GeneratedValue(strategy = GenerationType.IDENTITY):数据库自增主键@Column:指定字段名、长度、是否可空等@Version:乐观锁版本字段@OneToMany、@ManyToOne:实体关联关系
Repository 接口
1package com.example.demo.repository; 2 3import com.example.demo.entity.User; 4import org.springframework.data.jpa.repository.JpaRepository; 5import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 7import java.util.Optional; 8 9public interface UserRepository extends JpaRepository<User, Long>, 10 JpaSpecificationExecutor<User> { 11 12 Optional<User> findByEmail(String email); 13} 14
JpaRepository<User, Long> 里的两个泛型含义:
1User:实体类型 2Long:主键类型 3
继承后可以直接使用:
1save 2findById 3findAll 4deleteById 5count 6existsById 7
JpaSpecificationExecutor<User> 用来支持动态条件查询。
新增数据
1User user = new User(); 2user.setUsername("赵六"); 3user.setEmail("zhaoliu@example.com"); 4user.setAge(28); 5user.setStatus("ACTIVE"); 6user.setCreatedAt(LocalDateTime.now()); 7 8User saved = userRepository.save(user); 9 10System.out.println(saved.getId()); 11
save 可以用于新增,也可以用于更新。
判断标准大致是:
1实体没有主键,通常执行 insert 2实体有主键,通常执行 update 或 merge 3
实际 SQL 会由 Hibernate 根据实体状态生成。
根据 ID 查询
1Optional<User> user = userRepository.findById(1L); 2
返回 Optional<User>,表示数据可能存在,也可能不存在。
业务层可以这样处理:
1User user = userRepository.findById(id) 2 .orElseThrow(() -> new IllegalArgumentException("用户不存在")); 3
查询全部
1List<User> users = userRepository.findAll(); 2
业务表数据量较大时,findAll() 要谨慎使用。
更常见的是分页查询。
删除数据
按 ID 删除:
1userRepository.deleteById(1L); 2
按实体删除:
1userRepository.delete(user); 2
删除操作通常放在事务里。
JpaRepository 常用方法
| 方法 | 作用 |
|---|---|
| save(entity) | 新增或更新 |
| saveAll(entities) | 批量保存 |
| findById(id) | 按 ID 查询 |
| findAll() | 查询全部 |
| findAllById(ids) | 按 ID 集合查询 |
| deleteById(id) | 按 ID 删除 |
| delete(entity) | 按实体删除 |
| existsById(id) | 判断 ID 是否存在 |
| count() | 统计总数 |
| findAll(Pageable) | 分页查询 |
| findAll(Sort) | 排序查询 |
方法名查询
Spring Data JPA 支持按方法名生成查询。
比如:
1Optional<User> findByEmail(String email); 2
会根据方法名生成类似查询:
1where email = ? 2
常见写法:
1List<User> findByStatus(String status); 2 3List<User> findByAgeGreaterThan(Integer age); 4 5List<User> findByUsernameContaining(String keyword); 6 7List<User> findByStatusAndAgeGreaterThan(String status, Integer age); 8 9List<User> findByStatusOrderByIdDesc(String status); 10 11boolean existsByEmail(String email); 12 13long countByStatus(String status); 14
常见关键词:
| 关键词 | 示例 | 含义 |
|---|---|---|
| And | findByStatusAndAge | 并且 |
| Or | findByStatusOrAge | 或者 |
| Between | findByAgeBetween | 区间 |
| LessThan | findByAgeLessThan | 小于 |
| GreaterThan | findByAgeGreaterThan | 大于 |
| Containing | findByUsernameContaining | 包含,通常是 %keyword% |
| StartingWith | findByUsernameStartingWith | 前缀匹配 |
| EndingWith | findByUsernameEndingWith | 后缀匹配 |
| In | findByIdIn | IN 查询 |
| OrderBy | findByStatusOrderByIdDesc | 排序 |
方法名查询适合简单条件。
如果方法名变得很长,通常应该换成 @Query 或 Specification。
分页和排序
分页使用 Pageable。
1Pageable pageable = PageRequest.of( 2 0, 3 10, 4 Sort.by(Sort.Direction.DESC, "id") 5); 6 7Page<User> page = userRepository.findAll(pageable); 8
注意页码从 0 开始。
1第 1 页:0 2第 2 页:1 3第 3 页:2 4
Page<User> 常用方法:
1List<User> content = page.getContent(); 2long totalElements = page.getTotalElements(); 3int totalPages = page.getTotalPages(); 4int number = page.getNumber(); 5int size = page.getSize(); 6boolean hasNext = page.hasNext(); 7
如果只需要当前页数据,不需要总数,可以使用 Slice。
1Slice<User> findByStatus(String status, Pageable pageable); 2
Page 会查询总数。
Slice 通常只关心是否还有下一页。
@Query 自定义 JPQL
方法名查询不适合复杂条件时,可以使用 @Query。
JPQL 面向实体和属性,不是直接面向表和字段。
1@Query(""" 2 select u 3 from User u 4 where u.status = :status 5 and u.age >= :minAge 6 order by u.id desc 7 """) 8List<User> findActiveUsers(@Param("status") String status, 9 @Param("minAge") Integer minAge); 10
这里的 User 是实体类名。
status、age、id 是实体属性名。
不是数据库表名和字段名。
@Query 原生 SQL
如果需要直接写数据库 SQL,可以设置 nativeQuery = true。
1@Query(value = """ 2 select * 3 from users 4 where status = :status 5 and age >= :minAge 6 order by id desc 7 """, nativeQuery = true) 8List<User> findByNativeSql(@Param("status") String status, 9 @Param("minAge") Integer minAge); 10
原生 SQL 适合:
- 数据库特有语法
- 复杂报表
- 性能调优 SQL
- 很难用 JPQL 表达的查询
修改查询
update、delete 这类修改语句需要 @Modifying。
1@Modifying 2@Query(""" 3 update User u 4 set u.status = :status, 5 u.updatedAt = :updatedAt 6 where u.id = :id 7 """) 8int updateStatus(@Param("id") Long id, 9 @Param("status") String status, 10 @Param("updatedAt") LocalDateTime updatedAt); 11
调用这类方法时,需要事务。
通常放在 Service 层:
1@Transactional 2public void disable(Long id) { 3 userRepository.updateStatus(id, "DISABLED", LocalDateTime.now()); 4} 5
Specification 动态查询
多条件筛选时,Specification 很常用。
Repository 需要继承:
1public interface UserRepository extends JpaRepository<User, Long>, 2 JpaSpecificationExecutor<User> { 3} 4
动态条件示例:
1public Specification<User> buildSpec(String keyword, String status, Integer minAge) { 2 return (root, query, criteriaBuilder) -> { 3 List<Predicate> predicates = new ArrayList<>(); 4 5 if (keyword != null && !keyword.isBlank()) { 6 Predicate usernameLike = criteriaBuilder.like( 7 root.get("username"), 8 "%" + keyword + "%" 9 ); 10 Predicate emailLike = criteriaBuilder.like( 11 root.get("email"), 12 "%" + keyword + "%" 13 ); 14 predicates.add(criteriaBuilder.or(usernameLike, emailLike)); 15 } 16 17 if (status != null && !status.isBlank()) { 18 predicates.add(criteriaBuilder.equal(root.get("status"), status)); 19 } 20 21 if (minAge != null) { 22 predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("age"), minAge)); 23 } 24 25 return criteriaBuilder.and(predicates.toArray(new Predicate[0])); 26 }; 27} 28
调用:
1Specification<User> spec = buildSpec(keyword, status, minAge); 2Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); 3 4Page<User> page = userRepository.findAll(spec, pageable); 5
Specification 适合:
1查询条件很多 2每个条件都可选 3列表页筛选项较多 4需要复用条件片段 5
Projection 投影
有些接口只需要返回部分字段。
可以使用接口投影:
1public interface UserSummary { 2 Long getId(); 3 String getUsername(); 4 String getEmail(); 5} 6
Repository:
1List<UserSummary> findByStatus(String status); 2
这样接口返回的不是完整 User 实体,而是只包含部分字段的视图。
也可以使用 JPQL 构造 DTO。
DTO:
1package com.example.demo.dto; 2 3public class UserSummaryDTO { 4 private Long id; 5 private String username; 6 private String email; 7 8 public UserSummaryDTO(Long id, String username, String email) { 9 this.id = id; 10 this.username = username; 11 this.email = email; 12 } 13 14 // getter 15} 16
Repository:
1@Query(""" 2 select new com.example.demo.dto.UserSummaryDTO(u.id, u.username, u.email) 3 from User u 4 where u.status = :status 5 """) 6List<UserSummaryDTO> findSummaryByStatus(@Param("status") String status); 7
投影适合:
- 列表页
- 下拉选项
- 只读接口
- 不希望暴露完整实体的接口
审计字段
常见审计字段:
1created_at 2updated_at 3created_by 4updated_by 5
Spring Data JPA 可以自动填充创建时间和更新时间。
实体类先加监听器:
1import org.springframework.data.annotation.CreatedDate; 2import org.springframework.data.annotation.LastModifiedDate; 3import org.springframework.data.jpa.domain.support.AuditingEntityListener; 4 5import jakarta.persistence.EntityListeners; 6import jakarta.persistence.MappedSuperclass; 7import java.time.LocalDateTime; 8 9@MappedSuperclass 10@EntityListeners(AuditingEntityListener.class) 11public abstract class BaseEntity { 12 13 @CreatedDate 14 @Column(name = "created_at", nullable = false, updatable = false) 15 private LocalDateTime createdAt; 16 17 @LastModifiedDate 18 @Column(name = "updated_at") 19 private LocalDateTime updatedAt; 20 21 public LocalDateTime getCreatedAt() { 22 return createdAt; 23 } 24 25 public LocalDateTime getUpdatedAt() { 26 return updatedAt; 27 } 28} 29
启动类启用审计:
1import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 2 3@EnableJpaAuditing 4@SpringBootApplication 5public class JpaDemoApplication { 6} 7
实体继承:
1public class User extends BaseEntity { 2} 3
这样保存和更新时,时间字段会自动处理。
乐观锁
JPA 支持乐观锁。
实体字段:
1@Version 2private Integer version; 3
更新时会带上版本条件。
大致逻辑:
1where id = ? and version = ? 2
更新成功后版本号增加。
如果版本不一致,会抛出乐观锁相关异常。
这种机制适合防止并发修改覆盖。
关联关系和懒加载
实体关联常见有:
@OneToOne@OneToMany@ManyToOne@ManyToMany
订单和用户的关系通常是:
1多个订单属于一个用户。 2
订单实体:
1@ManyToOne(fetch = FetchType.LAZY) 2@JoinColumn(name = "user_id", nullable = false) 3private User user; 4
FetchType.LAZY 表示懒加载。
只有访问 order.getUser() 时,才加载用户。
懒加载要注意事务边界。
如果事务已经关闭,再访问懒加载属性,可能出现懒加载异常。
EntityGraph
EntityGraph 可以在查询时指定需要一起加载的关联对象。
Repository:
1@EntityGraph(attributePaths = "orders") 2Optional<User> findWithOrdersById(Long id); 3
查询用户时同时加载订单。
这类写法常用于处理 N + 1 查询问题。
所谓 N + 1,大致是:
1先查 1 次用户列表 2再为每个用户各查 1 次订单 3
如果用户有 100 条,就可能变成 101 次查询。
可以用 EntityGraph、fetch join、DTO 查询等方式处理。
Service 实战
1package com.example.demo.service; 2 3import com.example.demo.dto.UserSummaryDTO; 4import com.example.demo.entity.User; 5import com.example.demo.repository.UserRepository; 6import jakarta.persistence.EntityNotFoundException; 7import jakarta.persistence.criteria.Predicate; 8import org.springframework.data.domain.Page; 9import org.springframework.data.domain.PageRequest; 10import org.springframework.data.domain.Pageable; 11import org.springframework.data.domain.Sort; 12import org.springframework.data.jpa.domain.Specification; 13import org.springframework.stereotype.Service; 14import org.springframework.transaction.annotation.Transactional; 15 16import java.time.LocalDateTime; 17import java.util.ArrayList; 18import java.util.List; 19 20@Service 21@Transactional(readOnly = true) 22public class UserService { 23 24 private final UserRepository userRepository; 25 26 public UserService(UserRepository userRepository) { 27 this.userRepository = userRepository; 28 } 29 30 @Transactional 31 public Long create(User user) { 32 user.setStatus("ACTIVE"); 33 user.setCreatedAt(LocalDateTime.now()); 34 35 User saved = userRepository.save(user); 36 37 return saved.getId(); 38 } 39 40 public User detail(Long id) { 41 return userRepository.findById(id) 42 .orElseThrow(() -> new EntityNotFoundException("用户不存在")); 43 } 44 45 public Page<User> page(String keyword, String status, Integer minAge, int pageNumber, int pageSize) { 46 Specification<User> spec = buildSpec(keyword, status, minAge); 47 48 Pageable pageable = PageRequest.of( 49 pageNumber - 1, 50 pageSize, 51 Sort.by(Sort.Direction.DESC, "id") 52 ); 53 54 return userRepository.findAll(spec, pageable); 55 } 56 57 @Transactional 58 public void updateEmail(Long id, String email) { 59 User user = detail(id); 60 user.setEmail(email); 61 user.setUpdatedAt(LocalDateTime.now()); 62 } 63 64 @Transactional 65 public void disable(Long id) { 66 userRepository.updateStatus(id, "DISABLED", LocalDateTime.now()); 67 } 68 69 @Transactional 70 public void remove(Long id) { 71 userRepository.deleteById(id); 72 } 73 74 private Specification<User> buildSpec(String keyword, String status, Integer minAge) { 75 return (root, query, criteriaBuilder) -> { 76 List<Predicate> predicates = new ArrayList<>(); 77 78 if (keyword != null && !keyword.isBlank()) { 79 Predicate usernameLike = criteriaBuilder.like(root.get("username"), "%" + keyword + "%"); 80 Predicate emailLike = criteriaBuilder.like(root.get("email"), "%" + keyword + "%"); 81 predicates.add(criteriaBuilder.or(usernameLike, emailLike)); 82 } 83 84 if (status != null && !status.isBlank()) { 85 predicates.add(criteriaBuilder.equal(root.get("status"), status)); 86 } 87 88 if (minAge != null) { 89 predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("age"), minAge)); 90 } 91 92 return criteriaBuilder.and(predicates.toArray(new Predicate[0])); 93 }; 94 } 95} 96
这里有一个 JPA 常见点:
1User user = detail(id); 2user.setEmail(email); 3
在事务里查出来的实体是托管状态。
修改属性后,不一定需要显式调用 save。
事务提交时,Hibernate 会做脏检查并生成更新 SQL。
Controller 示例
1package com.example.demo.controller; 2 3import com.example.demo.entity.User; 4import com.example.demo.service.UserService; 5import org.springframework.data.domain.Page; 6import org.springframework.web.bind.annotation.DeleteMapping; 7import org.springframework.web.bind.annotation.GetMapping; 8import org.springframework.web.bind.annotation.PathVariable; 9import org.springframework.web.bind.annotation.PostMapping; 10import org.springframework.web.bind.annotation.PutMapping; 11import org.springframework.web.bind.annotation.RequestBody; 12import org.springframework.web.bind.annotation.RequestMapping; 13import org.springframework.web.bind.annotation.RequestParam; 14import org.springframework.web.bind.annotation.RestController; 15 16@RestController 17@RequestMapping("/users") 18public class UserController { 19 20 private final UserService userService; 21 22 public UserController(UserService userService) { 23 this.userService = userService; 24 } 25 26 @PostMapping 27 public Long create(@RequestBody User user) { 28 return userService.create(user); 29 } 30 31 @GetMapping("/{id}") 32 public User detail(@PathVariable Long id) { 33 return userService.detail(id); 34 } 35 36 @GetMapping 37 public Page<User> page(@RequestParam(required = false) String keyword, 38 @RequestParam(required = false) String status, 39 @RequestParam(required = false) Integer minAge, 40 @RequestParam(defaultValue = "1") int pageNumber, 41 @RequestParam(defaultValue = "10") int pageSize) { 42 return userService.page(keyword, status, minAge, pageNumber, pageSize); 43 } 44 45 @PutMapping("/{id}/email") 46 public void updateEmail(@PathVariable Long id, @RequestParam String email) { 47 userService.updateEmail(id, email); 48 } 49 50 @PutMapping("/{id}/disable") 51 public void disable(@PathVariable Long id) { 52 userService.disable(id); 53 } 54 55 @DeleteMapping("/{id}") 56 public void remove(@PathVariable Long id) { 57 userService.remove(id); 58 } 59} 60
实际接口不一定直接返回实体。
更常见的是返回 DTO,避免把关联字段、内部字段、懒加载对象直接暴露出去。
事务边界
JPA 非常依赖事务边界。
常见写法:
1@Service 2@Transactional(readOnly = true) 3public class UserService { 4 5 @Transactional 6 public Long create(User user) { 7 return userRepository.save(user).getId(); 8 } 9} 10
查询方法默认只读事务。
写入方法单独加普通事务。
这样做的好处:
- 查询语义更清楚
- 写操作有事务保护
- 懒加载和脏检查行为更稳定
Spring Data JPA 和 MyBatis 的区别
| 对比项 | Spring Data JPA | MyBatis |
|---|---|---|
| 思路 | 面向对象和实体关系 | 面向 SQL |
| 常规 CRUD | Repository 自动提供 | 通常手写 SQL |
| 动态查询 | 方法名、Specification、Criteria | XML 动态 SQL |
| 复杂 SQL | JPQL / 原生 SQL | XML SQL 更直接 |
| 关联关系 | 实体注解表达 | SQL join + ResultMap |
| 性能控制 | 关注懒加载、N+1、事务 | 关注 SQL 和执行计划 |
| 适合场景 | 领域模型清楚、CRUD 多 | SQL 复杂、报表多 |
简单理解:
1Spring Data JPA 更适合实体关系清楚的业务系统 2MyBatis 更适合 SQL 控制要求高的系统 3
常见使用建议
生产环境谨慎使用 ddl-auto update
ddl-auto: update 很适合本地开发。
生产环境里,表结构变更通常需要审批、回滚方案和版本记录。
更常见的是:
1spring: 2 jpa: 3 hibernate: 4 ddl-auto: validate 5
或者:
1spring: 2 jpa: 3 hibernate: 4 ddl-auto: none 5
表结构交给数据库迁移工具。
Repository 方法名不要过长
方法名查询很方便。
比如:
1findByStatusAndAgeGreaterThan 2
但如果变成:
1findByStatusAndAgeGreaterThanAndEmailContainingAndUsernameStartingWithOrderByCreatedAtDesc 2
可读性会下降。
这时更适合使用:
@QuerySpecification- 自定义 Repository 实现
控制实体返回范围
实体类通常包含很多数据库字段和关联关系。
接口层直接返回实体,可能带来:
- 字段暴露过多
- 懒加载触发异常
- JSON 循环引用
- 不必要的关联查询
更常见的方式是返回 DTO 或 Projection。
关联查询注意 N + 1
JPA 关联映射很方便,但也容易触发额外查询。
列表页如果要展示关联字段,可以优先考虑:
@EntityGraph- JPQL
fetch join - DTO 查询
- Projection
复杂报表优先考虑原生 SQL
Spring Data JPA 很适合常规业务数据访问。
但复杂报表、窗口函数、数据库特有语法、大量聚合统计,原生 SQL 通常更清楚。
可以使用:
1@Query(nativeQuery = true) 2
或者把复杂查询放到 MyBatis、JdbcTemplate、jOOQ 等更直接的 SQL 工具里。
常用注解汇总
| 注解 | 作用 |
|---|---|
| @Entity | 标记 JPA 实体 |
| @Table | 指定表名 |
| @Id | 指定主键 |
| @GeneratedValue | 指定主键生成策略 |
| @Column | 指定字段映射 |
| @Version | 乐观锁版本字段 |
| @OneToMany | 一对多关系 |
| @ManyToOne | 多对一关系 |
| @JoinColumn | 指定外键字段 |
| @Query | 自定义 JPQL 或原生 SQL |
| @Modifying | 标记更新或删除语句 |
| @EntityGraph | 指定查询时加载的关联属性 |
| @CreatedDate | 自动填充创建时间 |
| @LastModifiedDate | 自动填充更新时间 |
| @EnableJpaAuditing | 启用审计 |
常用接口和类汇总
| 名称 | 作用 |
|---|---|
| JpaRepository<T, ID> | 通用 Repository |
| JpaSpecificationExecutor<T> | Specification 动态查询 |
| Pageable | 分页参数 |
| PageRequest | 创建分页参数 |
| Page<T> | 分页结果,带总数 |
| Slice<T> | 分页切片,不一定查总数 |
| Sort | 排序参数 |
| Specification<T> | 动态条件 |
| EntityManager | JPA 原生入口 |
总结
Spring Data JPA 的重点是用 Repository 和实体关系来组织数据访问。
常见开发流程是:
1建表 2写 Entity 3写 Repository 4写 Service 5写 Controller 6根据复杂度选择方法名查询、@Query、Specification、Projection 7
适合它的场景:
- 实体关系清楚
- 标准 CRUD 多
- 分页列表多
- 业务更关注对象模型
- SQL 不需要处处手写控制
需要重点关注的地方:
- 事务边界
- 懒加载
- N + 1 查询
- DTO 和 Projection
ddl-auto的环境差异- 复杂 SQL 的处理边界
掌握这些内容后,Spring Data JPA 已经可以覆盖大多数常规业务系统的数据访问层开发。
《Java Spring Data JPA 实战指南:Repository 查询、分页与实体映射》 是转载文章,点击查看原文。