JPA 与 Hibernate

JPA(Jakarta Persistence API) 是 Java 的 ORM 标准规范;Hibernate 是其最主流的实现。Spring Data JPA 在 Hibernate 之上提供了 Repository 抽象,大幅简化数据访问层代码。


依赖配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>
# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: postgres
    password: secret
  jpa:
    hibernate:
      ddl-auto: validate          # 生产环境:validate;开发:update/create-drop
    show-sql: false               # 生产关闭,调试时开启
    open-in-view: false           # 关闭 OSIV,避免隐式懒加载
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 100   # 批量加载,减少 N+1
        jdbc:
          batch_size: 50                # 批量插入/更新
        order_inserts: true
        order_updates: true

一、实体定义

@Entity
@Table(name = "orders",
       indexes = @Index(name = "idx_order_user", columnList = "user_id"))
@EntityListeners(AuditingEntityListener.class)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)   // JPA 要求无参构造器
public class Order {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(name = "order_no", nullable = false, unique = true, length = 32)
    private String orderNo;
 
    @Column(nullable = false, precision = 12, scale = 2)
    private BigDecimal totalAmount;
 
    @Enumerated(EnumType.STRING)   // 存储枚举名称,不要用 ORDINAL
    @Column(nullable = false)
    private OrderStatus status;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
 
    @OneToMany(mappedBy = "order",
               cascade = CascadeType.ALL,
               orphanRemoval = true,
               fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
 
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
 
    @LastModifiedDate
    private LocalDateTime updatedAt;
 
    @Version
    private Long version;   // 乐观锁
 
    // 工厂方法(替代 public 构造器,确保不变式)
    public static Order create(String orderNo, User user) {
        Order order = new Order();
        order.orderNo = orderNo;
        order.user = user;
        order.status = OrderStatus.PENDING;
        order.totalAmount = BigDecimal.ZERO;
        return order;
    }
 
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
        totalAmount = totalAmount.add(item.getSubtotal());
    }
 
    public void confirm() {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("只有待确认订单可以确认");
        }
        status = OrderStatus.CONFIRMED;
    }
}

审计自动填充

@Configuration
@EnableJpaAuditing
public class JpaConfig { }

二、Repository

基础 CRUD

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
 
    // 方法名推导查询
    Optional<Order> findByOrderNo(String orderNo);
 
    List<Order> findByUserIdAndStatus(Long userId, OrderStatus status);
 
    boolean existsByOrderNo(String orderNo);
 
    // 统计
    long countByStatus(OrderStatus status);
 
    // 分页 + 排序
    Page<Order> findByUserId(Long userId, Pageable pageable);
}

JPQL 查询

public interface OrderRepository extends JpaRepository<Order, Long> {
 
    // JPQL(操作实体对象,不是表名/列名)
    @Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
    Optional<Order> findByIdWithItems(@Param("id") Long id);
 
    // 一次性批量更新(不触发实体生命周期回调)
    @Modifying
    @Transactional
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int updateStatusBatch(@Param("status") OrderStatus status,
                          @Param("ids") List<Long> ids);
 
    // 投影:只查需要的字段
    @Query("SELECT o.id AS id, o.orderNo AS orderNo, o.totalAmount AS totalAmount " +
           "FROM Order o WHERE o.user.id = :userId")
    List<OrderSummary> findSummaryByUserId(@Param("userId") Long userId);
}
 
// 投影接口(Spring Data 自动实现)
public interface OrderSummary {
    Long getId();
    String getOrderNo();
    BigDecimal getTotalAmount();
}

原生 SQL

@Query(value = "SELECT * FROM orders WHERE created_at > :since LIMIT :limit",
       nativeQuery = true)
List<Order> findRecentOrders(@Param("since") LocalDateTime since,
                              @Param("limit") int limit);

三、关联关系

@ManyToOne / @OneToMany

// 父表:User(一对多)
@Entity
public class User {
 
    @OneToMany(mappedBy = "user",
               cascade = {CascadeType.PERSIST, CascadeType.MERGE},
               fetch = FetchType.LAZY)   // 永远用 LAZY
    private List<Order> orders = new ArrayList<>();
}
 
// 子表:Order(多对一)
@Entity
public class Order {
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
}

@ManyToMany

@Entity
public class Product {
 
    @ManyToMany
    @JoinTable(
        name = "product_tag",
        joinColumns = @JoinColumn(name = "product_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private Set<Tag> tags = new HashSet<>();   // 多对多用 Set,避免重复
}

@Embedded — 值对象

@Embeddable
public record Address(
    @Column(nullable = false) String street,
    @Column(nullable = false) String city,
    @Column(nullable = false, length = 6) String zipCode
) { }
 
@Entity
public class User {
 
    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "street", column = @Column(name = "addr_street")),
        @AttributeOverride(name = "city",   column = @Column(name = "addr_city"))
    })
    private Address address;
}

