Stay Hungry.Stay Foolish.

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();
}
// 返回
[
  "lufficc@qq.com",
  "allen@qq.com",
  "mike@qq.com",
  "lucy@qq.com"
]

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": "lufficc@qq.com"
  },
  {
    "name": "Allen",
    "email": "allen@qq.com"
  },
  {
    "name": "Mike",
    "email": "mike@qq.com"
  },
  {
    "name": "Lucy",
    "email": "lucy@qq.com"
  }
]

注意到投影的时候,我们可以直接利用查询时用到的表达式来获取类型安全的值,如获取 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,可以直接运行的~~
大家有什么好的经验也可以在评论里提出来啊。

⬆️

写的不错,帮助到了您,赞助一下主机费~

扫一扫,用支付宝赞赏
扫一扫,用微信赞赏
2017-05-11 00:13

你好。我最近在用spring-boot + spring-data-mybatis框架开发后台。为了支持前端的多字段搜索接口,我需要根据参数是否为空来使用不同的SQL。我看到 http://www.ifrabbit.com/#core.extensions.querydsl 中提到使用querydsl组件进行动态sql生成。加上maven依赖,然后在实体类对应的repository接口上继承 org.springframework.data.querydsl.QueryPredicateExecutor接口。但是编译项目时报错了:
·org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'firmwareController': Unsatisfied dependency expressed through field 'firmwareRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'firmwareRepository': Invocation of init method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.mybatis.repository.support.QueryDslMybatisRepository]: Constructor threw exception; nested exception is java.lang.UnsupportedOperationException: unsupported QueryDsl Repository... at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:588) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:366) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1264) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483) ~[spring-beans-4.3.7.RELEASE.jar:4.3.7.RELEASE] at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306) ~[sp·
请问你知道问题出在哪儿吗?

2018-07-30 13:37

@BillGrim 你好,我也遇到这个问题了,请问解决了吗?

2018-07-31 11:25

@BillGrim 通过查看源码,我发现这个库是不支持dsl repository的

lufficc #806
2017-05-11 09:56

@BillGrim unsupported QueryDsl Repository ? 不支持吗?

2017-05-11 10:34

@lufficc

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.mybatis.repository.support.QueryDslMybatisRepository]: Constructor threw exception; nested exception is java.lang.UnsupportedOperationException: unsupported QueryDsl Repository...
2017-05-11 10:43

@lufficc 我不是太清楚原因,我的repository声明是这样的:
·public interface FirmwareRepository extends MybatisRepository<Firmware, Long>,QueryDslPredicateExecutor{}·
然后Firmware类使用的是org.springframework.data.mybatis.annotations.Entity注解,因为我要自定义数据表名:
·@Entity(table = 'iot_rom'
public class Firmware extends LongId{}·
很多文章说是要用javax.persistence.Entity注解才能被querydsl正确使用,但是那样就不能使用table属性了。

lufficc #809
2017-05-11 19:24

@BillGrim javax.persistence.Entity 注解才可以,但是你可以两个注解都加上啊,不会相互影响的

2018-07-30 13:38

@lufficc 博主你好,这个问题有解决方法了吗?

2017-05-12 13:47

@lufficc 我加上了·javax.persistence.Entity·注解,但是运行项目时还是报同样的错

2017-07-17 14:36

你好 请问怎样使用union

2017-09-04 23:23

@BillGrim
@方块
不知道

2017-11-21 16:33

你好,能不能写一个这样的示例参考:查询具有某个或者多个Tag的Posts?谢谢