返回

Spring JPA复杂查询:子查询JOIN的转换与实现

mysql

Spring JPA 中复杂 JOIN 查询的实现:子查询 JOIN 的转换

开发中经常遇到复杂查询,比如需要在 JOIN 子句中使用 SELECT 查询。咱来看看怎么把这种 SQL 语句转成 Spring JPA 的写法。

问题:嵌套 SELECT 的 JOIN 语句

我这儿有个原生 SQL 查询,想用 Spring JPA 来实现,不知道这种带 SELECT 子查询的 JOIN 行不行?

select distinct * 
from tablem m 
join (select distinct * from tablemp where
FST_NM='ABC'
and DOB='1990-01-01') AS mp
on m.ID=mp.ID
join tablep p on p.ID=mp.ID
and p.LIST in ('PTG'));

原因分析:JPA 标准与原生 SQL

JPA (Java Persistence API) 是个标准,它提供了对象关系映射 (ORM) 的方式,用起来比直接写 SQL 方便。但 JPA 为了保持通用性,有些复杂查询,尤其是涉及到特定数据库功能的,就不一定直接支持了。 上面那个 SQL 查询的难点在于 JOIN 后面跟了个 SELECT 子查询。标准 JPA 里,JOIN 后面通常直接跟实体或者关联的实体。

解决办法

对付这种查询,咱有几种招:

1. 使用 @Subselect (Hibernate 专属)

如果你的 JPA 实现是 Hibernate,那可以试试 @Subselect 这个注解。它允许你定义一个子查询,然后映射成一个实体。

原理: Hibernate 会把 @Subselect 里的 SQL 当成一个视图。

步骤:

  1. 创建 TableMPView 实体:
import javax.persistence.*;
import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;

@Entity
@Subselect(
    "select distinct * from tablemp where FST_NM='ABC' and DOB='1990-01-01'"
)
@Synchronize({"tablemp"}) // 声明这个视图依赖的表
public class TableMPView {

    @Id
    @Column(name = "ID") //假设ID是主键
    private Long id;

    // 其他字段...

     @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TableMPView that = (TableMPView) o;

        return id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }

    // getter/setter...
}
  1. TableM 实体:
import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "tablem")
public class TableM {

    @Id
    @Column(name = "ID")
    private Long id;
      // 其他字段...

    @ManyToMany //或者OneToMany, 看你实际关系
    @JoinTable(
        name = "tablem_tablempview", // 随便起个名字, 反正数据库里不会真创建这张表
        joinColumns = @JoinColumn(name = "tablem_id"),
        inverseJoinColumns = @JoinColumn(name = "tablempview_id")
    )
    private Set<TableMPView> tableMPViews;

        @ManyToMany //或者OneToMany, 看你实际关系
    @JoinTable(
        name = "tablem_tablep", // 随便起个名字
        joinColumns = @JoinColumn(name = "tablem_id"),
        inverseJoinColumns = @JoinColumn(name = "tablep_id")
    )
     private Set<TableP> tablePS;

    // 其他字段, getter/setter...

        @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TableM tableM = (TableM) o;

        return id.equals(tableM.id);
    }

    @Override
    public int hashCode() {
        return id.hashCode();
    }
}
  1. TableP 实体:

    import javax.persistence.*;
     import java.util.Set;
    
     @Entity
     @Table(name = "tablep")
    public class TableP {
    
         @Id
        private Long id;
    
        @ElementCollection
        @CollectionTable(name = "tablep_list", joinColumns = @JoinColumn(name = "tablep_id")) // 假设LIST字段存多个值
        @Column(name = "list_value") // 根据实际列名调整
        private Set<String> list;
    
         @ManyToMany(mappedBy = "tablePS") // 注意这里的 "tableMs"
         private Set<TableM> tableMs;
    
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
             if (o == null || getClass() != o.getClass()) return false;
    
             TableP tableP = (TableP) o;
    
             return id.equals(tableP.id);
         }
    
         @Override
         public int hashCode() {
             return id.hashCode();
         }
    
         // getter 和 setter 方法...
    }
    
  2. 查询:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface TableMRepository extends JpaRepository<TableM, Long> {
      @Query("SELECT DISTINCT m FROM TableM m " +
             "JOIN m.tableMPViews mpv " +
             "JOIN m.tablePS p " +
             "WHERE p.list IN (:listValues)")
      List<TableM> findByCustomJoin(List<String> listValues);
}

