Spring Data JPA 复合主键查询方法
本文深入探讨了SpringDataJPA在处理复合主键时findById()方法的正确使用方式。针对JpaRepository仅支持单一ID类型的限制,教程详细介绍了如何将EmbeddedId类型作为仓库的ID类型,并通过实例展示了使用findById()、自定义方法名查询以及JPQL@Query进行数据检索。同时,强调了处理Optional返回值的最佳实践,包括引入优雅的异常处理机制和推荐使用现代日期时间API,以构建健壮且可维护的SpringBoot应用。

本文深入探讨了Spring Data JPA在处理复合主键时findById()方法的正确使用方式。针对JpaRepository仅支持单一ID类型的限制,教程详细介绍了如何将EmbeddedId类型作为仓库的ID类型,并通过实例展示了使用findById()、自定义方法名查询以及JPQL @Query进行数据检索。同时,强调了处理Optional返回值的最佳实践,包括引入优雅的异常处理机制和推荐使用现代日期时间API,以构建健壮且可维护的Spring Boot应用。
理解 Spring Data JPA 与复合主键
在使用 Spring Data JPA 进行数据操作时,我们经常会遇到需要通过主键查询实体的情况。对于简单的单字段主键,JpaRepository 提供的 findById() 方法能够直接接收主键值并返回对应的 Optional 实体。然而,当实体采用复合主键(由多个字段组成的主键)时,直接尝试将多个字段作为参数传递给 findById() 是行不通的。
这是因为 JpaRepository 的定义 JpaRepository
正确配置复合主键实体与仓库
首先,确保你的复合主键类被正确地定义为 @Embeddable,并且你的实体类使用 @EmbeddedId 注解来引用这个复合主键。
// 复合主键类
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class PlansPKId implements Serializable {
private long planId;
private Date planDate; // 注意:推荐使用 java.time.* 替代 java.util.Date
}
// 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "plans")
public class Plans {
@EmbeddedId
private PlansPKId plansPKId;
@Column
private String planName;
@Column
private String weekday;
// ... 其他字段和关联关系
}接下来,关键在于正确定义你的 JpaRepository。你需要将复合主键类 (PlansPKId) 指定为仓库的 ID 类型:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PlansRepository extends JpaRepository{ // 仓库现在以 PlansPKId 作为其 ID 类型 }
使用 findById() 查询复合主键实体
一旦 PlansRepository 被正确定义,你就可以使用 findById() 方法了。此时,findById() 需要一个 PlansPKId 实例作为参数,而不是多个单独的字段。
import java.util.Date;
import java.util.Optional;
// ...
public Plans assignPlansToMeds(Long id, Long planId, Date planDate) {
// ... 获取 Meds 实体,这里也应遵循 Optional 的安全处理
Meds meds = medsRepo.findById(id)
.orElseThrow(() -> MedsNotFoundException.id(id).get()); // 示例:安全处理 Optional
// 创建复合主键实例
PlansPKId compositeId = new PlansPKId(planId, planDate);
// 使用 findById 查询
Plans plans = plansRepo.findById(compositeId)
.orElseThrow(() -> PlansNotFoundException.idAndDate(planId, planDate).get()); // 示例:安全处理 Optional
// ... 后续业务逻辑
// medsSet = plans.getAssignedMeds();
// medsSet.add(meds);
// plans.setAssignedMeds(medsSet);
// return plansRepo.save(plans);
return plans; // 示例返回
}自定义查询方法
除了使用 findById() 传递 PlansPKId 实例外,你还可以通过自定义仓库方法来实现更灵活的查询。
1. 基于方法名派生查询
Spring Data JPA 能够根据方法名自动生成查询。对于复合主键,你可以通过在方法名中引用复合主键类的字段来构建查询。
import java.util.Date; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PlansRepository extends JpaRepository{ // 通过方法名派生查询,Spring 会根据 PlansPKId 的 planId 和 planDate 字段生成查询 Optional findByPlansPKIdPlanIdAndPlansPKIdPlanDate(long planId, Date planDate); }
调用示例:
// ... 在服务层 OptionaloptionalPlans = plansRepo.findByPlansPKIdPlanIdAndPlansPKIdPlanDate(planId, planDate); Plans plans = optionalPlans.orElseThrow(() -> PlansNotFoundException.idAndDate(planId, planDate).get());
2. 使用 @Query 注解定义 JPQL 查询
如果你不喜欢冗长的方法名,或者需要更复杂的查询逻辑,可以使用 @Query 注解编写 JPQL (Java Persistence Query Language) 查询。
import java.util.Date; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface PlansRepository extends JpaRepository{ @Query("select p from Plans p where p.plansPKId.planId = :planId and p.plansPKId.planDate = :planDate") Optional findByCompositeId(@Param("planId") long planId, @Param("planDate") Date planDate); }
调用示例:
// ... 在服务层 OptionaloptionalPlans = plansRepo.findByCompositeId(planId, planDate); Plans plans = optionalPlans.orElseThrow(() -> PlansNotFoundException.idAndDate(planId, planDate).get());
最佳实践与注意事项
1. 安全处理 Optional 返回值
Spring Data JPA 的 findById() 和自定义查询方法通常返回 Optional
推荐使用 orElseThrow() 结合自定义异常,提供清晰的错误信息。
2. 优雅的错误处理机制
为了提供更好的用户体验和更清晰的错误日志,建议实现一套统一的异常处理机制。以下是一个基于 NotFoundException 抽象类的示例:
import java.util.Map;
import java.util.stream.Collectors;
// 抽象的 NotFoundException 基类
public abstract class NotFoundException extends RuntimeException {
protected NotFoundException(final String object, final String identifierName, final Object identifier) {
super(String.format("No %s found with %s %s", object, identifierName, identifier));
}
protected NotFoundException(final String object, final Map identifiers) {
super(String.format("No %s found with %s", object,
identifiers.entrySet().stream()
.map(entry -> String.format("%s %s", entry.getKey(), entry.getValue()))
.collect(Collectors.joining(" and "))));
}
} 针对特定实体创建具体的异常类,并提供静态工厂方法来方便地创建异常实例和 Supplier:
import java.util.Map;
import java.util.function.Supplier;
// PlansNotFoundException
public class PlansNotFoundException extends NotFoundException {
private PlansNotFoundException(final Map identifiers) {
super("plans", identifiers);
}
public static Supplier idAndDate(final long planId, final Date planDate) {
return () -> new PlansNotFoundException(Map.of("planId", planId, "planDate", planDate));
}
}
// MedsNotFoundException
public class MedsNotFoundException extends NotFoundException {
private MedsNotFoundException(final String identifierName, final Object identifier) {
super("meds", identifierName, identifier);
}
public static Supplier id(final long id) {
return () -> new MedsNotFoundException("id", id);
}
} 在服务层使用时,代码将更加简洁和健壮:
// ...
Meds meds = medsRepo.findById(id).orElseThrow(MedsNotFoundException.id(id));
Plans plans = plansRepo.findById(new PlansPKId(planId, planDate))
.orElseThrow(PlansNotFoundException.idAndDate(planId, planDate));
// ...结合 Spring 的 @ControllerAdvice 和 @ExceptionHandler,你可以将这些 NotFoundException 映射到 HTTP 404 状态码,并返回友好的错误消息。
3. 使用现代日期时间 API
在你的复合主键定义中,使用了 java.util.Date。强烈建议改用 java.time 包下的现代日期时间 API,如 LocalDate、LocalDateTime 或 ZonedDateTime。这些类提供了更好的线程安全性、不变性以及更丰富的日期时间操作功能。
例如,将 PlansPKId 中的 Date planDate 替换为 LocalDate planDate:
import java.time.LocalDate; // 导入 LocalDate
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class PlansPKId implements Serializable {
private long planId;
private LocalDate planDate; // 使用 LocalDate
}这将使你的代码更健壮,并避免 java.util.Date 带来的一些常见问题。
总结
处理 Spring Data JPA 中的复合主键需要理解 JpaRepository 的设计哲学。通过将复合主键封装为 @Embeddable 类型并正确配置仓库,你可以有效地使用 findById() 进行查询。此外,Spring Data JPA 还提供了基于方法名派生查询和 @Query 注解的强大功能,以满足更复杂的查询需求。在实际开发中,结合安全处理 Optional 返回值、实现优雅的异常处理机制以及采用现代日期时间 API,能够显著提升应用程序的健壮性、可维护性和用户体验。
Adobe Acrobat Pro 是面向办公、法务、财务、教育及出版用户的专业PDF处理软件,可在Mac上创建、编辑、转换、合并和保护文档,并提供OCR文字识别、表单、批注、内容比较、敏感信息遮盖及电子签名功能。
Blender 是一款免费开源、跨平台的专业 3D 创作软件,集建模、动画、渲染、视频编辑与视觉合成等功能于一体,广泛应用于影视动画、游戏设计和建筑可视化等领域。软件支持 Cycles 物理渲染器与 Eevee 实时渲染引擎,并提供多边形建模、骨骼绑定、物理模拟等专业工具。Blender 兼容 Windows、macOS 和 Linux 系统,安装包轻巧、运行流畅,依托活跃的全球开发者社区持续更新,是从初学者到专业创作者都值得选择的正版 3D 创作工具。
Shapr3D是一款面向工业设计、机械工程、建筑概念和三维打印工作流的CAD软件。Mac版采用Parasolid建模内核,支持草图约束、实体建模、工程图、可视化渲染及常见CAD格式交换,并可通过账户在多台设备之间同步项目。
PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。
AutoCAD 是 Autodesk 推出的专业计算机辅助设计软件,Windows 版支持精确二维绘图、三维实体与曲面建模、图纸标注、布局发布和任务自动化,并包含多套行业专业化工具组合,适用于建筑、机械、电气、工程建设及制造设计人员。














