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 对应方法

七、企业级最佳实践总结

  1. 简单单表查询优先用派生查询,语义清晰,不用写 SQL
  2. 关联查询必须杜绝 N+1,列表查询优先 @EntityGraph,单条复杂查询优先 FETCH JOIN
  3. 中等复杂业务查询用 JPQL,面向实体编程,数据库无关,移植性好
  4. 超复杂统计、多表关联、数据库特有函数用原生 SQL,不要硬套 JPQL
  5. 写操作必须加 @Modifying + @Transactional,且返回值只能是 int/void
  6. 命名查询适合复用度极高的静态查询,普通场景直接在 Repository 写 @Query 更直观,维护成本更低
  7. 参数绑定统一用 :参数名 + @Param,禁止用位置参数 ?1,可读性差易出错

 

 

 

posted @ 2026-08-01 01:52  漫漫长路</>  阅读(5)  评论(0)    收藏  举报