这篇文章主要解决以下问题:
- 在不能使用
@OneToMany
的情况下,如何使用关联查询? - 如何在一个 Spring Data Jpa 项目中使用 HQL?
- idea 支持 HQL 高亮、提示?
- 查询参数不定?
- 如果将关联结果集映射到 DTO 当中?
- 分页以及排序
- 在 HQL 不能使用子查询的情况下,并且限制了结果集,如何获取总数?
前言
事实上,如果在实体类上使用了 @OneToMany
、@ManyToOne
、@OneToOne
的话,就不需要看这篇文章了;可以详细参照 Hibernate 文档中的 Criteria 。
需要明确的是,HQL 是不支持类型安全的,这在官方文档中也给予标明(我一直在纠结类型是否安全,仔细想一下,官方建议使用 Criteria 的,但是公司又不让用 @OneToMany
,因此就相当于给 Hibernate 戴上了脚铐来跳舞):
Both HQL and JPQL are non-type-safe ways to perform query operations. Criteria queries offer a type-safe approach to querying. See Criteria for more information.
// Phone.person is a @ManyToOne
Join personJoin = root.join( Phone_.person );
使用 join()
方法的话,意味着必须要为关联字段加上 @OneToMany
属性。
如何在 Spring Data Jpa 项目中使用 HQL?
其实,Spring Data 底层是 Hibernate,所以,只要把 EntityManager
注入进 service/dao 就可以:
@Service
public class RecordApplyService{
@PersistenceContext
private EntityManager entityManager;
}
EntityManager
提供了 createQuery(String ...)
帮助我们可以使用 HQL 语言查询。
如何使用 HQL ,以及如何做关联查询?
其实 HQL 使用起来非常简单,与 SQL 是类似的,只不过 SQL 是针对数据库的,而 HQL 是针对对象以及字段的。而且 HQL 针对字段的那一部分是大小写敏感的。基本上,SQL 支持的 HQL 都支持。这篇文章的重点不是如何使用 HQL,因此只是简略介绍。
talk is cheap,show me the code
例如,有这样关于备案的实体类,里面记录了与备案相关信息;另外有一张公司表,备案表与公司表关系是一对多的关系。
@Entity
@Table(name = "....")
@Data
public class RecordApplyMessage implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "RECORD_ID", nullable = false)
private String recordId;
// 其它字段省略
}
@Entity
@Table(name = "....")
@Data
public class CompanyInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "COMPANY_ID", nullable = false)
private String companyId;
// 备案与公司的关系是一对多,一个备案可能有多个公司信息
@Column(name = "RECORD_ID")
private String recordId;
@Column(name = "ENTERPRISE_NAME")
private String enterpriseName;
}
那么,HQL 里面可以这样编写:
select r.recordId as recordId,
c.enterpriseName as enterpriseName,
from RecordApplyMessage r
inner join CompanyInfo c on r.recordId = c.recordId
查询参数不定长如何解决?
如果使用 MyBatis 的话,那么可以在 xml 文档中使用 if
标签判断,其实,使用 HQL 的话,与 MyBatis 的将查询结果集映射为 DTO?思想是类似的,也是拼装 SQL:(没必要为每个字段使用 as,这里 as 的目的是为了后面映射到 DTO)。
注意 RecordApplyMessageSql
类是专门用于维护 SQL 的工具类(公司要求)。只不过 getRecordApplyMessageListHql()
返回的 HQL。RecordApplyListPvo
表示入参 VO。
public class RecordApplyMessageSql{
public static String getRecordApplyMessageListHql(RecordApplyListPvo messagePvo) {
String sql =
"select r.recordId as recordId, " +
"c.enterpriseName as enterpriseName, " +
"from RecordApplyMessage r " +
"inner join CompanyInfo c on r.recordId = c.recordId ";
// 公司名称
if (StringUtils.isNotEmpty(messagePvo.getEnterpriseName())) {
sql += " and c.enterpriseName like :enterpriseName ";
}
if (StringUtils.isNotEmpty(messagePvo.getSocialCreditCode())) {
sql += " and (c.socialCreditCode = :socialCreditCode) ";
}
sql += " where 1=1 ";
if (StringUtils.isNotEmpty(messagePvo.getApplyRecordType())) {
sql += " and (r.applyRecordType = :applyRecordType)";
}
if (messagePvo.getKeepRecordAmountMoney() != null) {
sql += " and (r.keepRecordAmountMoney = :keepRecordAmountMoney)";
}
return sql;
}
}
在这个 HQL 我使用了类似于占位符的东西,例如 :enterpriseName
注意像这样的工具类,idea 默认是没有办法认为它是一段 SQL/HQL 语句的。
可以在 idea 上加上注释说明这是一段 HQL 语句:
// language=HQL
String sql = "......";
请注意需要开启下面的插件,默认是开启的:
回到刚才的问题,现在 HQL 已经拼装好了,那么怎么设置占位符呢?
在 Service 层获取到 getRecordApplyMessageListHql
生成的 HQL 之后,可以使用 setParameter()
的方式设置占位符,例如:
// 第一个参数是占位符名称,在这里是 :enterpriseName
// 第二个参数是占位符的实际值,一般是前台传入的值,在这里为方便展示定死
entityManager
.createQuery(hql)
.setParameter("enterpriseName","xxxx公司").setParamter("socialCreditCode","xxxx")
.getResultList();
但是,问题是,我的占位符是不定长的,也就是说,不确定有多少个 setParamter(...)
,怎么办?这个问题其实很好解决,就是使用 Map
记录不定参数的数目以及内容。可以改写刚才生成的方法,让其返回一个自定义类。
public class RecordApplyMessageSql{
public static HqlUtils getRecordApplyMessageListHql(RecordApplyListPvo messagePvo) {
Map paramMap = new HashMap<>();
String sql =
"select r.recordId as recordId, " +
"c.enterpriseName as enterpriseName, " +
"from RecordApplyMessage r " +
"inner join CompanyInfo c on r.recordId = c.recordId ";
// 公司名称
if (StringUtils.isNotEmpty(messagePvo.getEnterpriseName())) {
sql += " and c.enterpriseName like :enterpriseName ";
paramMap.put("enterpriseName", messagePvo.getEnterpriseName());
}
if (StringUtils.isNotEmpty(messagePvo.getSocialCreditCode())) {
sql += " and (c.socialCreditCode = :socialCreditCode) ";
paramMap.put("socialCreditCode", messagePvo.getSocialCreditCode());
}
sql += " where 1=1 ";
if (StringUtils.isNotEmpty(messagePvo.getApplyRecordType())) {
sql += " and (r.applyRecordType = :applyRecordType)";
paramMap.put("applyRecordType", messagePvo.getApplyRecordType());
}
if (messagePvo.getKeepRecordAmountMoney() != null) {
sql += " and (r.keepRecordAmountMoney = :keepRecordAmountMoney)";
paramMap.put("keepRecordAmountMoney", messagePvo.getKeepRecordAmountMoney());
}
return new HqlUtils(sql, "r", paramMap);
}
}
封装的 HqlUtils
类中有三个字段,其中一个就是 HQL 字符串,另一个是参数 Map。
primaryTable 是用于做统计用的,目前不好理解的话,你可以理解为这里面存储的是 HQL 中 from
后面的东西。
例如,你写的是 from RecordApplyMessage r where ...
,那么 primaryTable 就是 r
。
@Getter
@AllArgsConstructor
public class HqlUtils {
private final String hql;
private final String primaryTable;
private final Map paramMap;
}
这样封装的话,service
层就可以这样写:
HqlUtils messageSql = RecordApplyMessageSql.getRecordApplyMessageListHql(messagePvo);
Query resultQuery = entityManager.createQuery(messageSql.getHql());
hqlUtils.getParamMap().forEach(resultQuery::setParameter);
如何将结果映射到 DTO 中?
有三种方法:
- 使用构造器的方式
- 起别名的方式
- 使用命名查询的方式
这篇文章主要介绍前两种方式。
- 使用构造器方式
public class PostDTO {
private Long id;
private String title;
public PostDTO(Number id, String title) {
this.id = id.longValue();
this.title = title;
}
}
List postDTOs = entityManager.createQuery("
select new dto.PostDTO(p.id, p.title)
from Post p
where p.createdOn > :fromTimestamp",
PostDTO.class).setParameter(
"fromTimestamp",
Timestamp.from(LocalDate.of(2020, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC)
)
)
.getResultList();
缺点就是在 DTO 当中必须要有一个对应的构造器。
- 起别名的方式
public class PostDTO {
private Long id;
private String title;
public Long getId() {
return id;
}
public void setId(Number id) {
this.id = id.longValue();
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
List postDTOs = entityManager.createQuery("
select
p.id as id,
p.title as title
from Post p
where p.createdOn > :fromTimestamp
")
.setParameter(
"fromTimestamp",
Timestamp.from(
LocalDateTime.of(2020, 1, 1, 0, 0, 0)
.toInstant(ZoneOffset.UTC)
)
)
.unwrap(org.hibernate.query.Query.class)
.setResultTransformer(Transformers.aliasToBean(PostDTO.class))
.getResultList();
这里需要注意的是,需要为每个字段起别名。另外需要注意的是:
public void setId(Number id) {
this.id = id.longValue();
}
这样做的目的是,数据库可能会返回一个 BigInteger
类型的,而 DTO
需要的是 Long
,如果不这样写的话,可能会发生 Hibernate 反射错误的情况。不过大多数情况下,dto
中的类和数据库中的类型是一致的。(特例就是方言可能会把 DATETIME
解析为 TIMESTAMP
,而您又想用 LocalDateTime
来处理)。
分页和排序
在 HQL 语句中是不能写 limit
这样的分页语句的。我们可以借由 setFirstResult
和 setMaxResults
来实现。
Query resultQuery = entityManager.createQuery(orderHql)
.setFirstResult(pageable.getPageNumber() * pageable.getPageSize())
.setMaxResults(pageable.getPageSize())
.unwrap(org.hibernate.query.Query.class);
其中 Pageable
是 Spring Data 提供的。至于前端的分页参数如何封装到 Pageable
当中,在这片文中不作为重点,就忽略了。
排序比较复杂,我们需要使用拼接的做法,将 HQL 与排序内容拼接起来。
if (!sort.isEmpty()) {
result.append(" order by ");
StringBuilder orderBuffer = new StringBuilder();
sort.stream().parallel()
.forEach(
order -> orderBuffer.append(order.getProperty())
.append(" ")
.append(order.getDirection())
.append(" ,")
);
result.append(orderBuffer.substring(0, orderBuffer.length() -1 ));
}
其中,Sort
还是 Spring Data 提供的,可以通过 pageable.getSort()
获取。我的建议是封装一个 Dao 层,将其放到查询总数的后面。因为,查询总数并不需要排序。
限制结果集的情况下,获取总数?
前端在计算一共有多少页的情况下,需要返回数据库中一共有多少符合条件的数量。我首先想到的是使用如下 HQL 语句:
select count(*) from ( select ... from ... )
但是,hql 并不支持这样的子查询,所有,在前面封装到 HqlUtil
里面的 primaryTable
就起到了作用:
public class HqlUtils {
private final String hql;
private final String primaryTable;
private final Map paramMap;
public String getCountSql() {
return "select count(" + primaryTable + ") " + hql.substring(hql.toLowerCase().indexOf("from "));
}
}
这样,就可以获取总数量了:
public Long queryForCount(HqlUtils hqlUtils) {
TypedQuery totalQuery = entityManager.createQuery(hqlUtils.getCountSql(), Long.class);
hqlUtils.getParamMap().forEach(totalQuery::setParameter);
return totalQuery.getSingleResult();
}
参考
- The best way to map a projection query to a DTO (Data Transfer Object) with JPA and Hibernate