zoukankan      html  css  js  c++  java
  • Spring Boot (六): 为 JPA 插上翅膀的 QueryDSL

    在前面的文章中,我们介绍了 JPA 的基础使用方式,《Spring Boot (三): ORM 框架 JPA 与连接池 Hikari》,本篇文章,我们由入门至进阶的介绍一下为 JPA 插上翅膀的 QueryDSL。

    1. 引言

    不可否认的是 JPA 使用是非常方便的,极简化的配置,只需要使用注解,无需任何 xml 的配置文件,语义简单易懂,但是,以上的一切都建立在单表查询的前提下的,我们可以使用 JPA 默认提供的方法,简单加轻松的完成 CRUD 操作。

    但是如果涉及到多表动态查询, JPA 的功能就显得有些捉襟见肘了,虽然我们可以使用注解 @Query ,在这个注解中写 SQL 或者 HQL 都是在拼接字符串,并且拼接后的字符串可读性非常的差,当然 JPA 还为我们提供了 Specification 来做这件事情,从我个人使用体验上来讲,可读性虽然还不错,但是在初学者上手的时候, Predicate CriteriaBuilder 使用方式估计能劝退不少人,而且如果直接执行 SQL 连表查询,获得是一个 Object[] ,类型是什么?字段名是什么?这些都无法直观的获得,还需我们手动将 Object[] 映射到我们需要的 Model 类里面去,这种使用体验无疑是极其糟糕的。

    这一切都在 QueryDSL 出世以后终结了, QueryDSL 语法与 SQL 非常相似,代码可读性非常强,异常简介优美,,并且与 JPA 高度集成,无需多余的配置,从笔者个人使用体验上来讲是非常棒的。可以这么说,只要会写 SQL ,基本上只需要看一下示例代码完全可以达到入门的级别。

    2. QueryDSL 简介

    QueryDSL 是一个非常活跃的开源项目,目前在 Github 上的发布的 Release 版本已经多达 251 个版本,目前最新版是 4.2.1 ,并且由 Querydsl Google组 和 StackOverflow 两个团队提供支持。

    QueryDSL 是一个框架,可用于构造静态类型的类似SQL的查询。可以通过诸如 QueryDSL 之类的 API 构造查询,而不是将查询编写为内联字符串或将其外部化为XML文件。

    例如,与简单字符串相比,使用 API 的好处是

    • IDE中的代码完成

    • 几乎没有语法无效的查询

    • 可以安全地引用域类型和属性

    • 更好地重构域类型的更改

    3. QueryDSL 使用实战

    3.1 引入 Maven 依赖

    代码清单:spring-boot-jpa-querydsl/pom.xml


    <!--QueryDSL支持-->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <scope>provided</scope>
    </dependency>
    <!--QueryDSL支持-->
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
    </dependency>
    
    • 这里无需指定版本号,已在 spring-boot-dependencies 工程中定义。

    3.2 添加 Maven 插件

    添加这个插件是为了让程序自动生成 query type (查询实体,命名方式为:"Q"+对应实体名)。
    上文引入的依赖中 querydsl-apt 即是为此插件服务的。

    注:在使用过程中,如果遇到 query type 无法自动生成的情况,用maven更新一下项目即可解决(右键项目 -> Maven -> Update Folders)。

    代码清单:spring-boot-jpa-querydsl/pom.xml


    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>com.mysema.maven</groupId>
            <artifactId>apt-maven-plugin</artifactId>
            <version>1.1.3</version>
            <executions>
                <execution>
                    <goals>
                        <goal>process</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/generated-sources/java</outputDirectory>
                        <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
    

    3.3 更新和删除

    在 JPA 中已经为我们提供了非常简便的更新和删除的使用方式,我们完全没有必要使用 QueryDSL 的更新和删除,不过这里还是给出用法,供大家参考:

    代码清单:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java


    @Override
    public Long update(String id, String nickName) {
        QUserModel userModel = QUserModel.userModel;
        // 更新
        return queryFactory.update(userModel).set(userModel.nickName, nickName).where(userModel.id.eq(id)).execute();
    }
    
    @Override
    public Long delete(String id) {
        QUserModel userModel = QUserModel.userModel;
        // 删除
        return queryFactory.delete(userModel).where(userModel.id.eq(id)).execute();
    }
    

    3.2 查询

    QueryDSL 在查询这方面可以说玩的非常花了,比如一些有关 select()fetch() 常用的写法如下:

    代码清单:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java


    @Override
    public List<String> selectAllNameList() {
        QUserModel userModel = QUserModel.userModel;
        // 查询字段
        return queryFactory.select(userModel.nickName).from(userModel).fetch();
    }
    
    @Override
    public List<UserModel> selectAllUserModelList() {
        QUserModel userModel = QUserModel.userModel;
        // 查询实体
        return queryFactory.selectFrom(userModel).fetch();
    }
    
    @Override
    public List<UserDTO> selectAllUserDTOList() {
        QUserModel userModel = QUserModel.userModel;
        QLessonModel lessonModel = QLessonModel.lessonModel;
        // 连表查询实体并将结果封装至DTO
        return queryFactory
                .select(
                        Projections.bean(UserDTO.class, userModel.nickName, userModel.age, lessonModel.startDate, lessonModel.address, lessonModel.name)
                )
                .from(userModel)
                .leftJoin(lessonModel)
                .on(userModel.id.eq(lessonModel.userId))
                .fetch();
    }
    
    @Override
    public List<String> selectDistinctNameList() {
        QUserModel userModel = QUserModel.userModel;
        // 去重查询
        return queryFactory.selectDistinct(userModel.nickName).from(userModel).fetch();
    }
    
    @Override
    public UserModel selectFirstUser() {
        QUserModel userModel = QUserModel.userModel;
        // 查询首个实体
        return queryFactory.selectFrom(userModel).fetchFirst();
    }
    
    @Override
    public UserModel selectUser(String id) {
        QUserModel userModel = QUserModel.userModel;
        // 查询单个实体,如果结果有多个,会抛`NonUniqueResultException`。
        return queryFactory.selectFrom(userModel).fetchOne();
    }
    

    3.4 复杂查询操作

    上面列举了简单的查询,但实际我们会遇到相当复杂的操作,比如子查询,多条件查询,多表连查,使用示例如下:

    代码清单:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/LessonServiceImpl.java


    @Service
    public class LessonServiceImpl implements LessonService {
    
        @Autowired
        JPAQueryFactory queryFactory;
    
        @Override
        public List<LessonModel> findLessonList(String name, Date startDate, String address, String userId) throws ParseException {
            QLessonModel lessonModel = QLessonModel.lessonModel;
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            // 多条件查询示例
            return queryFactory.selectFrom(lessonModel)
                    .where(
                            lessonModel.name.like("%" + name + "%")
                            .and(lessonModel.address.contains(address))
                            .and(lessonModel.userId.eq(userId))
                            .and(lessonModel.startDate.between(simpleDateFormat.parse("2018-12-31 00:00:00"), new Date()))
                    )
                    .fetch();
        }
    
        @Override
        public List<LessonModel> findLessonDynaList(String name, Date startDate, String address, String userId) throws ParseException {
            QLessonModel lessonModel = QLessonModel.lessonModel;
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    
            // 动态查询示例
            BooleanBuilder builder = new BooleanBuilder();
    
            if (!StringUtils.isEmpty(name)){
                builder.and(lessonModel.name.like("%" + name + "%"));
            }
    
            if (startDate != null) {
                builder.and(lessonModel.startDate.between(simpleDateFormat.parse("2018-12-31 00:00:00"), new Date()));
            }
    
            if (!StringUtils.isEmpty(address)) {
                builder.and(lessonModel.address.contains(address));
            }
    
            if (!StringUtils.isEmpty(userId)) {
                builder.and(lessonModel.userId.eq(userId));
            }
    
            return queryFactory.selectFrom(lessonModel).where(builder).fetch();
        }
    
        @Override
        public List<LessonModel> findLessonSubqueryList(String name, String address) {
            QLessonModel lessonModel = QLessonModel.lessonModel;
            // 子查询示例,并无实际意义
            return queryFactory.selectFrom(lessonModel)
                    .where(lessonModel.name.in(
                            JPAExpressions
                                    .select(lessonModel.name)
                                    .from(lessonModel)
                                    .where(lessonModel.address.eq(address))
                    ))
                    .fetch();
        }
    }
    

    3.5 Mysql 聚合函数

    QueryDSL 已经内置了一些常用的 Mysql 的聚合函数,如果遇到 QueryDSL 没有提供的聚合函数也无需慌张, QueryDSL 为我们提供了 Expressions 这个类,我们可以使用这个类手动拼接一个就好,如下示例:

    代码清单:spring-boot-jpa-querydsl/src/main/java/com/springboot/springbootjpaquerydsl/service/impl/UserServiceImpl.java


    @Override
    public String mysqlFuncDemo(String id, String nickName, int age) {
    
        QUserModel userModel = QUserModel.userModel;
    
        // Mysql 聚合函数示例
    
        // 聚合函数-avg()
        Double averageAge = queryFactory.select(userModel.age.avg()).from(userModel).fetchOne();
    
        // 聚合函数-sum()
        Integer sumAge = queryFactory.select(userModel.age.sum()).from(userModel).fetchOne();
    
        // 聚合函数-concat()
        String concat = queryFactory.select(userModel.nickName.concat(nickName)).from(userModel).fetchOne();
    
        // 聚合函数-contains()
        Boolean contains = queryFactory.select(userModel.nickName.contains(nickName)).from(userModel).where(userModel.id.eq(id)).fetchOne();
    
        // 聚合函数-DATE_FORMAT()
        String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", userModel.createDate)).from(userModel).fetchOne();
    
        return null;
    }
    

    4. 小结

    有关 QueryDSL 的介绍到这里就结束了,不知道各位读者看了上面的示例,有没有一种直接读 SQL 的感觉,而且这种 SQL 还是使用 OOM 的思想,将原本 Hibernate 没有做好的事情给出了一个相当完美的解决方案,上手简单易操作,而又无需写 SQL ,实际上我们操作的还是对象类。

    5. 示例代码

    示例代码-Github

    示例代码-Gitee

    6. 参考

    《QueryDSL 官方文档》

  • 相关阅读:
    [原创]PostgreSQL Plus Advanced Server批量创建分区表写入亿级别数据实例
    [原创]从Oracle和Microsoft Sql Server迁移到PostgreSQL Plus Advanced Server
    [原创]PostgreSQL Plus Advince Server在 HA环境中一对多的Stream Replication配置(四)
    【译】x86程序员手册41-10.6 TLB(快表)测试
    【译】x86程序员手册40-10.5初始化的例子
    【译】x86程序员手册39-10.3切换到保护模式
    【译】x86程序员手册38-10.2实在址模式下的软件初始化
    【译】x86程序员手册37-第10章 初始化
    【译】x86程序员手册36-9.9异常汇总
    【译】x86程序员手册35-9.8异常条件
  • 原文地址:https://www.cnblogs.com/babycomeon/p/11605809.html
Copyright © 2011-2022 走看看