Spring Data JPA 高级查询 -----15
基于你项目已有的「公司 - 岗位」一对多实体,覆盖派生查询局限、N+1 问题与解决方案、JPQL、原生 SQL、命名查询全知识点,采用行业最佳实践写法,可直接复制运行。
一、前置说明:派生查询的局限(对应 156 集)
派生查询(方法名自动生成 SQL)适合简单单表条件查询,但有明显边界:
- 多表关联、复杂条件组合、聚合统计时,方法名会变得极长且难读
- 无法实现自定义返回字段、批量更新、复杂排序分页组合
- 超过 3 个查询条件后,可读性和维护性急剧下降
此时就需要用
@Query 写自定义查询,这也是本节的核心。二、N+1 问题:复现 + 解决方案(对应 157、158 集)
1. 什么是 N+1(通俗解释)
查询所有公司(1 条 SQL)→ 遍历每个公司的岗位列表 → 每个公司单独发 1 条 SQL 查岗位,N 个公司就是 N 条 SQL,总共 N+1 条。
数据量小时无感,数据量大了会成为性能杀手,数据库直接被打满。
2. 复现代码(默认懒加载场景)
java
运行
// 直接调用 findAll(),然后遍历 getJobs() 就会触发 N+1 List<Company> companies = companyRepository.findAll(); for (Company company : companies) { // 每次循环都会发一条SQL查岗位 System.out.println(company.getJobs().size()); }
开启
spring.jpa.show-sql=true 就能看到多条 SELECT 语句。3. 三种解决方案(最佳实践排序)
表格
| 方案 | 适用场景 | 优点 |
|---|---|---|
| Fetch Join(JPQL) | 单条 / 列表关联查询 | 最灵活,一次 SQL 查完所有数据 |
| @EntityGraph | 派生查询也想关联加载 | 不用写 JPQL,和派生查询无缝配合 |
| @BatchSize | 列表批量加载 | 把 N 条 SQL 合并成少量 SQL,适合大量数据遍历 |
三、实体类升级(补充注解支持高级查询)
1. 公司实体 Company.java
java
运行
package com.example.jobportal.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import java.util.ArrayList; import java.util.List; @Data @Entity @Table(name = "companies") // 命名查询定义(对应162集) @NamedQuery( name = "Company.searchByNameKeyword", query = "SELECT c FROM Company c WHERE c.name LIKE CONCAT('%', :keyword, '%')" ) // 实体图定义:解决N+1,指定要一起加载的关联 @NamedEntityGraph( name = "Company.withJobs", attributeNodes = @NamedAttributeNode("jobs") ) public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String address; @OneToMany( mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true ) @OnDelete(action = OnDeleteAction.CASCADE) // 方案3:批量抓取,N+1变少量SQL @BatchSize(size = 10) private List<Job> jobs = new ArrayList<>(); }
2. 岗位实体 Job.java
java
运行
package com.example.jobportal.entity; import jakarta.persistence.*; import lombok.Data; @Data @Entity @Table(name = "jobs") // 原生命名查询(对应162集) @NamedNativeQuery( name = "Job.statisticsByLocation", query = "SELECT location, COUNT(*) AS job_count, AVG(salary) AS avg_salary " + "FROM jobs GROUP BY location", resultSetMapping = "JobStatisticsMapping" ) // 原生查询结果映射 @SqlResultSetMapping( name = "JobStatisticsMapping", columns = { @ColumnResult(name = "location"), @ColumnResult(name = "job_count"), @ColumnResult(name = "avg_salary") } ) public class Job { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String jobTitle; private Integer salary; private String location; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "company_id") private Company company; }
四、Repository 层完整代码(核心)
1. CompanyRepository.java
java
运行
package com.example.jobportal.repository; import com.example.jobportal.entity.Company; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @Repository public interface CompanyRepository extends JpaRepository<Company, Long> { // ========== 1. 派生查询(简单场景) ========== Optional<Company> findByName(String name); List<Company> findByAddressContaining(String addressKeyword); // ========== 2. EntityGraph 解决 N+1(对应158集) ========== // 复用派生查询逻辑,同时加载岗位,只发1条SQL @Override @EntityGraph("Company.withJobs") List<Company> findAll(); @EntityGraph("Company.withJobs") Optional<Company> findByIdWithJobsById(Long id); // ========== 3. JPQL 自定义查询(对应159、160集) ========== /** * Fetch Join 解决 N+1:一次SQL查出公司+所有岗位 * LEFT JOIN FETCH 左连接抓取,没有岗位的公司也会返回 */ @Query("SELECT c FROM Company c LEFT JOIN FETCH c.jobs WHERE c.id = :id") Optional<Company> findByIdWithJobsJpql(@Param("id") Long id); /** * 多条件模糊搜索 */ @Query("SELECT c FROM Company c WHERE c.name LIKE %:keyword% OR c.address LIKE %:keyword%") List<Company> searchByKeyword(@Param("keyword") String keyword); /** * 批量更新地址 * 写操作必须加 @Modifying + @Transactional */ @Modifying @Transactional @Query("UPDATE Company c SET c.address = :address WHERE c.id = :id") int updateAddressById(@Param("id") Long id, @Param("address") String address); // ========== 4. 命名查询对应方法(对应163集) ========== // 方法名和 @NamedQuery 的 name 属性完全一致 List<Company> searchByNameKeyword(@Param("keyword") String keyword); }
2. JobRepository.java
java
运行
package com.example.jobportal.repository; import com.example.jobportal.entity.Job; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; 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.List; @Repository public interface JobRepository extends JpaRepository<Job, Long> { // 派生查询 List<Job> findByLocation(String location); Page<Job> findByCompanyId(Long companyId, Pageable pageable); // ========== JPQL 复杂条件查询 ========== /** * 薪资范围 + 地点 多条件查询,带排序 */ @Query("SELECT j FROM Job j " + "WHERE j.salary BETWEEN :min AND :max " + "AND (:location IS NULL OR j.location = :location) " + "ORDER BY j.salary DESC") List<Job> findJobsByCondition(@Param("min") Integer minSalary, @Param("max") Integer maxSalary, @Param("location") String location); // ========== 原生 SQL 查询(对应161集) ========== /** * 按城市统计岗位数量和平均薪资 * nativeQuery = true 表示执行真实SQL,不是JPQL */ @Query(value = "SELECT location, COUNT(*) AS job_count, AVG(salary) AS avg_salary " + "FROM jobs GROUP BY location", nativeQuery = true) List<Object[]> getLocationStatisticsNative(); // ========== 命名原生查询对应方法 ========== List<Object[]> statisticsByLocation(); }
五、测试接口 Controller
java
运行
package com.example.jobportal.controller; import com.example.jobportal.entity.Company; import com.example.jobportal.entity.Job; import com.example.jobportal.repository.CompanyRepository; import com.example.jobportal.repository.JobRepository; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/jpa-query") @RequiredArgsConstructor public class JpaQueryTestController { private final CompanyRepository companyRepository; private final JobRepository jobRepository; // 1. 测试N+1解决方案:查所有公司+岗位,只发1条SQL @GetMapping("/companies/all") public List<Company> getAllCompanies() { return companyRepository.findAll(); } // 2. JPQL 多条件搜索公司 @GetMapping("/companies/search") public List<Company> searchCompanies(@RequestParam String keyword) { return companyRepository.searchByKeyword(keyword); } // 3. 多条件查询岗位 @GetMapping("/jobs/filter") public List<Job> filterJobs(@RequestParam Integer min, @RequestParam Integer max, @RequestParam(required = false) String location) { return jobRepository.findJobsByCondition(min, max, location); } // 4. 原生SQL统计 @GetMapping("/jobs/statistics") public List<Object[]> getJobStatistics() { return jobRepository.getLocationStatisticsNative(); } // 5. 更新公司地址 @PutMapping("/companies/{id}/address") public String updateAddress(@PathVariable Long id, @RequestParam String address) { int rows = companyRepository.updateAddressById(id, address); return "更新成功,影响行数:" + rows; } }
六、课程知识点对应表
表格
| 集数 | 核心内容 | 对应代码 |
|---|---|---|
| 156 | 派生查询的局限性 | 前置说明 + Repository 派生查询对比 |
| 157 | N+1 问题原理与危害 | N+1 复现代码 + 原理说明 |
| 158 | 批量抓取解决 N+1 | @BatchSize、Fetch Join、@EntityGraph 三种方案 |
| 159、160 | @Query + JPQL 自定义查询 | Repository 中所有 @Query JPQL 写法 |
| 161 | 原生 SQL 查询实战 | nativeQuery = true 统计查询示例 |
| 162、163 | 命名查询 / 命名原生查询 | 实体类 @NamedQuery + Repository 对应方法 |
七、企业级最佳实践总结
- 简单单表查询优先用派生查询,语义清晰,不用写 SQL
- 关联查询必须杜绝 N+1,列表查询优先
@EntityGraph,单条复杂查询优先FETCH JOIN - 中等复杂业务查询用 JPQL,面向实体编程,数据库无关,移植性好
- 超复杂统计、多表关联、数据库特有函数用原生 SQL,不要硬套 JPQL
- 写操作必须加
@Modifying+@Transactional,且返回值只能是 int/void - 命名查询适合复用度极高的静态查询,普通场景直接在 Repository 写
@Query更直观,维护成本更低 - 参数绑定统一用
:参数名+@Param,禁止用位置参数?1,可读性差易出错

浙公网安备 33010602011771号