5.2 SpringDataJPA:接口即实现


5.2 Spring Data JPA:接口即实现

本节摘要:Spring Data JPA 把数据访问的抽象推到"只写接口"的程度——你声明方法名,框架在启动期生成实现。本节走完实体定义、接口派生查询、自定义查询三步,划清派生查询的适用边界,并给出懒加载陷阱的机理与规避。

三步搭起数据访问

第一步:定义实体。 类即表、字段即列,注解完成映射:

@Entity @Table(name = "orders") public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long userId; private BigDecimal amount; @Enumerated(EnumType.STRING) private OrderStatus status; // 枚举必须声明按名存储 @ManyToOne(fetch = FetchType.LAZY) private User user; // 省略 getter setter }

第二步:声明接口。 不写实现类,方法名就是查询语义:

public interface OrderRepository extends JpaRepository<Order, Long> { List<Order> findByUserIdOrderByAmountDesc(Long userId); long countByStatus(OrderStatus status); @Query("SELECT o FROM Order o WHERE o.amount > :min") List<Order> findBigOrders(@Param("min") BigDecimal min); }

第三步:直接注入使用。 启动时框架为接口生成代理实现并注册进容器,业务代码照常注入:

@Service public class OrderQueryService { private final OrderRepository repo; public OrderQueryService(OrderRepository repo) { this.repo = repo; } public List<Order> topOrders(Long userId) { return repo.findByUserIdOrderByAmountDesc(userId); } }

findByUserIdOrderByAmountDesc 这串名字不是约定俗成的黑话,而是可拆解的语法:查询过滤字段、排序字段、限制条数都能编进方法名。名字拆不出来时(聚合、子查询、复杂关联),退到 @Query 手写对象查询语句——派生查询负责八成常规场景,剩下两成交给显式查询,别硬凑方法名。

图 5-2 方法名到 SQL 的翻译过程

图 5-2 方法名到 SQL 的翻译过程

懒加载:旅程边界上的暗雷

上面实体里关联的用户字段标了懒加载:查询订单时不取用户,第一次访问 getUser() 时才发 SQL。问题在于 SQL 发出的时机必须有会话在场,而会话在事务方法返回时关闭。控制器里直接访问懒字段:

@GetMapping("/orders/{id}") public Order detail(@PathVariable Long id) { return repo.findById(id).orElseThrow(); // 序列化时访问 user 字段 → 会话已关 → 抛延迟初始化异常 }

三种修法:查询时就抓取(改急加载或用带抓取提示的查询);在 Service 层(事务内)把需要的数据装配成视图对象返回;或用专为只读视图设计的接口投影,直接查所需列。工程上我选第二种:事务边界内装配视图,边界外只有纯数据,边界纪律清晰,不依赖全局配置的运气。

⚠️ 另一个数量级陷阱:循环里对每条订单访问懒字段,N 条订单发 N 加一条 SQL。列表页出现"数据库 CPU 突增",先数日志里同一条查询出现的次数。

实战:亲手制造并修好一次 N 加一查询

背景:列表接口上线后数据库 CPU 飙升,需要复现、定位、修复三步闭环。操作:第一步复现——在配置里打开语句日志(把持久化包调成最细级别),调用订单列表接口,日志里"查用户"那条语句出现次数等于列表条数,N 加一实锤。第二步定位——查看序列化代码,发现视图对象在控制器里访问了订单的懒加载用户字段,每访问一次触发一条 SQL。第三步修复——在 Service 的事务方法内把列表一次性装配成视图:

@Transactional(readOnly = true) // 只读事务内完成所有懒加载 public List<OrderListVO> listByUser(Long userId) { return repo.findByUserIdOrderByAmountDesc(userId).stream() .map(o -> new OrderListVO( // 访问懒字段发生在事务内 o.getId(), o.getAmount(), o.getUser().getName())) .toList(); }

修复后再看日志:查用户语句只剩一次——框架把同一会话内已加载的对象缓存在持久化上下文里,后续访问直接命中。解读:N 加一的根因不是"懒加载不好",而是"懒字段访问发生在会话关闭之后或循环里";把装配收进事务边界内、用一条带抓取提示的查询一次取足,两条路任选其一即可。变式:把方法上的只读标记去掉对比日志条数,理解只读标记对快照与脏检查的优化作用。

学完本节的务实建议:给团队的仓储层立两条规矩——派生查询方法名最长两三个关键词,超出即改显式查询;列表接口必须在事务内装配视图,禁止把实体直接泄出边界。两条规矩背后都是同一件事:让"会话边界"这条隐形线在代码评审里可见。

本节要点回顾

  • 三步走:实体映射、接口派生、注入即用,实现由框架在启动期生成
  • 派生查询覆盖常规八成,复杂查询显式声明,不硬凑名字
  • 懒字段访问必须在会话存活期间,视图装配放事务内
  • 警惕 N 加一查询,列表场景优先一次抓足

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U