Preface {#preface}
前一阵子使用了一段时间的 Mybatis Plus 操作数据库查询数据,觉得 LambdaQuery()
方法甚是好用,由此产生了在 Hibernate 中实现此功能;况且老早之前就想着在 Hibernate 中实现分页查询的功能,索性这次两个功能一起完成。
Hibernate 和 JPA(Java Persistence API)之间存在密切的关系,可以说它们是关系数据库持久化领域的两个相关的技术。
- Hibernate: Hibernate 是一个开源的对象关系映射(ORM)框架,用于将Java中的对象模型映射到关系数据库中。它简化了数据库访问和操作,使开发人员可以通过面向对象的方式来处理数据库交互,而不必直接使用 SQL。
- JPA: Java Persistence API 是 Java EE(Enterprise Edition)规范的一部分,定义了一组标准的接口和注解,用于简化 Java 应用程序与关系数据库的交互。JPA 的目标是提供一种标准化的方式,使开发人员可以使用统一的 API 进行对象持久化,而不受特定 ORM 框架的限制。JPA 提供了一种规范,允许开发人员在不同的 ORM 框架之间进行切换而不必改变代码。
关系:
- Hibernate 实现了 JPA: Hibernate 是 JPA 的一种实现。具体来说,Hibernate 提供了对 JPA 标准的实现,这意味着你可以使用 Hibernate 作为 JPA 的提供者,从而利用 JPA 的标准 API 来实现对象的持久化。
- JPA 是规范,Hibernate 是实现: JPA 是一种规范,定义了一套标准的接口和行为,而 Hibernate 是实现了这个规范的 ORM 框架。因此,如果你使用 JPA,你可以选择使用任何实现了 JPA 规范的 ORM 框架,而不仅限于 Hibernate。不过,Hibernate 是最为常用和流行的 JPA 实现之一。
总的来说,Hibernate 和 JPA 是关系数据库持久化领域中的两个相关概念,Hibernate 是 JPA 的一种实现,而 JPA 提供了一种标准的接口规范,使得开发人员可以在不同的 ORM 框架之间切换而不用修改大量的代码。
运行环境:
- Java 17
- SpringBoot 3.1.5
- Hibernate 6.2.13.Final
简单演示 {#简单演示}
分页查询演示
@Test
void testPage() {
// 模拟前端发起的请求参数
PageForm pageForm = new PageForm();
// 当前页
pageForm.put("page", 1);
// 页大小
pageForm.put("size", 3);
// 查询参数
pageForm.put("sex", "男");
String jpql = """
select a.id, a.name, a.age, a.sex, a.phone
from AuthUser a
where
(:sex is null or a.sex=:sex)
and (:phone is null or a.phone like concat('%', :phone, '%'))
""";
PageVO<AuthUserVO> pageVO = authUserDao.getPage(jpql, pageForm, AuthUserVO.class);
System.out.println("page = " + pageVO);
/* 输出:
page = Page{total=3, rows=[AuthUserVO(id=1, name=张三, age=13, sex=男, phone=13333333333), AuthUserVO(id=4, name=赵六, age=16, sex=男, phone=16666666666), AuthUserVO(id=5, name=钱七, age=17, sex=男, phone=17777777777)]}
*/
}
LambdaQuery() 查询演示
@Test
void testLambdaQuery() {
List<AuthUserVO> authUserList = authUserDao.lambdaQuery()
.eq(AuthUser::getSex, "男")
.searchAll(AuthUserVO.class);
System.out.println("authUserList = " + authUserList);
/* 输出:
authUserList = [AuthUserVO(id=1, name=张三, age=13, sex=男, phone=13333333333), AuthUserVO(id=4, name=赵六, age=16, sex=男, phone=16666666666), AuthUserVO(id=5, name=钱七, age=17, sex=男, phone=17777777777)]
*/
}
@Test
void testLambdaQueryPage() {
PageVO<AuthUserVO> pageVO = authUserDao.lambdaQuery()
.eq(AuthUser::getSex, "男")
.getPage(1, 3, AuthUserVO.class);
System.out.println("pageVO = " + pageVO);
/* 输出:
pageVO = Page{total=3, rows=[AuthUserVO(id=1, name=张三, age=13, sex=男, phone=13333333333), AuthUserVO(id=4, name=赵六, age=16, sex=男, phone=16666666666), AuthUserVO(id=5, name=钱七, age=17, sex=男, phone=17777777777)]}
*/
}
核心原理 {#核心原理}
实现这两个功能的基本方法是拼接 JPQL 语句然后使用 EntityManager.createQuery()
来执行拼接好的 JPQL 语句。而这一基本方法又围绕着两个核心原理展开,一是如何拼接 JPQL 语句,二是如何将查询结果设置到目标类型中。
原理一:如何获取查询字段名,比如从 .eq(AuthUser::getSex, "男")
获取查询的字段 sex
。
这用到了 Java 中序列化和反序列化的知识。
对象序列化中的 writeReplace 和 readResolve:
writeReplace:在将对象序列化之前,如果对象的类或父类中存在writeReplace方法,则使用writeReplace的返回值作为真实被序列化的对象;writeReplace 在 writeObject 之前执行;readResolve:在将对象反序列化之后,ObjectInputStream.readObject 返回之前,如果从对象流中反序列化得到的对象所属类或父类中存在 readResolve方法,则使用 readResolve 的返回值作为 ObjectInputStream.readObject 的返回值;readResolve 在 readObject 之后执行;
函数式接口如果继承了 Serializable,使用 Lambda 表达式来传递函数式接口时,编译器会为 Lambda 表达式生成一个 writeReplace 方法,这个生成的 writeReplace 方法会返回 java.lang.invoke.SerializedLambda;可以从反射 Lambda 表达式的 Class 证明 writeReplace 的存在;所以在序列化 Lambda 表达式时,实际上写入对象流中的是一个 SerializedLambda 对象,且这个对象包含了 Lambda 表达式的一些描述信息。
从上述引用的这段话可知我们首先需要一个继承 Serializable 的函数式接口,如下:
/**
* 支持序列化的函数式接口
*
* @author ldwcool
*/
@FunctionalInterface
public interface SFunction<E, R> extends Function<E, R>, Serializable {
}
有了这个 SFunction
函数式接口之后,我们将他作为方法的参数接收 Lambda 表达式,可从实际写入对象流中的 SerializedLambda 对象提取出想要的信息。
import cn.hutool.json.JSONUtil;
import cool.ldw.mwapi.framework.core.db.querywrapper.SFunction;
import cool.ldw.mwapi.test.model.entity.AuthUser;
import lombok.Data;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
@Data
public class LambdaTest {
public static void main(String[] args) throws Exception {
SerializedLambda serializedLambda = doSFunction(AuthUser::getSex);
System.out.println("方法名:" + serializedLambda.getImplMethodName());
System.out.println("类名:" + serializedLambda.getImplClass());
System.out.println("serializedLambda:" + JSONUtil.toJsonStr(serializedLambda));
/* 输出:
方法名:getSex
类名:cool/ldw/mwapi/test/model/entity/AuthUser
serializedLambda:{"capturingClass":"cool/ldw/mwapi/LambdaTest","functionalInterfaceClass":"cool/ldw/mwapi/framework/core/db/querywrapper/SFunction","functionalInterfaceMethodName":"apply","functionalInterfaceMethodSignature":"(Ljava/lang/Object;)Ljava/lang/Object;","implClass":"cool/ldw/mwapi/test/model/entity/AuthUser","implMethodName":"getSex","implMethodSignature":"()Ljava/lang/String;","implMethodKind":5,"instantiatedMethodType":"(Lcool/ldw/mwapi/test/model/entity/AuthUser;)Ljava/lang/String;"}
*/
}
private static <T, R> java.lang.invoke.SerializedLambda doSFunction(SFunction<T, R> func) throws Exception {
// 直接调用 writeReplace
Method writeReplace = func.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
//反射调用
Object sl = writeReplace.invoke(func);
java.lang.invoke.SerializedLambda serializedLambda = (java.lang.invoke.SerializedLambda) sl;
return serializedLambda;
}
}
从 serializedLambda.getImplMethodName()
和 serializedLambda.getImplClass()
可以分别获取方法名和类名,再通过一些字符串处理方法就可以得到查询的字段 sex
。
/**
* 反射调用 writeReplace 方法,获取序列化信息,其中包含调用的方法名,类名等信息
*
* @param func lambda 表达式
* @param <T> -
* @param <Y> -
* @return 序列化信息
*/
public static <T, Y> SerializedLambda resolveLambda(SFunction<T, Y> func) {
try {
// 直接调用 writeReplace
Method writeReplace = func.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
// 反射调用
return (SerializedLambda) writeReplace.invoke(func);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 从 get 方法名中提取字段名
*
* @param func lambda 表达式
* @param <E> -
* @param <Y> -
* @return 字段名
*/
public static <E, Y> String resolveFieldName(SFunction<E, Y> func) {
// 解析 lambda 方法
SerializedLambda serializedLambda = JpqlUtil.resolveLambda(func);
// 获取 get 方法名称
String methodName = serializedLambda.getImplMethodName();
methodName = methodName.replace("get", "");
return (methodName.charAt(0) + "").toLowerCase() + methodName.substring(1);
}
/**
* 从 get 方法名中提取全限定类名
*
* @param func lambda 表达式
* @param <E> -
* @param <Y> -
* @return 字段名
*/
public static <E, Y> String resolveFullyQualifiedClassName(SFunction<E, Y> func) {
// 解析 lambda 方法
SerializedLambda serializedLambda = JpqlUtil.resolveLambda(func);
// 获取全类名
return serializedLambda.getImplClass().replaceAll("/", ".");
}
原理二:查询出来的结果如何设置到目标类型中去,比如 VO 类。
本来 Hibernate 中 entityManager.createQuery()
是可以将查询结果封装到指定类型中的,如:
String jpql = "select new cool.ldw.mwapi.test.model.vo.AuthUserVO(a.id, a.name, a.age, a.sex, a.phone) from AuthUser a";
TypedQuery<AuthUserVO> query = entityManager.createQuery(jpql, AuthUserVO.class);
但这种方法有一个不好地方,原因在于它是通过有参构造方法来新建对象的,也就是说在 AuthUserVO 这个类中必须包含 (a.id, a.name, a.age, a.sex, a.phone) 这 5 个参数的构造方法,需要我们在目标类中手动创造出这个构造方法,而这就增加了我们的工作量。
那么有没有不使用构造方法也能将查询结果封装到 AuthUserVO 类中呢?答案是有的,而这也正是该原理所做的事情。
具体方法是,使用 entityManager.createQuery()
时将结果类型设置为 Object[].class
。
TypedQuery<Object[]> query = this.entityManager.createQuery(jpql, Object[].class);
这样查询出来的结果会是 List<Object[]>
类型,分三种情况:
- 查询全部字段时:
String jpql = "select a from AuthUser a";
List<Object[]>
具体形式为:Object[]
数组包含一个元素,元素类型为实体类型,即 AuthUser 实体类。
- 查询多个字段时:
String jpql = "select a.id, a.name, a.age from AuthUser a";
List<Object[]>
具体形式为:Object[]
数组包含三个元素,元素的值分别对应查询字段 id、name、age 的值。
- 查询单个字段时:
String jpql = "select a.name from AuthUser a";
List<Object[]>
具体形式为:Object[]
数组包含一个元素,元素值即为查询字段 name 对应的值。
拿到查询结果之后,再通过 Java 反射相关的功能将值设置到目标VO 类中去即可,具体方法不再详述。
详细功能 {#详细功能}
TODO:等能稳定运行之后再更。