
本教程详细阐述了如何在spring data jpa中有效处理复合主键查询。文章首先指出`jparepository`对单一id类型的限制,进而提供了三种核心解决方案:直接使用`embeddedid`类型进行`findbyid`查询、利用spring data jpa的派生查询方法,以及通过`@query`注解自定义jpql查询。此外,教程还强调了使用现代日期时间api(如`localdate`)和健壮的`optional`处理机制(特别是结合自定义异常实现优雅的错误管理)等最佳实践。
在Spring Data JPA应用中,处理具有复合主键的实体是常见需求。然而,JpaRepository的findById()方法默认只接受一个单一类型的ID参数,这使得直接使用多个字段进行复合主键查询变得不直观。本文将深入探讨如何在Spring Data JPA中优雅地实现复合主键查询,并提供相关的最佳实践。
首先,我们需要正确定义复合主键。Spring Data JPA通常通过@Embeddable注解的类结合@EmbeddedId注解在实体中使用。
以下是一个复合主键PlansPKId和使用它的Plans实体的示例:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.EqualsAndHashCode;
import j*ax.persistence.Embeddable;
import j*a.io.Serializable;
import j*a.util.Date; // 注意:推荐使用j*a.time.* 包下的日期类型
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class PlansPKId implements Serializable {
private long planId;
private Date planDate; // 格式: yyyy-mm-dd
}import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import j*ax.persistence.*;
import j*a.util.HashSet;
import j*a.util.Set;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "plans")
public class Plans {
@EmbeddedId
private PlansPKId plansPKId;
@Column
private String planName;
@Column
private String weekday;
@ManyToMany
@JoinTable(name = "Plan_Meds", joinColumns = {
@JoinColumn(name = "planDate", referencedColumnName = "planDate"),
@JoinColumn(name = "planId", referencedColumnName = "planId") }, inverseJoinColumns = @JoinColumn(name = "planId")) // 修正:这里inverseJoinColumns应该是Meds的id
private Set<Meds> assignedMeds = new HashSet<>();
}JpaRepository接口的第二个泛型参数指定了实体的主键类型。对于复合主键,这个类型应该就是我们定义的@Embeddable类。
步骤:
歌者PPT
歌者PPT,AI 写 PPT 永久免费
358
查看详情
定义Repository接口:将PlansPKId作为JpaRepository的ID类型。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PlansRepository extends JpaRepository<Plans, PlansPKId> {
}调用findById方法:在查询时,需要创建一个PlansPKId的实例作为参数传递给findById。
import j*a.util.Date; // 假设传入的planDate是j*a.util.Date类型
import j*a.util.Optional;
// ... 在某个服务类中
public Plans findPlanByCompositeKey(long planId, Date planDate) {
PlansPKId compositeId = new PlansPKId(planId, planDate);
Optional<Plans> optionalPlans = plansRepo.findById(compositeId);
// 推荐使用orElseThrow进行健壮的Optional处理
return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
}Spring Data JPA能够根据方法名自动生成查询。对于复合主键,可以通过引用EmbeddedId类的字段来构建查询方法。
步骤:
定义派生查询方法:方法名遵循findBy + EmbeddedId属性名 + EmbeddedId属性内的字段名的模式。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import j*a.util.Date;
import j*a.util.Optional;
@Repository
public interface PlansRepository extends JpaRepository<Plans, PlansPKId> {
// 根据复合主键的planId和planDate字段查找
Optional<Plans> findByPlansPKIdPlanIdAndPlansPKIdPlanDate(long planId, Date planDate);
}调用方法:
import j*a.util.Date;
import j*a.util.Optional;
// ... 在某个服务类中
public Plans findPlanByDerivedQuery(long planId, Date planDate) {
Optional<Plans> optionalPlans = plansRepo.findByPlansPKIdPlanIdAndPlansPKIdPlanDate(planId, planDate);
return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
}这种方法的缺点是当复合主键字段较多时,方法名可能会变得非常冗长。
如果派生查询方法名过长或需要更复杂的查询逻辑,可以使用@Query注解定义JPQL(J*a Persistence Query Language)查询。
步骤:
定义自定义查询方法:使用@Query注解编写JPQL,并通过@Param注解将方法参数绑定到查询中的命名参数。
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;
import j*a.util.Date;
import j*a.util.Optional;
@Repository
public interface PlansRepository extends JpaRepository<Plans, PlansPKId> {
@Query("select p from Plans p where p.plansPKId.planId = :planId and p.plansPKId.planDate = :planDate")
Optional<Plans> findByCompositeId(@Param("planId") long planId, @Param("planDate") Date planDate);
}调用方法:
import j*a.util.Date;
import j*a.util.Optional;
// ... 在某个服务类中
public Plans findPlanByCustomQuery(long planId, Date planDate) {
Optional<Plans> optionalPlans = plansRepo.findByCompositeId(planId, planDate);
return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
}这种方法提供了最大的灵活性,并且可以使方法名更具可读性。
强烈建议使用j*a.time包下的日期时间API(如LocalDate, LocalDateTime, ZonedDateTime)代替传统的j*a.util.Date。j*a.time提供了更好的线程安全性、不变性、清晰的语义和更强大的功能。
示例:将PlansPKId中的Date替换为LocalDate
import j*a.time.LocalDate; // 导入LocalDate
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class PlansPKId implements Serializable {
private long planId;
private LocalDate planDate; // 使用LocalDate
}直接调用Optional.get()而不检查其是否存在是非常危险的,可能导致NoSuchElementException。推荐使用orElseThrow()结合自定义异常来提供更清晰、更友好的错误信息和HTTP状态码。
实现步骤:
定义一个抽象的NotFoundException基类:
import j*a.util.Map;
import j*a.util.stream.Collectors;
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<String, Object> 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 "))));
}
}为特定实体创建具体的NotFoundException子类:
import j*a.util.Map;
import j*a.util.function.Supplier;
public class PlansNotFoundException extends NotFoundException {
private PlansNotFoundException(final Map<String, Object> identifiers) {
super("plans", identifiers);
}
public static Supplier<PlansNotFoundException> idAndDate(final long planId, final Date planDate) {
// 注意:如果使用LocalDate,这里也应传入LocalDate
return () -> new PlansNotFoundException(Map.of("id", planId, "date", planDate));
}
}
public class MedsNotFoundException extends NotFoundException {
private MedsNotFoundException(final String identifierName, final Object identifier) {
super("meds", identifierName, identifier);
}
public static Supplier<MedsNotFoundException> id(final long id) {
return () -> new MedsNotFoundExceptio
n("id", id);
}
}在服务层中使用orElseThrow:
import j*a.util.Date;
import j*a.util.Optional;
import org.springframework.stereotype.Service;
@Service
public class AssignService { // 假设这是您的服务层
private final PlansRepository plansRepo;
private final MedsRepository medsRepo; // 假设有MedsRepository
public AssignService(PlansRepository plansRepo, MedsRepository medsRepo) {
this.plansRepo = plansRepo;
this.medsRepo = medsRepo;
}
public Plans assignPlansToMeds(Long id, Long planId, Date planDate) {
// 使用orElseThrow结合自定义异常
Meds meds = medsRepo.findById(id)
.orElseThrow(MedsNotFoundException.id(id));
Plans plans = plansRepo.findById(new PlansPKId(planId, planDate))
.orElseThrow(PlansNotFoundException.idAndDate(planId, planDate));
// ... 后续业务逻辑
plans.getAssignedMeds().add(meds);
return plansRepo.s*e(plans);
}
}通过这种方式,当实体未找到时,会抛出特定的NotFoundException子类。结合Spring的@ControllerAdvice,可以将这些异常统一处理为HTTP 404 Not Found响应,并返回包含有意义错误信息的消息体,极大提升API的用户体验和可维护性。
处理Spring Data JPA中的复合主键查询有多种策略,包括直接使用EmbeddedId类型与findById、利用派生查询方法以及自定义JPQL查询。每种方法都有其适用场景,开发者应根据具体需求和代码可读性进行选择。同时,遵循使用现代日期时间API和健壮的Optional处理(特别是结合自定义异常)的最佳实践,将有助于构建更稳定、更易于维护的Spring Data JPA应用程序。
以上就是Spring Data JPA 复合主键查询与最佳实践指南的详细内容,更多请关注其它相关文章!
# stream
# java
# 保利物业营销推广策划书
# 乐从seo优化教程
# 金华专注企业网站建设
# 永州百度营销推广方法
# 方山网站推广技术指导
# 湖州网站建设系统
# 抖音宣传关键词排名方法
# 海南seo排名快速入门
# 平遥网络推广营销
# 金华优化网站建设推广项目
# 是一个
# 访问控制
# 在某个
# 如何用
# 错误信息
# 类中
# 推荐使用
# 子类
# 自定义
# 主键
# yy
# 代码可读性
# 状态码
相关栏目:
【
Google疑问12 】
【
Facebook疑问10 】
【
优化推广96088 】
【
技术知识133117 】
【
IDC资讯59369 】
【
网络运营7196 】
【
IT资讯61894 】
相关推荐:
大众点评了却看不到是怎么回事
《桃源记2》资源采集攻略
AI图层蒙版怎么用_AI图层蒙版应用技巧与设计实例
j*a中ArrayBlockingQueue的使用
《植物大战僵尸3》火龙草作用介绍
海棠阅读网页版_进入海棠网页版在线阅读中心
mysql怎么查询数据_mysql基础查询语句使用教程
mysql数据库索引类型有哪些_mysql索引类型解析
C#中的Record类型有什么优势?C# 9新特性Record与Class的用法区别
小红书网页版在线直达 小红书网页版免费登录入口
《深林》冬季章节图文攻略
安居客移动经纪人怎么设置自动回复?-安居客移动经纪人设置自动回复的方法
12306夜间购票失败? | 查看官方公布的暂停服务公告与应对方案
智学网app怎么登录忘记密码_智学网app忘记密码找回与重新登录操作方法
excel怎么计算平均值 excel平均函数*ERAGE使用教学
C++ priority_queue怎么用_C++优先队列底层实现与自定义比较器
猫眼电影app如何参与官方的抽奖活动_猫眼电影官方抽奖参与方法
Dagster资产间数据传递与用户配置管理教程
阿里云共享相册入口在哪
VS Code的时间线(Timeline)视图:您的代码时光机
《小宇宙》标记不友善评论方法
火柴人战争网页版在线玩
4399正版网页版入口高清直达链接
店铺如何做视频号推广?做视频号推广有用吗?
Eclipse开发J*a快速入门
小米civi如何设置锁屏时间
@Team是什么?揭秘团队含义
繁花漫画使用教程
如何通过settings.json个性化您的VS Code体验
荣耀Magic7拍照夜景噪点处理_荣耀Magic7相机优化
GBA模拟器手柄按键设置
J*aScript事件处理:优化键盘输入与表单提交的实践指南
抖音号显示企业机构号是什么意思?企业机构号申请条件是什么?
在VS Code中利用AI辅助进行代码迁移
《海底捞》点外卖方法
小红书如何引流到私信?引流到私信有用吗?
lol小红书怎么|直播|?lol小红书|直播|是什么意思?
微博网页版入口链接 微博网页版在线互动平台
一点万象签到领积分指南
嘴唇干裂起皮怎么办 唇部护理与预防干裂的方法【详解】
win11关机几秒又自己开机 Win11关机自动重启问题修复
WooCommerce 新客户订单自动添加管理员备注教程
Python实时数据流中高效查找最大最小值
C#解析来自网络的XML流数据 实时错误处理与重试机制
电脑“无法访问指定设备、路径或文件”怎么办?五种权限设置方法
猫眼电影app如何设置电影上映提醒_猫眼电影上映提醒设置教程
优酷下载视频的清晰度怎么选_优酷缓存清晰度设置与选择指南
作业帮网页版不用下载入口 在线问老师快速答疑
Excel宏怎么删除_Excel中删除宏的详细操作流程
《全民k歌》网页版最新登录入口一览
2025-12-08
运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。