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 在视图层触发 SQL | 设 open-in-view: false |