//使用:
List<String> listToSearch = List.of("PTG");
List<TableM> results = tableMRepository.findByCustomJoin(listToSearch);

安全提示: 保证 @Subselect 里的 SQL 语句安全,防止注入。

2. 使用 JPQL + Native Query

可以把部分查询写成 JPQL,子查询部分用 Native Query。

原理: JPQL 处理实体间关系,Native Query 搞定子查询,最后拼起来。

步骤:

  1. 先用 Native Query 查出子查询结果 (ID 列表):
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface TableMPRepository extends JpaRepository<TableMP, Long> {

    @Query(value = "SELECT distinct ID FROM tablemp WHERE FST_NM = :fstNm AND DOB = :dob", nativeQuery = true)
    List<Long> findIdsByFstNmAndDob(String fstNm, String dob);
}
  1. 再用 JPQL 写主查询:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface TableMRepository extends JpaRepository<TableM, Long> {

    @Query("SELECT DISTINCT m FROM TableM m " +
           "JOIN TableMP mp ON m.id = mp.id " +
           "JOIN TableP p ON mp.id = p.id " +
           "WHERE mp.id IN :ids AND p.list IN :listValues")
    List<TableM> findBySubqueryIdsAndList(List<Long> ids, List<String> listValues);
}

    //使用方法:
    //  List<Long> subqueryIds = tableMPRepository.findIdsByFstNmAndDob("ABC", "1990-01-01");
    //        List<String> listToSearch = List.of("PTG");
    //  List<TableM> result = tableMRepository.findBySubqueryIdsAndList(subqueryIds, listToSearch);

注意: 上面的实体关系需要根据实际表结构调整。

3. Native SQL (最直接,但不推荐)

如果实在搞不定,直接上 Native SQL 吧。

原理: 直接写 SQL,啥数据库都能跑。但是就失去了 JPA 的可移植性。

步骤:

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface TableMRepository extends JpaRepository<TableM, Long> {

    @Query(value = "select distinct * " +
                   "from tablem m " +
                   "join (select distinct * from tablemp where FST_NM = :fstNm AND DOB = :dob) AS mp " +
                   "on m.ID = mp.ID " +
                   "join tablep p on p.ID = mp.ID " +
                   "and p.LIST in (:listValues)", nativeQuery = true)
    List<TableM> findByNativeQuery(String fstNm, String dob, List<String> listValues);
}

进阶技巧 : 可以使用SqlResultSetMapping将查询结果直接映射到非Entity类。

@SqlResultSetMapping(name="customMapping", classes = {
        @ConstructorResult(targetClass = CustomResult.class,
                columns = {@ColumnResult(name = "id"), @ColumnResult(name = "name")})
})

public class CustomResult {
  //... 构造函数和属性.
}

安全提示: Native SQL 一定要注意防止 SQL 注入! 参数要用占位符。

4. Criteria API (繁琐, 但类型安全)

JPA 的 Criteria API 可以动态构建查询,类型安全,但写起来很繁琐。

原理: 用 Java 代码查询条件。

这里我不展开写代码了,太长了。简单说下思路:

  1. 创建 CriteriaBuilder
  2. 创建 CriteriaQuery
  3. 创建 RootJoin 等对象,表示查询的实体和关联。
  4. Predicate 构建 WHERE 条件。
  5. 对于子查询,可以创建 Subquery 对象,然后在主查询里用 in 表达式。

通常不直接使用此方法处理。

总结

具体选哪个方法,看你项目情况:

  • Hibernate 项目,@Subselect 方便。
  • 想兼顾 JPA 标准和灵活性,JPQL + Native Query 组合拳。
  • Native SQL 最直接,但要小心可移植性和安全。
  • Criteria API 太啰嗦了,除非对类型安全有极致要求,通常不考虑。

建议优先考虑 @Subselect或者 JPQL+ NativeQuery.