在本文之前,本應當專門有一篇博客講解SpringDataJPA使用自帶的Specification+JpaSpecificationExecutor去說明如何玩條件查詢,但是看到新奇、編碼更簡單易懂的技術總是會讓人感到驚喜,而且QueryDSL對SpringDataJPA有着完美的支持。如果你沒有使用過自帶的Specification去做復雜查詢,不用擔心,本節分享的QueryDSL技術與SpringDataJPA自帶支持的Specification沒有關聯。注意,本文內容一切都是基於SpringBoot1.x上構建的,如果還沒有使用過SpringBoot的伙伴,請移步SpringBoot進行初步入門。
1、QueryDSL簡介
如果說Hibernate等ORM是JPA的實現,而SpringDataJPA是對JPA使用的封裝,那么QueryDSL可以是與SpringDataJPA有着同階層的級別,它也是基於各種ORM之上的一個通用查詢框架,使用它的API類庫可以寫出“Java代碼的sql”,不用去手動接觸sql語句,表達含義卻如sql般准確。更重要的一點,它能夠構建類型安全的查詢,這比起JPA使用原生查詢時有很大的不同,我們可以不必再對惡心的“Object[]”進行操作了。當然,我們可以SpringDataJPA + QueryDSL JPA聯合使用,它們之間有着完美的相互支持,以達到更高效的編碼。
2、QueryDSL JPA的使用
2.1 編寫配置
2.1.1 pom.xml配置
在maven pom.xml的plugins標簽中配置以下plugin:
-
<build>
-
<plugins>
-
<!--其他plugin...........-->
-
-
<!--因為是類型安全的,所以還需要加上Maven APT plugin,使用 APT 自動生成一些類:-->
-
<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>
繼續在pom.xml 的dependencies中配置以下依賴:
-
<dependencies>
-
<!--SpringDataJPA-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-data-jpa
</artifactId>
-
</dependency>
-
<!--Web支持-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-web
</artifactId>
-
</dependency>
-
<!--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>
-
<!--mysql驅動-->
-
<dependency>
-
<groupId>mysql
</groupId>
-
<artifactId>mysql-connector-java
</artifactId>
-
<scope>runtime
</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-test
</artifactId>
-
<scope>test
</scope>
-
</dependency>
-
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
-
<dependency>
-
<groupId>org.projectlombok
</groupId>
-
<artifactId>lombok
</artifactId>
-
<version>1.16.10
</version>
-
<scope>provided
</scope>
-
</dependency>
-
</dependencies>
2.1.2 application.properties配置
application.properties與之前幾篇SpringDataJPA文章的application.yml配置作用是相同的,配置如下:
-
server.port=
8888
-
server.context-path=/
-
server.tomcat.uri-encoding=utf
-8
-
-
#數據源配置
-
spring.datasource.url=jdbc:mysql:
//127.0.0.1:3306/springboot_test?characterEncoding=utf8
-
#數據庫賬號
-
spring.datasource.username=root
-
#數據庫密碼
-
spring.datasource.password=
-
spring.jpa.database=mysql
-
#是否展示sql
-
spring.jpa.show-sql=
true
-
#是否自動生/更新成表,根據什么策略
-
spring.jpa.hibernate.ddl-auto=update
-
#命名策略,會將Java代碼中的駝峰命名法映射到數據庫中會變成下划線法
-
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy
2.1.3 JPAQueryFactory配置
使用QueryDSL的功能時,會依賴使用到JPAQueryFactory,而JPAQueryFactory在這里依賴使用EntityManager,所以在主類中做如下配置,使得Spring自動幫我們注入EntityManager與自動管理JPAQueryFactory:
-
@SpringBootApplication
-
public
class App {
-
-
public static void main(String[] args) {
-
SpringApplication.run(App.class, args);
-
}
-
//讓Spring管理JPAQueryFactory
-
@Bean
-
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){
-
return
new JPAQueryFactory(entityManager);
-
}
-
}
2.1.4 編寫實體建模
在這里我們先介紹單表,待會兒介紹多表時我們在進行關聯Entity的配置
-
@Data
-
@Entity
-
@Table(name =
"t_user")
-
public
class User {
-
@Id
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
-
private Integer userId;
-
private String username;
-
private String password;
-
private String nickName;
-
private Date birthday;
-
private BigDecimal uIndex;
//排序號
-
}
這個實體類非常簡單,相信大家一定有所理解。沒有使用過@Data注解,也可以不用,暫且就將他當做可以自動生成getXXX/setXXX方法的工具,如果你沒有使用這個注解,也可以直接使用IDEA快捷鍵進行生成setXXX/getXXX。如果想了解這類注解,請前往lombok介紹
2.1.5 執行maven命令
然后在IDEA中選中你的MavenProject按鈕,選中你的maven項目,雙擊compile按鈕:
如果你的控制台提示你compile執行失敗了,那么請留意一下你的maven路徑是否在IDEA中進行了正確的配置。
以上步驟執行完畢后,會在你的target中自動生成了QUser類:
該類中的代碼大致是這樣的:
-
@Generated(
"com.querydsl.codegen.EntitySerializer")
-
public
class QUser extends EntityPathBase<User> {
-
-
private
static
final
long serialVersionUID = -
646136422L;
-
-
public
static
final QUser user =
new QUser(
"user");
-
-
public
final DateTimePath<java.util.Date> birthday = createDateTime(
"birthday", java.util.Date.class);
-
-
public
final StringPath nickName = createString(
"nickName");
-
-
public
final StringPath password = createString(
"password");
-
-
public
final NumberPath<java.math.BigDecimal> uIndex = createNumber(
"uIndex", java.math.BigDecimal.class);
-
-
public
final NumberPath<Integer> userId = createNumber(
"userId", Integer.class);
-
-
public
final StringPath username = createString(
"username");
-
-
public QUser(String variable) {
-
super(User.class, forVariable(variable));
-
}
-
-
public QUser(Path<? extends User> path) {
-
super(path.getType(), path.getMetadata());
-
}
-
-
public QUser(PathMetadata metadata) {
-
super(User.class, metadata);
-
}
-
-
}
一般每有一個實體Bean配置了@Entity被檢測到之后,就會在target的子目錄中自動生成一個Q+實體名稱 的類,這個類對我們使用QueryDSL非常重要,正是因為它,我們才使得QueryDSL能夠構建類型安全的查詢。
2.2 使用
2.2.1 單表使用
在repository包中添加UserRepository,使用QueryDSL時可以完全不依賴使用QueryDslPredicateExecutor,但是為了展示與SpringDataJPA的聯合使用,我們讓repository繼承這個接口,以便獲得支持:
-
public
interface UserRepository extends JpaRepository<User, Integer>, QueryDslPredicateExecutor<User> {
-
}
為了講解部分字段映射查詢,我們在bean包創建一個UserDTO類,該類只有User實體的部分字段:
-
@Data
-
@Builder
-
public
class UserDTO {
-
private String userId;
-
private String username;
-
private String nickname;
-
private String birthday;
-
}
對於@Data與@Buider,都是lombok里的注解,能夠幫我們生成get/set、toString方法,還能使用建造者模式去創建UserDTO,如果不會的同學也可以使用IDE生成get/set,想了解的同學可以進入lombok進行簡單學習。
在service包中添加UserService,代碼如下:
-
@Service
-
public
class UserService {
-
@Autowired
-
private UserRepository userRepository;
-
@Autowired
-
JPAQueryFactory jpaQueryFactory;
-
-
//////////////////////////以下展示使用原生的dsl/////////////////////
-
-
/**
-
* 根據用戶名和密碼查找(假定只能找出一條)
-
*
-
* @param username
-
* @param password
-
* @return
-
*/
-
public User findByUsernameAndPassword(String username, String password) {
-
QUser user = QUser.user;
-
return jpaQueryFactory
-
.selectFrom(user)
-
.where(
-
user.username.eq(username),
-
user.password.eq(password)
-
)
-
.fetchOne();
-
}
-
-
/**
-
* 查詢所有的實體,根據uIndex字段排序
-
*
-
* @return
-
*/
-
public List<User> findAll() {
-
QUser user = QUser.user;
-
return jpaQueryFactory
-
.selectFrom(user)
-
.orderBy(
-
user.uIndex.asc()
-
)
-
.fetch();
-
}
-
-
/**
-
*分頁查詢所有的實體,根據uIndex字段排序
-
*
-
* @return
-
*/
-
public QueryResults<User> findAllPage(Pageable pageable) {
-
QUser user = QUser.user;
-
return jpaQueryFactory
-
.selectFrom(user)
-
.orderBy(
-
user.uIndex.asc()
-
)
-
.offset(pageable.getOffset())
//起始頁
-
.limit(pageable.getPageSize())
//每頁大小
-
.fetchResults();
//獲取結果,該結果封裝了實體集合、分頁的信息,需要這些信息直接從該對象里面拿取即可
-
}
-
-
/**
-
* 根據起始日期與終止日期查詢
-
* @param start
-
* @param end
-
* @return
-
*/
-
public List<User> findByBirthdayBetween(Date start, Date end){
-
QUser user = QUser.user;
-
return jpaQueryFactory
-
.selectFrom(user)
-
.where(
-
user.birthday.between(start, end)
-
)
-
.fetch();
-
}
-
-
/**
-
* 部分字段映射查詢
-
* 投影為UserRes,lambda方式(靈活,類型可以在lambda中修改)
-
*
-
* @return
-
*/
-
public List<UserDTO> findAllUserDto(Pageable pageable) {
-
QUser user = QUser.user;
-
List<UserDTO> dtoList = jpaQueryFactory
-
.select(
-
user.username,
-
user.userId,
-
user.nickName,
-
user.birthday
-
)
-
.from(user)
-
.offset(pageable.getOffset())
-
.limit(pageable.getPageSize())
-
.fetch()
-
.stream()
-
.map(tuple -> UserDTO.builder()
-
.username(tuple.get(user.username))
-
.nickname(tuple.get(user.nickName))
-
.userId(tuple.get(user.userId).toString())
-
.birthday(
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss").format(tuple.get(user.birthday)))
-
.build()
-
)
-
.collect(Collectors.toList());
-
-
return dtoList;
-
}
-
-
/**
-
* 部分字段映射查詢
-
* 投影為UserRes,自帶的Projections方式,不夠靈活,不能轉換類型,但是可以使用as轉換名字
-
*
-
* @return
-
*/
-
/*public List<UserDTO> findAllDto2() {
-
QUser user = QUser.user;
-
List<UserDTO> dtoList = jpaQueryFactory
-
.select(
-
Projections.bean(
-
UserDTO.class,
-
user.username,
-
user.userId,
-
user.nickName,
-
user.birthday
-
)
-
)
-
.from(user)
-
.fetch();
-
-
return dtoList;
-
}*/
-
-
//////////////////////////以下展示使用與SpringDataJPA整合的dsl/////////////////////
-
-
/**
-
* 根據昵稱與用戶名查詢,並且根據uIndex排序
-
*
-
* @param nickName
-
* @return
-
*/
-
public List<User> findByNicknameAndUsername(String nickName, String username) {
-
QUser user = QUser.user;
-
List<User> users = (List<User>) userRepository.findAll(
-
user.nickName.eq(nickName)
-
.and(user.username.eq(username)),
-
user.uIndex.asc()
//排序參數
-
);
-
return users;
-
}
-
-
/**
-
* 統計名字像likeName的記錄數量
-
*
-
* @return
-
*/
-
public long countByNickNameLike(String likeName) {
-
QUser user = QUser.user;
-
return userRepository.count(
-
user.nickName.like(
"%" + likeName +
"%")
-
);
-
}
-
-
//////////////////////////展示dsl動態查詢////////////////////////////////
-
-
/**
-
* 所有條件動態分頁查詢
-
*
-
* @param username
-
* @param password
-
* @param nickName
-
* @param birthday
-
* @param uIndex
-
* @return
-
*/
-
public Page<User> findByUserProperties(Pageable pageable, String username, String password, String nickName, Date birthday, BigDecimal uIndex) {
-
QUser user = QUser.user;
-
//初始化組裝條件(類似where 1=1)
-
Predicate predicate = user.isNotNull().or(user.isNull());
-
-
//執行動態條件拼裝
-
predicate = username ==
null ? predicate : ExpressionUtils.and(predicate,user.username.eq(username));
-
predicate = password ==
null ? predicate : ExpressionUtils.and(predicate,user.password.eq(password));
-
predicate = nickName ==
null ? predicate : ExpressionUtils.and(predicate,user.nickName.eq(username));
-
predicate = birthday ==
null ? predicate : ExpressionUtils.and(predicate,user.birthday.eq(birthday));
-
predicate = uIndex ==
null ? predicate : ExpressionUtils.and(predicate,user.uIndex.eq(uIndex));
-
-
Page<User> page = userRepository.findAll(predicate, pageable);
-
return page;
-
}
-
-
/**
-
* 動態條件排序、分組查詢
-
* @param username
-
* @param password
-
* @param nickName
-
* @param birthday
-
* @param uIndex
-
* @return
-
*/
-
public List<User> findByUserPropertiesGroupByUIndex(String username, String password, String nickName, Date birthday, BigDecimal uIndex) {
-
-
QUser user = QUser.user;
-
//初始化組裝條件(類似where 1=1)
-
Predicate predicate = user.isNotNull().or(user.isNull());
-
//執行動態條件拼裝
-
predicate = username ==
null ? predicate : ExpressionUtils.and(predicate, user.username.eq(username));
-
predicate = password ==
null ? predicate : ExpressionUtils.and(predicate, user.password.eq(password));
-
predicate = nickName ==
null ? predicate : ExpressionUtils.and(predicate, user.nickName.eq(username));
-
predicate = birthday ==
null ? predicate : ExpressionUtils.and(predicate, user.birthday.eq(birthday));
-
predicate = uIndex ==
null ? predicate : ExpressionUtils.and(predicate, user.uIndex.eq(uIndex));
-
//執行拼裝好的條件並根據userId排序,根據uIndex分組
-
List<User> list = jpaQueryFactory
-
.selectFrom(user)
-
.where(predicate)
//執行條件
-
.orderBy(user.userId.asc())
//執行排序
-
.groupBy(user.uIndex)
//執行分組
-
.having(user.uIndex.longValue().max().gt(
7))
//uIndex最大值小於7
-
.fetch();
-
-
//封裝成Page返回
-
return list;
-
}
-
}
代碼有點多,解釋一波。
a、第一個方法是根據用戶名與密碼進行查詢,QUser中有一個靜態user屬性,直接生成QUser的實例,QueryDSL都是圍繞着這個QXxx來進行操作的。代碼很直觀,selectFrom是select方法與from方法的合並,這里為了方便就不分開寫了,where中可以收可變參數Predicate,由於Predicate是一個接口,由user.username.eq或者user.uIndex.gt等等方法返回的都是BooleanExpression或者XXXExpression,這些XXXExpression都是Predicate的實現,故直接傳入,讓QueryDSL在內部做處理,其實Predicate也是實現了Expression接口,大家如果有興趣可以自行跟蹤源碼研究。
b、第二個方法也相當的直觀,跟sql的字面意思幾乎一模一樣。
c、第三個方法是第二個方法的排序寫法,主要用到了offerset、limit方法,根據傳入的pageable參數進行分頁,最后返回的結果是一個QuerResults類型的返回值,該返回值對象簡單的封裝了一些分頁的參數與返回的實體集,然調用者自己根據需求去取出使用。
d、第四個方法展示了日期查詢,也相當的直觀,大家嘗試了就知道了,主要使用到了between方法。
e、第五個方法是比較重要的方法,這個方法展示了如何進行部分字段的映射查詢,這個方法的目的是只查詢uerrname、userId、nickname、birthday四個字段,然后封裝到UserDTO中,最后返回。其中,由於select與from拆分了以后返回的泛型類型就是Tuple類型(Tuple是一個接口,它可以根據tuple.get(QUser.username)的方式獲取User.username的真實值,暫時將他理解為一個類型安全的Map就行),根據pageable參數做了分頁處理,fetch之后就返回了一個List<Tuple>對象。從fetch()方法之后,使用到了Stream,緊接着使用Java8的高階函數map,這個map函數的作用是將List<Tuple>中的Tuple元素依次轉換成List<UserDTO>中的UserDTO元素,在這個過程中我們還可以做bean的屬性類型轉換,將User的Date、Integer類型都轉換成String類型。最后,通過collect結束stream,返回一個我們需要的List<UserDTO>。
f、第六個方法與第五個方法的效果相同,使用QueryDSL的Projections實現。但是有一點,當User實體的屬性類型與UserDTO中的屬性類型不相同時,不方便轉換。除了屬性類型相同時轉換方便以外,還是建議使用map函數進行操作。
g、第七、第八個方法展示了QueryDSL與SpringDataJPA的聯合使用,由於我們的UserRepository繼承了QueryDslPredicateExecutor,所以獲得了聯合使用的支持。來看一看QueryDslPredicateExcutor接口的源碼:
-
public
interface QueryDslPredicateExecutor<T> {
-
T findOne(Predicate var1);
-
-
Iterable<T> findAll(Predicate var1);
-
-
Iterable<T> findAll(Predicate var1, Sort var2);
-
-
Iterable<T> findAll(Predicate var1, OrderSpecifier... var2);
-
-
Iterable<T> findAll(OrderSpecifier... var1);
-
-
Page<T> findAll(Predicate var1, Pageable var2);
-
-
long count(Predicate var1);
-
-
boolean exists(Predicate var1);
-
}
這里面的方法大多數都是可以傳入Predicate類型的參數,說明還是圍繞着QUser來進行操作的,如傳入quser.username.eq("123")的方式,操作都非常簡單。以下是部分源碼,請看:
-
/*
-
* Copyright 2008-2017 the original author or authors.
-
*
-
* Licensed under the Apache License, Version 2.0 (the "License");
-
* you may not use this file except in compliance with the License.
-
* You may obtain a copy of the License at
-
*
-
* http://www.apache.org/licenses/LICENSE-2.0
-
*
-
* Unless required by applicable law or agreed to in writing, software
-
* distributed under the License is distributed on an "AS IS" BASIS,
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-
* See the License for the specific language governing permissions and
-
* limitations under the License.
-
*/
-
package org.springframework.data.jpa.repository.support;
-
-
import java.io.Serializable;
-
import java.util.List;
-
import java.util.Map.Entry;
-
-
import javax.persistence.EntityManager;
-
import javax.persistence.LockModeType;
-
-
import org.springframework.data.domain.Page;
-
import org.springframework.data.domain.Pageable;
-
import org.springframework.data.domain.Sort;
-
import org.springframework.data.querydsl.EntityPathResolver;
-
import org.springframework.data.querydsl.QSort;
-
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
-
import org.springframework.data.querydsl.SimpleEntityPathResolver;
-
import org.springframework.data.repository.support.PageableExecutionUtils;
-
import org.springframework.data.repository.support.PageableExecutionUtils.TotalSupplier;
-
-
import com.querydsl.core.types.EntityPath;
-
import com.querydsl.core.types.OrderSpecifier;
-
import com.querydsl.core.types.Predicate;
-
import com.querydsl.core.types.dsl.PathBuilder;
-
import com.querydsl.jpa.JPQLQuery;
-
import com.querydsl.jpa.impl.AbstractJPAQuery;
-
-
/**
-
* QueryDsl specific extension of {@link SimpleJpaRepository} which adds implementation for
-
* {@link QueryDslPredicateExecutor}.
-
*
-
* @author Oliver Gierke
-
* @author Thomas Darimont
-
* @author Mark Paluch
-
* @author Jocelyn Ntakpe
-
* @author Christoph Strobl
-
*/
-
public
class QueryDslJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
-
implements
QueryDslPredicateExecutor<
T> {
-
-
private
static
final EntityPathResolver DEFAULT_ENTITY_PATH_RESOLVER = SimpleEntityPathResolver.INSTANCE;
-
-
private
final EntityPath<T> path;
-
private
final PathBuilder<T> builder;
-
private
final Querydsl querydsl;
-
-
/**
-
* Creates a new {@link QueryDslJpaRepository} from the given domain class and {@link EntityManager}. This will use
-
* the {@link SimpleEntityPathResolver} to translate the given domain class into an {@link EntityPath}.
-
*
-
* @param entityInformation must not be {@literal null}.
-
* @param entityManager must not be {@literal null}.
-
*/
-
public QueryDslJpaRepository(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
-
this(entityInformation, entityManager, DEFAULT_ENTITY_PATH_RESOLVER);
-
}
-
...............
-
............
至於源碼,感興趣的同學可以自行跟蹤研究。
h、最后,我們的殺手鐧來了,JPA對動態條件拼接查詢支持一向都不太靈活,如果在mybatis中,我們使用if標簽可以很容易的實現動態查詢,但是在JPA中,就沒有那么方便了。SpringDataJPA給我們提供了Specification+JpaSpecificationExecutor幫我們解決,但是,在需要編寫某些復雜的動態條件拼接、分組之后又動態拼接的復雜查詢,可能就顯得力不從心了,這個時候可能需要直接對entityManager操作,然后對sql進行拼接,請看以下老代碼是怎么寫的:
-
//自定義動態報表原生sql查詢
-
//只要isSelect不為空,那么就執行條件查詢:前端自行根據返回值判斷是否已經選題(大於0就是選了題的)
-
public Page<StudentToAdminVO> findAllStudentToAdminVOPage(Pageable pageable, String majorId, Boolean isSelect, Boolean isSelectSuccess) {
-
//設置條件
-
StringBuffer where =
new StringBuffer(
" where 1=1 ");
-
StringBuffer having =
new StringBuffer(
" having 1=1");
-
if(!StringUtils.isEmpty(majorId)){
-
where.append(
" and s.major_id=:majorId ");
-
}
-
if(isSelect!=
null){
-
//是否選題了,只需要看查出的這個數是否大於零即可
-
if(isSelect)
-
having.append(
" and count(se.id)>0 ");
-
else
-
having.append(
" and count(se.id)=0 ");
-
}
-
if(isSelectSuccess !=
null){
-
if(isSelectSuccess)
-
having.append(
" and max(se.is_select)>0");
-
else
-
having.append(
" and (max(se.is_select) is null or max(se.is_select)<=0)");
-
}
-
//主體sql
-
String sql =
"select s.id, s.username, s.nickname, s.sclass, m.name majorName, count(se.id) as choose, max(se.is_select) as selectSuccess from student s"
-
+
" left join selection se on s.id=se.student_id "
-
+
" left join major m on m.id=s.major_id "
-
+ where
-
+
" group by s.id"
-
+ having;
-
String countSql =
null;
-
//計算總記錄數sql
-
if(isSelect!=
null){
-
countSql =
"select count(*) from student s "
-
+ where
-
+
" and s.id in(select ss.id FROM student ss left join selection se on se.student_id=ss.id GROUP BY ss.id "
-
+ having
-
+
" )";
-
}
else{
-
countSql =
"select count(*) from student s " + where;
-
}
-
//創建原生查詢
-
Query query = em.createNativeQuery(sql);
-
Query countQuery = em.createNativeQuery(countSql);
-
if(!StringUtils.isEmpty(majorId)){
-
query.setParameter(
"majorId", majorId);
-
countQuery.setParameter(
"majorId", majorId);
-
}
-
int total = Integer.valueOf(countQuery.getSingleResult().toString());
-
// pageable.getPageNumber()==0 ? pageable.getOffset() : pageable.getOffset()-5
-
if(pageable!=
null){
-
query.setFirstResult(pageable.getOffset());
-
query.setMaxResults(pageable.getPageSize());
-
}
-
//對象映射
-
query.unwrap(SQLQuery.class)
-
.addScalar(
"id", StandardBasicTypes.STRING)
-
.addScalar(
"username", StandardBasicTypes.STRING)
-
.addScalar(
"nickname", StandardBasicTypes.STRING)
-
.addScalar(
"sclass", StandardBasicTypes.STRING)
-
.addScalar(
"majorName", StandardBasicTypes.STRING)
-
.addScalar(
"choose", StandardBasicTypes.INTEGER)
-
.addScalar(
"selectSuccess", StandardBasicTypes.INTEGER)
-
.setResultTransformer(Transformers.aliasToBean(StudentToAdminVO.class));
-
-
return
new PageImpl<StudentToAdminVO>(query.getResultList(), pageable, total);
-
}
我們再來看一個對JPQL拼接的例子:
-
/**
-
* 動態查詢
-
* @param pageable
-
* @param isSelect 是否確選(只要非空,都相當於加了條件)
-
* @param titleLike 根據title模糊查詢
-
* @param teacherId 根據老師的id查詢
-
* @return
-
*/
-
Page<SubjectToTeacherVO> findAllSubjectToTeacherVO(Pageable pageable, Boolean isSelect, String titleLike,
-
String teacherId, String majorId, String studentId){
-
//條件組合
-
StringBuffer where =
new StringBuffer(
" where 1=1 ");
-
StringBuffer having =
new StringBuffer();
-
if(isSelect !=
null){
-
if(isSelect)
-
having.append(
" having max(se.isSelection)>0 ");
-
else
-
having.append(
" having ((max(se.isSelection) is null) or max(se.isSelection)<=0) ");
-
}
-
if(!StringUtils.isEmpty(titleLike))
-
where.append(
" and su.title like :titleLike");
-
if(!StringUtils.isEmpty(teacherId))
-
where.append(
" and su.teacher.id=:teacherId");
-
if(!StringUtils.isEmpty(majorId))
-
where.append(
" and su.major.id=:majorId");
-
if(!StringUtils.isEmpty(studentId)){
-
where.append(
" and su.major.id=(select stu.major.id from Student stu where stu.id=:studentId)");
-
}
-
//主jpql 由於不能使用 if(exp1,rsul1,rsul2)只能用case when exp1 then rsul1 else rsul2 end
-
String jpql =
"select new cn.edu.glut.vo.SubjectToTeacherVO(su.id, su.title, cast(count(se.id) as int) as guysNum, max(se.isSelection) as choose, "
-
+
" (select ss.nickname from Selection as sel left join sel.student as ss where sel.subject.id=su.id and sel.isSelection=1) as stuName, "
-
+
" (select t.nickname from Teacher t where t.id=su.teacher.id) as teacherName, "
-
+
" ma.id as majorId, ma.name as majorName) "
-
+
" from Subject as su left join su.selections as se"
-
+
" left join su.major as ma "
-
+ where
-
+
" group by su.id "
-
+ having;
-
-
String countJpql =
null;
-
if(isSelect !=
null)
-
countJpql =
"select count(*) from Subject su left join su.selections as se left join se.student as s"
-
+ where
-
+
" and su.id in(select s.id from Subject s left join s.selections se group by s.id "
-
+ having
-
+
" )";
-
else
-
countJpql =
"select count(*) from Subject su left join su.selections as se left join se.student as s" + where;
-
Query query = em.createQuery(jpql, SubjectToTeacherVO.class);
-
Query countQuery = em.createQuery(countJpql);
-
// pageable.getPageNumber()==0 ? pageable.getOffset() : pageable.getOffset()-5
-
if(
null != pageable){
-
query.setFirstResult(pageable.getOffset());
-
query.setMaxResults(pageable.getPageSize());
-
}
-
if(!StringUtils.isEmpty(titleLike)){
-
query.setParameter(
"titleLike",
"%"+titleLike+
"%");
-
countQuery.setParameter(
"titleLike",
"%"+titleLike+
"%");
-
}
-
if(!StringUtils.isEmpty(teacherId)){
-
query.setParameter(
"teacherId", teacherId);
-
countQuery.setParameter(
"teacherId", teacherId);
-
}
-
if(!StringUtils.isEmpty(majorId)){
-
query.setParameter(
"majorId", majorId);
-
countQuery.setParameter(
"majorId", majorId);
-
}
-
if(!StringUtils.isEmpty(studentId)){
-
query.setParameter(
"studentId", studentId);
-
countQuery.setParameter(
"studentId", studentId);
-
}
-
List<SubjectToTeacherVO> voList = query.getResultList();
-
return
new PageImpl<SubjectToTeacherVO>(voList, pageable, Integer.valueOf(countQuery.getSingleResult().toString()));
-
}
說惡心一點都不過分...不知道小伙伴們覺得如何,反正我是忍不了了...
我們UserService中的最后兩個方法就是展示如何做動態查詢的,第一個方法結合了與SpringDataJPA整合QueryDSL的findAll來實現,第二個方法使用QueryDSL本身的API進行實現,其中Page是Spring自身的的。第一行user.isNotNull.or(user.isNull())可以做到類似where 1=1的效果以便於后面進行的拼接,當然,主鍵是不能為空的,所以此處也可以只寫user.isNotNull()也可以。緊接着是一串的三目運算符表達式,以第一行三目表達式為例,表達意義在於:如果用戶名為空,就返回定義好的predicate;如果不為空,就使用ExpressionUtils的and方法將username作為條件進行組合,ExpressionUtils的and方法返回值也是一個Predicate。下面的幾條三目運算符與第一條是類似的。最后使用findAll方法傳入分頁參數與條件參數predicate,相比起上面頭疼的自己拼接,是不是簡潔了很多?
最后一個方法展示的是如果有分組條件時進行的查詢,相信大家就字面意思理解也能知道大概的意思了,前半部分代碼是相同的,groupby之后需要插入條件是要用到having的。就與sql的規范一樣,如果對分組與having還不了解,希望大家多多google哦。
2.2.2 多表使用
對於多表使用,大致與單表類似。我們先創建一個一對多的關系,一個部門對應有多個用戶,創建Department實體:
-
@Data
-
@Entity
-
@Table(name =
"t_department")
-
public
class Department {
-
@Id
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
-
private Integer deptId;
//部門id
-
private String deptName;
//部門名稱
-
private Date createDate;
//創建時間
-
}
在User實體需要稍稍修改,User實體中添加department建模:
-
@Data
-
@Entity
-
@Table(name =
"t_user")
-
public
class User {
-
@Id
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
-
private Integer userId;
-
private String username;
-
private String password;
-
private String nickName;
-
private Date birthday;
-
private BigDecimal uIndex;
//排序號
-
//一對多映射
-
@ManyToOne(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
-
@JoinColumn(name =
"department_id")
-
private Department department;
//部門實體
-
}
我們假設有一個這樣的需求,前端需要展示根據部門deptId來查詢用戶的基礎信息,在展示用戶基礎信息的同時需要展示該用戶所屬的部門名稱以及該部門創建的時間。那么,我們創建一個這樣的DTO來滿足前端的需求:
-
@Data
-
@Builder
-
public
class UserDeptDTO {
-
//用戶基礎信息
-
private String username;
//用戶名
-
private String nickname;
//昵稱
-
private String birthday;
//用戶生日
-
//用戶的部門信息
-
private String deptName;
//用戶所屬部門
-
private String deptBirth;
//部門創建的時間
-
}
大家一定想到了使用部分字段映射的投影查詢,接下來我們在UserService中添加如下代碼:
-
/**
-
* 根據部門的id查詢用戶的基本信息+用戶所屬部門信息,並且使用UserDeptDTO進行封裝返回給前端展示
-
* @param departmentId
-
* @return
-
*/
-
public List<UserDeptDTO> findByDepatmentIdDTO(int departmentId) {
-
QUser user = QUser.user;
-
QDepartment department = QDepartment.department;
-
//直接返回
-
return jpaQueryFactory
-
//投影只去部分字段
-
.select(
-
user.username,
-
user.nickName,
-
user.birthday,
-
department.deptName,
-
department.createDate
-
-
)
-
.from(user)
-
//聯合查詢
-
.join(user.department, department)
-
.where(department.deptId.eq(departmentId))
-
.fetch()
-
//lambda開始
-
.stream()
-
.map(tuple ->
-
//需要做類型轉換,所以使用map函數非常適合
-
UserDeptDTO.builder()
-
.username(tuple.get(user.username))
-
.nickname(tuple.get(user.nickName))
-
.birthday(
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss").format(tuple.get(user.birthday)))
-
.deptName(tuple.get(department.deptName))
-
.deptBirth(
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss").format(tuple.get(department.createDate)))
-
.build()
-
)
-
.collect(Collectors.toList());
-
}
select部分是選擇需要查詢的字段,leftjoin的第一個參數是用戶所關聯的部門,第二個參數可以當做該user.department別名來使用,往后看即可理解。where中只有一個很簡單的條件,即根據部門的id來進行查詢,最后使用stream來將Tuple轉換成UserDeptDTO,中間在map函數中對一些屬性的類型進行了轉換。其他的關聯操作與上述代碼類似,對於orderBy、groupBy、聚合函數、分頁操作的API都與單表的類似,只是where中的條件自己進行適配即可。
在應用開發中我們可能不會在代碼中設置@ManyToOne、@ManyToMany這種類型的“強建模”,而是在隨從的Entity中僅僅聲明一個外鍵屬性,比如User實體的下面代碼,只是添加了一個departmentId:
-
@Data
-
@Entity
-
@Table(name =
"t_user")
-
public
class User {
-
@Id
-
@GeneratedValue(strategy = GenerationType.IDENTITY)
-
private Integer userId;
-
private String username;
-
private String password;
-
private String nickName;
-
private Date birthday;
-
private BigDecimal uIndex;
//排序號
-
private Integer departmentId;
-
}
這時候我們的多表關聯業務代碼只需要稍作修改就可以:
-
/**
-
* 根據部門的id查詢用戶的基本信息+用戶所屬部門信息,並且使用UserDeptDTO進行封裝返回給前端展示
-
*
-
* @param departmentId
-
* @return
-
*/
-
public List<UserDeptDTO> findByDepatmentIdDTO(int departmentId) {
-
QUser user = QUser.user;
-
QDepartment department = QDepartment.department;
-
//直接返回
-
return jpaQueryFactory
-
//投影只去部分字段
-
.select(
-
user.username,
-
user.nickName,
-
user.birthday,
-
department.deptName,
-
department.createDate
-
-
)
-
.from(user, department)
-
//聯合查詢
-
.where(
-
user.departmentId.eq(department.deptId).and(department.deptId.eq(departmentId))
-
)
-
.fetch()
-
//lambda開始
-
.stream()
-
.map(tuple ->
-
//需要做類型轉換,所以使用map函數非常適合
-
UserDeptDTO.builder()
-
.username(tuple.get(user.username))
-
.nickname(tuple.get(user.nickName))
-
.birthday(
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss").format(tuple.get(user.birthday)))
-
.deptName(tuple.get(department.deptName))
-
.deptBirth(
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss").format(tuple.get(department.createDate)))
-
.build()
-
)
-
.collect(Collectors.toList());
-
}
我們在from中多加了department參數,在where中多加了一個user.department.eq(department.deptId)條件,與sql中的操作類似。為什么在這里不適用join....on....呢,原因是我們使用的是QueryDSL-JPA,QueryDSL對JPA支持是全面的,當然也有QueryDSL-SQL,但是配置起來會比較麻煩。如果大家想了解QueryDSL-SQL可以點擊這里進行了解。
3 結語
使用SpringDataJPA能夠解決我們大多數問題,但是在處理復雜條件、動態條件、投影查詢時可能QueryDSL JPA更加直觀,而且SpringDataJPA對QueryDSL有着非常好的支持,SpringDataJPA+QueryDSL在我眼里看來是天生一對,互相補漏。在使用一般查詢、能夠滿足基礎條件的查詢我們使用SpringDataJPA更加簡潔方便,當遇到復雜、投影、動態查詢時我們可以考慮使用QueryDSL做開發。以上方案可以解決大多數持久層開發問題,當然,如果問題特別刁鑽,還是不能滿足你的需求,你也可以考慮直接操作HQL或者SQL。
今天的分享就到此結束了,小伙伴們如果有更好的建議,歡迎大家提出指正,但是請不要謾罵與辱罵,寫一篇博客實屬不易。