51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

JPA 分页及 LambdaQuery 查询方法实现

Preface {#preface}

前一阵子使用了一段时间的 Mybatis Plus 操作数据库查询数据,觉得 LambdaQuery() 方法甚是好用,由此产生了在 Hibernate 中实现此功能;况且老早之前就想着在 Hibernate 中实现分页查询的功能,索性这次两个功能一起完成。

Hibernate 和 JPA(Java Persistence API)之间存在密切的关系,可以说它们是关系数据库持久化领域的两个相关的技术。

  1. Hibernate: Hibernate 是一个开源的对象关系映射(ORM)框架,用于将Java中的对象模型映射到关系数据库中。它简化了数据库访问和操作,使开发人员可以通过面向对象的方式来处理数据库交互,而不必直接使用 SQL。
  2. 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[]> 类型,分三种情况:

  1. 查询全部字段时:
String jpql = "select a from AuthUser a";

List<Object[]> 具体形式为:Object[] 数组包含一个元素,元素类型为实体类型,即 AuthUser 实体类。

image-20231116162324849

  1. 查询多个字段时:
String jpql = "select a.id, a.name, a.age from AuthUser a";

List<Object[]> 具体形式为:Object[] 数组包含三个元素,元素的值分别对应查询字段 id、name、age 的值。

image-20231116162549482

  1. 查询单个字段时:
String jpql = "select a.name from AuthUser a";

List<Object[]> 具体形式为:Object[] 数组包含一个元素,元素值即为查询字段 name 对应的值。

image-20231116162907318

拿到查询结果之后,再通过 Java 反射相关的功能将值设置到目标VO 类中去即可,具体方法不再详述。

详细功能 {#详细功能}

TODO:等能稳定运行之后再更。

参考资源 {#参考资源}

赞(3)
未经允许不得转载:工具盒子 » JPA 分页及 LambdaQuery 查询方法实现