Spring Data JPA
本页聚焦 Repository、Specification、审计和 DTO 投影。实体生命周期、延迟加载与 N+1 问题见 ORM 深入。
JPA 把数据库表映射成 Java 对象,你操作对象,框架帮你生成 SQL。不用手写 SELECT/INSERT,方法名就能查询,审计字段自动填充。但 ORM 的「魔法」背后藏着 N+1 陷阱、懒加载异常、持久化上下文脏检查——理解原理才能用好。本章从实体定义到 Repository,从 Specification 动态查询到审计功能,把 JPA 的核心用法和常见坑讲清楚。
1. 实体定义与关联映射
1.1 基础实体
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false)
private String email;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UserStatus status = UserStatus.ACTIVE;
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
@PrePersist
protected void onCreate() {
this.createTime = LocalDateTime.now();
}
// getters & setters
}
public enum UserStatus {
ACTIVE, INACTIVE, BANNED
}1.2 关联映射
数据库表之间的关系(一对一、一对多、多对多)通过 JPA 注解映射到 Java 对象:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// 一对多:一个用户有多个订单
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("createTime DESC")
private List<Order> orders = new ArrayList<>();
// 多对多:用户和角色
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String orderNo;
// 多对一:多个订单属于一个用户
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
// 一对一:一个订单对应一笔支付
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "payment_id")
private Payment payment;
}1.3 关联映射要点
| 关系 | 注解 | fetch 默认 | 注意事项 |
|---|---|---|---|
@OneToOne | @JoinColumn | EAGER | 考虑用 LAZY 避免 N+1 |
@ManyToOne | @JoinColumn | EAGER | 默认 EAGER 通常是性能陷阱 |
@OneToMany | mappedBy | LAZY | 用 orphanRemoval=true 管理子实体生命周期 |
@ManyToMany | @JoinTable | LAZY | 避免用 CascadeType.ALL,用单独管理 |
踩坑提醒:
@ManyToOne默认是FetchType.EAGER,意味着查询 Order 时会自动 JOIN User。如果一次查 100 个 Order,会发出 100 条查询 User 的 SQL(N+1 问题)。所有关联都建议用LAZY,需要时再用JOIN FETCH显式加载。
2. Repository 接口
2.1 方法命名查询
Spring Data 解析方法名自动生成 SQL,不用写一行查询代码:
public interface UserRepository extends JpaRepository<User, Long> {
// SELECT * FROM users WHERE username = ?
Optional<User> findByUsername(String username);
// SELECT * FROM users WHERE email LIKE '%keyword%'
List<User> findByEmailContaining(String keyword);
// SELECT * FROM users WHERE status = ? ORDER BY create_time DESC
List<User> findByStatusOrderByCreateTimeDesc(UserStatus status);
// SELECT * FROM users WHERE username LIKE ? AND status = ?
Page<User> findByUsernameContainingAndStatus(String keyword, UserStatus status,
Pageable pageable);
// EXISTS 查询
boolean existsByEmail(String email);
// COUNT 查询
long countByStatus(UserStatus status);
// DELETE 查询(需要 @Transactional)
@Transactional
int deleteByStatus(UserStatus status);
}2.2 @Query JPQL
方法名太长或查询太复杂时,用 @Query 注解直接写 JPQL:
public interface UserRepository extends JpaRepository<User, Long> {
// JPQL(面向对象的查询语言,用实体名而非表名)
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> search(@Param("keyword") String keyword);
// 原生 SQL
@Query(value = "SELECT * FROM users WHERE create_time > :date", nativeQuery = true)
List<User> findRecentUsers(@Param("date") LocalDateTime date);
// 关联查询(解决 N+1)
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders WHERE u.id = :id")
Optional<User> findByIdWithOrders(@Param("id") Long id);
// 更新查询(必须加 @Modifying)
@Modifying
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
// 分页 + 排序
@Query("SELECT u FROM User u WHERE u.status = :status")
Page<User> findActiveUsers(@Param("status") UserStatus status, Pageable pageable);
}踩坑提醒:
@Modifying方法必须在@Transactional环境中执行(Service 层加@Transactional或方法本身加)。另外@Modifying默认不自动清除持久化上下文,如果更新后在同一事务中查询,可能得到旧数据——加@Modifying(clearAutomatically = true)解决。
2.3 N+1 问题与解决方案
N+1 问题是 JPA 最常见的性能陷阱:
// ❌ N+1 问题:查 100 个 Order,每个 Order 再查一次 User
// 总共 1 + 100 = 101 条 SQL
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
System.out.println(order.getUser().getName()); // 触发懒加载
}解决方案:
public interface OrderRepository extends JpaRepository<Order, Long> {
// 方案一:JOIN FETCH
@Query("SELECT o FROM Order o JOIN FETCH o.user")
List<Order> findAllWithUser();
// 方案二:@EntityGraph
@EntityGraph(attributePaths = {"user", "items"})
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatusWithDetails(@Param("status") String status);
}3. Specification 动态查询
搜索功能有多个可选条件,用户可能只填其中几个。Specification 让你动态组合查询条件:
// 定义 Specification
public class UserSpecs {
public static Specification<User> usernameLike(String username) {
return (root, query, cb) -> {
if (StringUtils.isBlank(username)) return null;
return cb.like(root.get("username"), "%" + username + "%");
};
}
public static Specification<User> emailEquals(String email) {
return (root, query, cb) -> {
if (StringUtils.isBlank(email)) return null;
return cb.equal(root.get("email"), email);
};
}
public static Specification<User> statusEquals(UserStatus status) {
return (root, query, cb) -> {
if (status == null) return null;
return cb.equal(root.get("status"), status);
};
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) -> {
if (date == null) return null;
return cb.greaterThan(root.get("createTime"), date);
};
}
}
// Repository 继承 JpaSpecificationExecutor
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {
}
// Service 中动态组合
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public Page<User> search(UserSearchRequest request, Pageable pageable) {
Specification<User> spec = Specification
.where(UserSpecs.usernameLike(request.getUsername()))
.and(UserSpecs.emailEquals(request.getEmail()))
.and(UserSpecs.statusEquals(request.getStatus()))
.and(UserSpecs.createdAfter(request.getStartTime()));
return userRepository.findAll(spec, pageable);
}
}
// 搜索请求 DTO
@Data
public class UserSearchRequest {
private String username;
private String email;
private UserStatus status;
private LocalDateTime startTime;
}踩坑提醒:
Specification返回null表示该条件不参与查询(Spring Data 会忽略 null specification)。但第一个where不能传 null——用where(spec1).and(spec2)而不是where(null).and(spec1)。
4. 审计功能
每张表都有 created_by、created_at、updated_by、updated_at 字段,每个实体都要手动赋值?JPA 审计自动填充。
4.1 开启审计
// 1. 启用 JPA 审计
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
// 从 SecurityContext 获取当前用户
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName)
.or(() -> Optional.of("SYSTEM"));
}
}4.2 审计基类
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// getters & setters
}4.3 实体继承基类
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// createdAt, updatedAt, createdBy, updatedBy 自动填充
}踩坑提醒:
@CreatedDate和@LastModifiedDate只在 JPApersist和merge时触发。如果用@Query的@Modifying直接写 SQL 更新,审计字段不会自动更新——因为绕过了 JPA 的实体生命周期。
5. 原生 SQL 与 DTO 投影
5.1 原生 SQL 查询
public interface UserRepository extends JpaRepository<User, Long> {
@Query(value = "SELECT u.*, o.order_count " +
"FROM users u LEFT JOIN " +
"(SELECT user_id, COUNT(*) as order_count FROM orders GROUP BY user_id) o " +
"ON u.id = o.user_id WHERE u.status = :status", nativeQuery = true)
List<UserWithOrderCount> findUsersWithOrderCount(@Param("status") String status);
}5.2 DTO 投影
只查需要的字段,减少内存占用:
// DTO
public record UserSummary(Long id, String username, String email) {}
// Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT new com.example.dto.UserSummary(u.id, u.username, u.email) " +
"FROM User u WHERE u.status = 'ACTIVE'")
List<UserSummary> findActiveUserSummaries();
}6. 最佳实践
- 关联关系用
FetchType.LAZY——避免 N+1 查询问题 - 需要时用
JOIN FETCH或@EntityGraph一次性加载关联数据 - 审计字段用
@CreatedDate/@LastModifiedDate——无需手动维护 - 批量操作用
@Modifying+@Query——避免逐条查询再更新 - DTO 投影优于实体查询——只查需要的字段,减少内存占用
- 动态查询用 Specification——搜索条件不确定时,比拼接 JPQL 更安全