Spring JPA复杂查询:子查询JOIN的转换与实现
2025-03-15 20:26:27
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 当成一个视图。
步骤:
- 创建
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...
}
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();
}
}
-
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 方法... }
-
查询:
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 搞定子查询,最后拼起来。
步骤:
- 先用 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);
}
- 再用 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 代码查询条件。
这里我不展开写代码了,太长了。简单说下思路:
- 创建
CriteriaBuilder
。 - 创建
CriteriaQuery
。 - 创建
Root
、Join
等对象,表示查询的实体和关联。 - 用
Predicate
构建 WHERE 条件。 - 对于子查询,可以创建
Subquery
对象,然后在主查询里用in
表达式。
通常不直接使用此方法处理。
总结
具体选哪个方法,看你项目情况:
- Hibernate 项目,
@Subselect
方便。 - 想兼顾 JPA 标准和灵活性,JPQL + Native Query 组合拳。
- Native SQL 最直接,但要小心可移植性和安全。
- Criteria API 太啰嗦了,除非对类型安全有极致要求,通常不考虑。
建议优先考虑 @Subselect
或者 JPQL+ NativeQuery.