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. 獲取所有用戶名,以及對應用戶的 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,可以直接運行的~~
大家有什么好的經驗也可以在評論里提出來啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM