0

0

Spring Data JPA 复合主键查询与最佳实践指南

霞舞

霞舞

发布时间:2025-12-08 23:31:25

|

283人浏览过

|

来源于php中文网

原创

Spring Data JPA 复合主键查询与最佳实践指南

本教程详细阐述了如何在spring data jpa中有效处理复合主键查询。文章首先指出`jparepository`对单一id类型的限制,进而提供了三种核心解决方案:直接使用`embeddedid`类型进行`findbyid`查询、利用spring data jpa的派生查询方法,以及通过`@query`注解自定义jpql查询。此外,教程还强调了使用现代日期时间api(如`localdate`)和健壮的`optional`处理机制(特别是结合自定义异常实现优雅的错误管理)等最佳实践。

Spring Data JPA 复合主键查询策略

在Spring Data JPA应用中,处理具有复合主键的实体是常见需求。然而,JpaRepository的findById()方法默认只接受一个单一类型的ID参数,这使得直接使用多个字段进行复合主键查询变得不直观。本文将深入探讨如何在Spring Data JPA中优雅地实现复合主键查询,并提供相关的最佳实践。

1. 理解复合主键的定义

首先,我们需要正确定义复合主键。Spring Data JPA通常通过@Embeddable注解的类结合@EmbeddedId注解在实体中使用。

以下是一个复合主键PlansPKId和使用它的Plans实体的示例:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.EqualsAndHashCode;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.Date; // 注意:推荐使用java.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 javax.persistence.*;
import java.util.HashSet;
import java.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 assignedMeds = new HashSet<>();
}

2. 使用EmbeddedId类型进行findById查询

JpaRepository接口的第二个泛型参数指定了实体的主键类型。对于复合主键,这个类型应该就是我们定义的@Embeddable类。

步骤:

Playground AI
Playground AI

AI图片生成和修图

下载
  1. 定义Repository接口:将PlansPKId作为JpaRepository的ID类型。

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface PlansRepository extends JpaRepository {
    }
  2. 调用findById方法:在查询时,需要创建一个PlansPKId的实例作为参数传递给findById。

    import java.util.Date; // 假设传入的planDate是java.util.Date类型
    import java.util.Optional;
    
    // ... 在某个服务类中
    public Plans findPlanByCompositeKey(long planId, Date planDate) {
        PlansPKId compositeId = new PlansPKId(planId, planDate);
        Optional optionalPlans = plansRepo.findById(compositeId);
        // 推荐使用orElseThrow进行健壮的Optional处理
        return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
    }

3. 利用派生查询方法

Spring Data JPA能够根据方法名自动生成查询。对于复合主键,可以通过引用EmbeddedId类的字段来构建查询方法。

步骤:

  1. 定义派生查询方法:方法名遵循findBy + EmbeddedId属性名 + EmbeddedId属性内的字段名的模式。

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    import java.util.Date;
    import java.util.Optional;
    
    @Repository
    public interface PlansRepository extends JpaRepository {
        // 根据复合主键的planId和planDate字段查找
        Optional findByPlansPKIdPlanIdAndPlansPKIdPlanDate(long planId, Date planDate);
    }
  2. 调用方法

    import java.util.Date;
    import java.util.Optional;
    
    // ... 在某个服务类中
    public Plans findPlanByDerivedQuery(long planId, Date planDate) {
        Optional optionalPlans = plansRepo.findByPlansPKIdPlanIdAndPlansPKIdPlanDate(planId, planDate);
        return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
    }

这种方法的缺点是当复合主键字段较多时,方法名可能会变得非常冗长。

4. 使用自定义JPQL查询

如果派生查询方法名过长或需要更复杂的查询逻辑,可以使用@Query注解定义JPQL(Java Persistence Query Language)查询。

步骤:

  1. 定义自定义查询方法:使用@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 java.util.Date;
    import java.util.Optional;
    
    @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);
    }
  2. 调用方法

    import java.util.Date;
    import java.util.Optional;
    
    // ... 在某个服务类中
    public Plans findPlanByCustomQuery(long planId, Date planDate) {
        Optional optionalPlans = plansRepo.findByCompositeId(planId, planDate);
        return optionalPlans.orElseThrow(() -> new RuntimeException("Plan not found with id " + planId + " and date " + planDate));
    }

这种方法提供了最大的灵活性,并且可以使方法名更具可读性。

5. 最佳实践与注意事项

5.1 现代化日期时间API

强烈建议使用java.time包下的日期时间API(如LocalDate, LocalDateTime, ZonedDateTime)代替传统的java.util.Date。java.time提供了更好的线程安全性、不变性、清晰的语义和更强大的功能。

示例:将PlansPKId中的Date替换为LocalDate

import java.time.LocalDate; // 导入LocalDate

@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class PlansPKId implements Serializable {
    private long planId;
    private LocalDate planDate; // 使用LocalDate
}

5.2 健壮的Optional处理与优雅的异常管理

直接调用Optional.get()而不检查其是否存在是非常危险的,可能导致NoSuchElementException。推荐使用orElseThrow()结合自定义异常来提供更清晰、更友好的错误信息和HTTP状态码

实现步骤:

  1. 定义一个抽象的NotFoundException基类

    import java.util.Map;
    import java.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 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 "))));
        }
    }
  2. 为特定实体创建具体的NotFoundException子类

    import java.util.Map;
    import java.util.function.Supplier;
    
    public class PlansNotFoundException extends NotFoundException {
    
        private PlansNotFoundException(final Map identifiers) {
            super("plans", identifiers);
        }
    
        public static Supplier 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 id(final long id) {
            return () -> new MedsNotFoundException("id", id);
        }
    }
  3. 在服务层中使用orElseThrow

    import java.util.Date;
    import java.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.save(plans);
        }
    }

通过这种方式,当实体未找到时,会抛出特定的NotFoundException子类。结合Spring的@ControllerAdvice,可以将这些异常统一处理为HTTP 404 Not Found响应,并返回包含有意义错误信息的消息体,极大提升API的用户体验和可维护性。

总结

处理Spring Data JPA中的复合主键查询有多种策略,包括直接使用EmbeddedId类型与findById、利用派生查询方法以及自定义JPQL查询。每种方法都有其适用场景,开发者应根据具体需求和代码可读性进行选择。同时,遵循使用现代日期时间API和健壮的Optional处理(特别是结合自定义异常)的最佳实践,将有助于构建更稳定、更易于维护的Spring Data JPA应用程序。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

840

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

742

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

737

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

Java JVM 原理与性能调优实战
Java JVM 原理与性能调优实战

本专题系统讲解 Java 虚拟机(JVM)的核心工作原理与性能调优方法,包括 JVM 内存结构、对象创建与回收流程、垃圾回收器(Serial、CMS、G1、ZGC)对比分析、常见内存泄漏与性能瓶颈排查,以及 JVM 参数调优与监控工具(jstat、jmap、jvisualvm)的实战使用。通过真实案例,帮助学习者掌握 Java 应用在生产环境中的性能分析与优化能力。

11

2026.01.20

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.7万人学习

C# 教程
C# 教程

共94课时 | 7.1万人学习

Java 教程
Java 教程

共578课时 | 48.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号