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. 獲取所有用戶名,以及對應用戶的 Java
、Python
分類下的文章數量:
@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. 統計每一年發文章數量,包括 Java
、Python
分類下的文章數量:
@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,可以直接運行的~~
大家有什么好的經驗也可以在評論里提出來啊。