四、N+1 问题与解决方案

N+1 是 JPA 最常见的性能陷阱:查询 N 条主记录后,每条各触发 1 次懒加载查询。

// 触发 N+1:查出 100 个 Order,每个 Order 访问 user 时发一条 SQL
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> System.out.println(o.getUser().getName())); // N+1!

解决方案 1:JOIN FETCH(JPQL)

@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.user JOIN FETCH o.items")
List<Order> findAllWithUserAndItems();

多个集合同时 JOIN FETCH 会产生笛卡尔积,只能对一个集合用 JOIN FETCH,其余改用 @BatchSize

解决方案 2:@BatchSize(批量加载)

@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 100)   // 懒加载时一次批量查 100 条,而非 N 次
private List<OrderItem> items;

全局配置(推荐):

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

解决方案 3:@EntityGraph

@EntityGraph(attributePaths = {"user", "items"})
@Query("SELECT o FROM Order o WHERE o.status = :status")
List<Order> findByStatusWithGraph(@Param("status") OrderStatus status);

解决方案 4:投影 / DTO 查询

直接查所需字段,完全绕过懒加载:

@Query("SELECT new com.example.dto.OrderDto(o.id, o.orderNo, u.name) " +
       "FROM Order o JOIN o.user u WHERE o.status = 'CONFIRMED'")
List<OrderDto> findConfirmedOrderDtos();

五、分页与排序

@Service
@RequiredArgsConstructor
public class OrderService {
 
    private final OrderRepository orderRepo;
 
    public Page<OrderSummary> getOrders(Long userId, int page, int size, String sortField) {
        Pageable pageable = PageRequest.of(
            page, size,
            Sort.by(Sort.Direction.DESC, sortField)
        );
        return orderRepo.findSummaryByUserId(userId, pageable);
    }
}

六、动态查询(Specification)

public class OrderSpec {
 
    public static Specification<Order> hasStatus(OrderStatus status) {
        return (root, query, cb) ->
            status == null ? null : cb.equal(root.get("status"), status);
    }
 
    public static Specification<Order> createdAfter(LocalDateTime since) {
        return (root, query, cb) ->
            since == null ? null : cb.greaterThan(root.get("createdAt"), since);
    }
 
    public static Specification<Order> belongsToUser(Long userId) {
        return (root, query, cb) ->
            userId == null ? null : cb.equal(root.get("user").get("id"), userId);
    }
}
 
// Repository 继承 JpaSpecificationExecutor
public interface OrderRepository extends JpaRepository<Order, Long>,
                                          JpaSpecificationExecutor<Order> { }
 
// 使用
Specification<Order> spec = Specification
    .where(OrderSpec.belongsToUser(userId))
    .and(OrderSpec.hasStatus(status))
    .and(OrderSpec.createdAfter(since));
 
Page<Order> result = orderRepo.findAll(spec, pageable);

七、乐观锁与悲观锁

// 乐观锁:@Version 字段(并发更新时自动检测冲突)
@Version
private Long version;
 
// 并发更新冲突时抛出 OptimisticLockException
try {
    orderRepo.save(order);
} catch (OptimisticLockException e) {
    throw new BusinessException("订单已被其他操作修改,请刷新后重试");
}
 
// 悲观锁:查询时加数据库行锁(SELECT ... FOR UPDATE)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT o FROM Order o WHERE o.id = :id")
Optional<Order> findByIdForUpdate(@Param("id") Long id);

事务管理详见 事务管理;分布式锁场景见 分布式锁


八、批量操作

@Service
@RequiredArgsConstructor
public class BulkImportService {
 
    @PersistenceContext
    private EntityManager em;
 
    @Transactional
    public void batchInsert(List<Product> products) {
        for (int i = 0; i < products.size(); i++) {
            em.persist(products.get(i));
            // 每 50 条刷入数据库并清空一级缓存,防止内存溢出
            if (i % 50 == 0) {
                em.flush();
                em.clear();
            }
        }
    }
}

常见陷阱速查

陷阱原因解决
N+1 查询懒加载在循环中触发JOIN FETCH / BatchSize / EntityGraph
LazyInitializationException在事务外访问懒加载属性关闭 OSIV;在 Service 层内加载
@Enumerated(ORDINAL)枚举顺序变动导致数据错乱改用 EnumType.STRING
双向关联内存溢出toString()/equals() 触发循环加载排除关联字段或用 Lombok @ToString(exclude=...)
大事务持有连接批量操作不 flush/clear分批 flush+clear 或用 Spring Batch
OSIV 隐式查询open-in-view=true 在视图层触发 SQLopen-in-view: false

相关链接