阅读本文需要Mysql,Maven和SpringBoot基础知识。
更新日志
- 2018.03.19更新:增加
二、1.2.7 分页的两种写法
和二、1.2.8 使用Template实现QueryDSL未支持的语法
- 2018.01.25更新:增加使用心得(查询条件中字段为String时关于null,empty,blank的表达)
- 2018.01.24更新:增加mysql聚合函数CONCAT,DATE_FORMAT的使用示例
一、环境配置
1. 引入maven依赖
<!-- querydsl -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
2. 添加maven插件
添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:"Q"+对应实体名)。
上文引入的依赖中querydsl-apt
即是为此插件服务的。
注:在使用过程中,如果遇到query type无法自动生成的情况,用maven更新一下项目即可解决(右键项目->Maven->Update Project)。
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
补充:
QueryDSL默认使用HQL发出查询语句。但也支持原生SQL查询。
若要使用原生SQL查询,你需要使用下面这个maven插件生成相应的query type。
<project>
<build>
<plugins>
...
<plugin>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-maven-plugin</artifactId>
<version>${querydsl.version}</version>
<executions>
<execution>
<goals>
<goal>export</goal>
</goals>
</execution>
</executions>
<configuration>
<jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>
<jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>
<packageName>com.mycompany.mydomain</packageName>
<targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>
</configuration>
<dependencies>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>${derby.version}</version>
</dependency>
</dependencies>
</plugin>
...
</plugins>
</build>
</project>
二、使用
在Spring环境下,我们可以通过两种风格来使用QueryDSL。
一种是使用JPAQueryFactory
的原生QueryDSL风格,
另一种是基于Spring Data提供的QueryDslPredicateExecutor<T>
的Spring-data风格。
使用QueryDslPredicateExecutor<T>
可以简化一些代码,使得查询更加优雅。
而JPAQueryFactory
的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。
下面分别介绍两种风格的使用方式。
1. JPAQueryFactory
JPAQueryFactory使用逻辑类似于HQL/SQL语法,不再额外说明。
QueryDSL在支持JPA的同时,也提供了对Hibernate的支持。可以通过HibernateQueryFactory
来使用。
装配
@Bean
@Autowired
public JPAQueryFactory jpaQuery(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
注入
@Autowired
JPAQueryFactory queryFactory;
1.1 更新/删除
Update
QMemberDomain qm = QMemberDomain.memberDomain;
queryFactory.update(qm).set(qm.status, "0012").where(qm.status.eq("0011")).execute();
Delete
QMemberDomain qm = QMemberDomain.memberDomain;
queryFactory.delete(qm).where(qm.status.eq("0012")).execute();
1.2 查询
查询简直可以玩出花来。
1.2.1 select()和fetch()的几种常用写法
QMemberDomain qm = QMemberDomain.memberDomain;
//查询字段-select()
List<String> nameList = queryFactory.select(qm.name).from(qm).fetch();
//查询实体-selectFrom()
List<MemberDomain> memberList = queryFactory.selectFrom(qm).fetch();
//查询并将结果封装至dto中
List<MemberFavoriteDto> dtoList = queryFactory.select(Projections.constructor(MemberFavoriteDto.class,qm.name,qf.favoriteStoreCode)).from(qm).leftJoin(qm.favoriteInfoDomains,qf).fetch();
//去重查询-selectDistinct()
List<String> distinctNameList = queryFactory.selectDistinct(qm.name).from(qm).fetch();
//获取首个查询结果-fetchFirst()
MemberDomain firstMember = queryFactory.selectFrom(qm).fetchFirst();
//获取唯一查询结果-fetchOne()
//当fetchOne()根据查询条件从数据库中查询到多条匹配数据时,会抛`NonUniqueResultException`。
MemberDomain anotherFirstMember = queryFactory.selectFrom(qm).fetchOne();
1.2.2 where子句查询条件的几种常用写法
//查询条件示例
List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm)
//like示例
.where(qm.name.like('%'+"Jack"+'%')
//contain示例
.and(qm.address.contains("厦门"))
//equal示例
.and(qm.status.eq("0013"))
//between
.and(qm.age.between(20, 30)))
.fetch();
如果你觉得上面的写法不够优雅,我们可以使用QueryDSL提供的BooleanBuilder
来进行查询条件管理。
如下
BooleanBuilder builder = new BooleanBuilder();
//like
builder.and(qm.name.like('%'+"Jack"+'%'));
//contain
builder.and(qm.address.contains("厦门"));
//equal示例
builder.and(qm.status.eq("0013"));
//between
builder.and(qm.age.between(20, 30));
List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm).where(builder).fetch();
使用BooleanBuilder
,更复杂的查询关系也不怕。
例如
BooleanBuilder builder = new BooleanBuilder();
builder.and(qm.address.contains("厦门"));
BooleanBuilder builder2 = new BooleanBuilder();
builder2.or(qm.status.eq("0013"));
builder2.or(qm.status.eq("0014"));
builder.and(builder2);
List<MemberDomain> memberComplexConditionList = queryFactory.selectFrom(qm).where(builder).fetch();
1.2.3 多表查询
//以左关联为例-left join
QMemberDomain qm = QMemberDomain.memberDomain;
QFavoriteInfoDomain qf= QFavoriteInfoDomain.favoriteInfoDomain;
List<MemberDomain> leftJoinList = queryFactory.selectFrom(qm).leftJoin(qm.favoriteInfoDomains,qf).where(qf.favoriteStoreCode.eq("0721")).fetch();
1.2.4 使用Mysql聚合函数
//聚合函数-avg()
Double averageAge = queryFactory.select(qm.age.avg()).from(qm).fetchOne();
//聚合函数-concat()
String concat = queryFactory.select(qm.name.concat(qm.address)).from(qm).fetchOne();
//聚合函数-date_format()
String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).from(qm).fetchOne();
当用到DATE_FORMAT这类QueryDSL似乎没有提供支持的Mysql函数时,我们可以手动拼一个String表达式。这样就可以无缝使用Mysql中的函数了。
1.2.5 使用子查询
下面的用法中子查询没有什么实际意义,只是作为一个写法示例。
//子查询
List<MemberDomain> subList = queryFactory.selectFrom(qm).where(qm.status.in(JPAExpressions.select(qm.status).from(qm))).fetch();
1.2.6 排序
//排序
List<MemberDomain> orderList = queryFactory.selectFrom(qm).orderBy(qm.name.asc()).fetch();
1.2.7 分页的两种写法
QMemberDomain qm = QMemberDomain.memberDomain;
//写法一
JPAQuery<MemberDomain> query = queryFactory.selectFrom(qm).orderBy(qm.age.asc());
long total = query.fetchCount();//hfetchCount的时候上面的orderBy不会被执行
List<MemberDomain> list0= query.offset(2).limit(5).fetch();
//写法二
QueryResults<MemberDomain> results = queryFactory.selectFrom(qm).orderBy(qm.age.asc()).offset(2).limit(5).fetchResults();
List<MemberDomain> list = results.getResults();
logger.debug("total:"+results.getTotal());
logger.debug("limit:"+results.getLimit());
logger.debug("offset:"+results.getOffset());
写法一和二都会发出两条sql进行查询,一条查询count,一条查询具体数据。
写法二的getTotal()
等价于写法一的fetchCount
。
无论是哪种写法,在查询count的时候,orderBy、limit、offset这三个都不会被执行。可以大胆使用。
1.2.8 使用Template实现QueryDSL未支持的语法
其实Template我们在1.2.4 使用Mysql聚合函数
中已经使用过了。QueryDSL并没有对Mysql的所有函数提供支持,好在它给我们提供了Template特性。我们可以使用Template来实现各种QueryDSL未直接支持的语法。
示例如下。
QMemberDomain qm = QMemberDomain.memberDomain;
//使用booleanTemplate充当where子句或where子句的一部分
List<MemberDomain> list = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{} = \"tofu\"", qm.name)).fetch();
//上面的写法,当booleanTemplate中需要用到多个占位时
List<MemberDomain> list1 = queryFactory.selectFrom(qm).where(Expressions.booleanTemplate("{0} = \"tofu\" and {1} = \"Amoy\"", qm.name,qm.address)).fetch();
//使用stringTemplate充当查询语句的某一部分
String date = queryFactory.select(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate)).from(qm).fetchFirst();
//在where子句中使用stringTemplate
String id = queryFactory.select(qm.id).from(qm).where(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", qm.registerDate).eq("2018-03-19")).fetchFirst();
不过Template好用归好用,但也有其局限性。
例如当我们需要用到复杂的正则表达式匹配的时候,就有些捉襟见肘了。这是由于Template中使用了{}
来作为占位符,而正则表达式中也可能使用了{}
,因而会产生冲突。
2. QueryDslPredicateExecutor
我们通常使用Repository来继承QueryDslPredicateExecutor<T>
接口。通过注入Repository来使用。
继承
@Repository
public interface IMemberDomainRepository extends JpaRepository<MemberDomain,String>,QueryDslPredicateExecutor<MemberDomain> {
}
注入
@Autowired
IMemberDomainRepository memberRepo;
2.1 查询
简单查询
QMemberDomain qm = QMemberDomain.memberDomain;
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"));
也可以使用更优雅的BooleanBuilder
来进行条件分支管理
BooleanBuilder builder = new BooleanBuilder();
builder.and(qm.address.contains("厦门"));
builder.and(qm.status.eq("0013"));
Iterable<MemberDomain> iterable2 = memberRepo.findAll(builder);
QueryDslPredicateExecutor<T>
接口提供了findOne()
,findAll()
,count()
,exists()
四个方法来支持查询。
count()
会返回满足查询条件的数据行的数量,exists()
会根据所要查询的数据是否存在返回一个boolean值,都很简单,因此不再赘述。
下面着重进行介绍findOne()
和findAll()
两个关键查询方法。
2.1.1 findOne()
findOne,顾名思义,从数据库中查出一条数据。没有重载方法。
和JPAQuery
的fetchOne()
一样,当根据查询条件从数据库中查询到多条匹配数据时,会抛NonUniqueResultException
。使用的时候需要慎重。
2.1.2 findAll()
findAll是从数据库中查出匹配的所有数据。提供了以下几个重载方法。
- findAll(Predicate predicate)
- findAll(OrderSpecifier<?>... orders)
- findAll(Predicate predicate,OrderSpecifier<?>... orders)
- findAll(Predicate predicate,Sort sort)
第一个重载方法是不带排序的,第二个重载方法是只带QueryDSL提供的OrderSpecifier方式实现排序而不带查询条件的,而第三个方法则是既有条件又有排序的。
因此我们直接来看第三个方法的使用示例。
QMemberDomain qm = QMemberDomain.memberDomain;
OrderSpecifier<Integer> order = new OrderSpecifier<>(Order.DESC, qm.age);
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order);
除了QueryDSL提供的排序实现,我们还有支持Spring Data提供的Sort的第四个重载方法。示例如下
QMemberDomain qm = QMemberDomain.memberDomain;
Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC, "age"));
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"), sort);
三、使用心得
1. 查询条件中字段为String时关于null,empty,blank的表达
(如果你还不了解null,empty,blank的区别,请先自行搜索了解)
QueryDSL为String类型的字段提供了.isEmpty()
,isNull()
,.isNotEmpty()
,isNotNull()
这四个函数支持,唯独没有对blank提供支持。经过测试,我发现可以通过这种方式来实现对blank的使用:.eq("")
,.ne("")
四、参考
五、扩展阅读
以上。
希望我的文章对你能有所帮助。
我不能保证文中所有说法的百分百正确,
但我能保证它们都是我的理解和感悟以及拒绝直接复制黏贴(确实需要引用的部分我会附上源地址)。
有什么意见、见解或疑惑,欢迎留言讨论。
网友评论