V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lufficc
V2EX  ›  程序员

Spring Boot JPA - 使用 Querydsl 处理复杂的操作

  •  1
     
  •   lufficc ·
    lufficc · 2017-03-27 21:43:55 +08:00 · 21391 次点击
    这是一个创建于 2578 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址:Spring Boot JPA - Querydsl

    Querydsl 是一个类型安全的 Java 查询框架,支持 JPA, JDO, JDBC, Lucene, Hibernate Search 等标准。类型安全( Type safety )和一致性( Consistency )是它设计的两大准则。在 Spring Boot 中可以很好的弥补 JPA 的不灵活,实现更强大的逻辑。

    依赖

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-apt</artifactId>
        <scope>provided</scope>
    </dependency>
    
    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-jpa</artifactId>
    </dependency>
    

    因为是类型安全的,所以还需要加上 Maven APT plugin ,使用 APT 自动生成一些类:

    <project>
      <build>
      <plugins>
        ...
        <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>apt-maven-plugin</artifactId>
        <version>1.1.3</version>
        <executions>
            <execution>
                <phase>generate-sources</phase>
                <goals>
                    <goal>process</goal>
                </goals>
                <configuration>
                    <outputDirectory>target/generated-sources</outputDirectory>
                    <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                </configuration>
            </execution>
        </executions>
    </plugin>
        ...
      </plugins>
      </build>
    </project>
    

    基本概念

    每一个 Model (使用 @javax.persistence.Entity 注解的), Querydsl 都会在同一个包下生成一个以 Q 开头(默认,可配置)的类,来实现便利的查询操作。 如:

    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
    // 基本查询
    List<Person> persons = queryFactory.selectFrom(person)
      .where(
        person.firstName.eq("John"),
        person.lastName.eq("Doe"))
      .fetch();
    
    // 排序
    List<Person> persons = queryFactory.selectFrom(person)
      .orderBy(person.lastName.asc(), 
               person.firstName.desc())
      .fetch();
    
    // 子查询
    List<Person> persons = queryFactory.selectFrom(person)
      .where(person.children.size().eq(
        JPAExpressions.select(parent.children.size().max())
                      .from(parent)))
      .fetch();
    
    // 投影
    List<Tuple> tuples = queryFactory.select(
        person.lastName, person.firstName, person.yearOfBirth)
      .from(person)
      .fetch();
    

    有没有很强大? 。。。。。 来看一个具体一点的例子吧:

    实例

    接着上一章的几个表,来一点高级的查询操作:

    Spring 提供了一个便捷的方式使用 Querydsl ,只需要在 Repository 中继承 QueryDslPredicateExecutor 即可:

    @Repository
    public interface UserRepository extends JpaRepository<User, Long>, QueryDslPredicateExecutor<User> {
    }
    

    然后就可以使用 UserRepository 无缝和 Querydsl 连接:

    userRepository.findAll(user.name.eq("lufifcc")); // 所有用户名为 lufifcc 的用户
    
    userRepository.findAll(
            user.email.endsWith("@gmail.com")
                    .and(user.name.startsWith("lu"))
                    .and(user.id.in(Arrays.asList(520L, 1314L, 1L, 2L, 12L)))
    ); // 所有 Gmail 用户,且名字以 lu 开始,并且 ID 在 520L, 1314L, 1L, 2L, 12L 中
    
    userRepository.count(
            user.email.endsWith("@outlook.com")
                    .and(user.name.containsIgnoreCase("a"))
    ); // Outlook 用户,且名字以包含 a (不区分大小写)的用户数量
    
    userRepository.findAll(
            user.email.endsWith("@outlook.com")
                    .and(user.posts.size().goe(5))
    ); // Outlook 用户,且文章数大于等于 5
    
    userRepository.findAll(
            user.posts.size().eq(JPAExpressions.select(user.posts.size().max()).from(user))
    );// 文章数量最多的用户
    

    另外, Querydsl 还可以采用 SQL 模式查询,加入依赖:

    <dependency>
        <groupId>com.querydsl</groupId>
        <artifactId>querydsl-sql</artifactId>
    </dependency>
    

    1. 然后,获取所有用户邮箱:

    @GetMapping("users/emails")
    public Object userEmails() {
        QUser user = QUser.user;
        return queryFactory.selectFrom(user)
                .select(user.email)
                .fetch();
    }
    // 返回
    [
      "[email protected]",
      "[email protected]",
      "[email protected]",
      "[email protected]"
    ]
    

    2. 获取所有用户名和邮箱:

    @GetMapping("users/names-emails")
    public Object userNamesEmails() {
        QUser user = QUser.user;
        return queryFactory.selectFrom(user)
                .select(user.email, user.name)
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, String> map = new LinkedHashMap<>();
                    map.put("name", tuple.get(user.name));
                    map.put("email", tuple.get(user.email));
                    return map;
                }).collect(Collectors.toList());
    }
    // 返回
    [
      {
        "name": "Lufficc",
        "email": "[email protected]"
      },
      {
        "name": "Allen",
        "email": "[email protected]"
      },
      {
        "name": "Mike",
        "email": "[email protected]"
      },
      {
        "name": "Lucy",
        "email": "[email protected]"
      }
    ]
    

    注意到投影的时候,我们可以直接利用查询时用到的表达式来获取类型安全的值,如获取 name : tuple.get(user.name),返回值是 String 类型的。

    3. 获取所有用户 ID ,名称,和他们的文章数量:

    @GetMapping("users/posts-count")
    public Object postCount() {
        QUser user = QUser.user;
        QPost post = QPost.post;
        return queryFactory.selectFrom(user)
                .leftJoin(user.posts, post)
                .select(user.id, user.name, post.count())
                .groupBy(user.id)
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("id", tuple.get(user.id));
                    map.put("name", tuple.get(user.name));
                    map.put("posts_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    // 返回
    [
      {
        "id": 1,
        "name": "Lufficc",
        "posts_count": 9
      },
      {
        "id": 2,
        "name": "Allen",
        "posts_count": 6
      },
      {
        "id": 3,
        "name": "Mike",
        "posts_count": 6
      },
      {
        "id": 4,
        "name": "Lucy",
        "posts_count": 6
      }
    ]
    

    4. 获取所有用户名,以及对应用户的 JavaPython 分类下的文章数量:

    @GetMapping("users/category-count")
    public Object postCategoryMax() {
        QUser user = QUser.user;
        QPost post = QPost.post;
        NumberExpression<Integer> java = post.category
                .name.lower().when("java").then(1).otherwise(0);
        NumberExpression<Integer> python = post.category
                .name.lower().when("python").then(1).otherwise(0);
        return queryFactory.selectFrom(user)
                .leftJoin(user.posts, post)
                .select(user.name, user.id, java.sum(), python.sum(), post.count())
                .groupBy(user.id)
                .orderBy(user.name.desc())
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("username", tuple.get(user.name));
                    map.put("java_count", tuple.get(java.sum()));
                    map.put("python_count", tuple.get(python.sum()));
                    map.put("total_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    
    // 返回
    [
      {
        "username": "Mike",
        "java_count": 3,
        "python_count": 1,
        "total_count": 5
      },
      {
        "username": "Lufficc",
        "java_count": 2,
        "python_count": 4,
        "total_count": 9
      },
      {
        "username": "Lucy",
        "java_count": 1,
        "python_count": 1,
        "total_count": 5
      },
      {
        "username": "Allen",
        "java_count": 2,
        "python_count": 1,
        "total_count": 5
      }
    ]
    

    注意这里用到了强大的 Case 表达式( Case expressions )

    5. 统计每一年发文章数量,包括 JavaPython 分类下的文章数量:

    @GetMapping("posts-summary")
    public Object postsSummary() {
        QPost post = QPost.post;
        ComparableExpressionBase<?> postTimePeriodsExp = post.createdAt.year();
        NumberExpression<Integer> java = post.category
                .name.lower().when("java").then(1).otherwise(0);
        NumberExpression<Integer> python = post.category
                .name.lower().when("python").then(1).otherwise(0);
        return queryFactory.selectFrom(post)
                .groupBy(postTimePeriodsExp)
                .select(postTimePeriodsExp, java.sum(), python.sum(), post.count())
                .orderBy(postTimePeriodsExp.asc())
                .fetch()
                .stream()
                .map(tuple -> {
                    Map<String, Object> map = new LinkedHashMap<>();
                    map.put("time_period", tuple.get(postTimePeriodsExp));
                    map.put("java_count", tuple.get(java.sum()));
                    map.put("python_count", tuple.get(python.sum()));
                    map.put("total_count", tuple.get(post.count()));
                    return map;
                }).collect(Collectors.toList());
    }
    
    // 返回
    [
      {
        "time_period": 2015,
        "java_count": 1,
        "python_count": 3,
        "total_count": 6
      },
      {
        "time_period": 2016,
        "java_count": 4,
        "python_count": 2,
        "total_count": 14
      },
      {
        "time_period": 2017,
        "java_count": 3,
        "python_count": 2,
        "total_count": 7
      }
    ]
    

    补充一点

    Spring 参数支持解析 com.querydsl.core.types.Predicate,根据用户请求的参数自动生成 Predicate,这样搜索方法不用自己写啦,比如:

    @GetMapping("posts")
    public Object posts(@QuerydslPredicate(root = Post.class) Predicate predicate) {
        return postRepository.findAll(predicate);
    }
    // 或者顺便加上分页
    @GetMapping("posts")
    public Object posts(@QuerydslPredicate(root = Post.class) Predicate predicate, Pageable pageable) {
        return postRepository.findAll(predicate, pageable);
    }
    

    然后请求:

    /posts?title=title01 // 返回文章 title 为 title01 的文章
    
    /posts?id=2 // 返回文章 id 为 2 的文章
    
    /posts?category.name=Python // 返回分类为 Python 的文章(你没看错,可以嵌套,访问关系表中父类属性)
    
    /posts?user.id=2&category.name=Java // 返回用户 ID 为 2 且分类为 Java 的文章
    

    注意,这样不会产生 SQL 注入问题的,因为不存在的属性写了是不起效果的, Spring 已经进行了判断。 再补充一点,你还可以修改默认行为,继承 QueryDslPredicateExecutor 接口:

    @Repository
    public interface PostRepository extends JpaRepository<Post, Long>, QueryDslPredicateExecutor<Post>, QuerydslBinderCustomizer<QPost> {
        default void customize(QuerydslBindings bindings, QPost post) {
            bindings.bind(post.title).first(StringExpression::containsIgnoreCase);
        }
    }
    

    这样你再访问 /posts?title=title01 时,返回的是文章标题包含 title01 ,而不是仅仅等于的所有文章啦!

    总结

    本文知识抛砖引玉, Querydsl 的强大之处并没有完全体现出来,而且 Spring Boot 官方也提供了良好的支持,所以,掌握了 Querydsl ,真的不愁 Java 写不出来的 Sql,重要的是类型安全(没有强制转换),跨数据库( Querydsl 底层还是 JPA 这样的技术,所有只要不写原生 Sql ,基本关系型数据库通用)。对了,源代码在这里 example-jpa,可以直接运行的~~ 大家有什么好的经验也可以在评论里提出来啊。

    9 条回复    2020-06-13 14:13:57 +08:00
    reeco
        1
    reeco  
       2017-03-27 22:08:25 +08:00
    JPA 算得上是 ORM 的全家桶,什么都有,反而有点凌乱的感觉
    lufficc
        2
    lufficc  
    OP
       2017-03-27 22:12:50 +08:00
    @reeco Spring 有其他好的 ORM 解决方案吗?
    reeco
        3
    reeco  
       2017-03-27 22:23:23 +08:00
    @luffylcc Mybatis,JOOQ 。这两种方案都有自己鲜明的特征,但在 JPA 上感觉都能找到一点影子。
    hantsy
        4
    hantsy  
       2017-03-28 10:50:17 +08:00
    JPA 自己有一套 Criteria API 实现 Typesafe 查询, Spring Data JPA 作了封装,提供简洁的 Specification 接口。参见 https://github.com/hantsy/spring4-sandbox 中 data-jpa 目录。

    QueryDSL 同样支持 JPA , Jdbc , Mongo , Java Collections 等数据操作。
    hantsy
        5
    hantsy  
       2017-03-28 10:54:23 +08:00
    @reeco JOOQ 结合 Java 8 Stream, Labmda 新特性操作 Jdbc 比较方便, QueryDSL 的 JDBC 操作也不错。 Spring Jdbc , MyBatis 等在 Typesafe 方面显得不足。
    lufficc
        6
    lufficc  
    OP
       2017-03-28 12:32:54 +08:00
    @hantsy Criteria 的标准,投影,表连接这些操作可以吗?
    hantsy
        7
    hantsy  
       2017-03-28 17:09:06 +08:00
    @luffylcc 当然可以。
    jack80342
        8
    jack80342  
       2017-10-26 23:48:50 +08:00
    最近翻译了 Spring Boot 最新的官方文档,欢迎👏Fork,https://www.gitbook.com/book/jack80342/spring-boot/details
    yukiloh
        9
    yukiloh  
       2020-06-13 14:13:57 +08:00
    我知道挖坟不好,但是最后这段写错了
    >注意,这样不会产生 SQL 注入问题的,因为不存在的属性写了是不起效果的,Spring 已经进行了判断。 再补充一点,你还可以修改默认行为,继承 QueryDslPredicateExecutor 接口:

    并不是因为继承 `QueryDslPredicateExecutor` , 而是 `QuerydslBinderCustomizer`
    前者主要提供类似 findAll,findOne 等 springDataJpa 的接口,而后者才是自定义生成 Predicate

    我就是百度搜 `QuerydslBinderCustomizer` 有啥用找到的 ,顺便感谢楼主..
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3879 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 04:22 · PVG 12:22 · LAX 21:22 · JFK 00:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.