0


JPA 之 QueryDSL-JPA 使用指南

Querydsl-JPA 框架(推荐)

官网:传送门

参考:

  • JPA整合Querydsl入门篇
  • SpringBoot环境下QueryDSL-JPA的入门及进阶

概述及依赖、插件、生成查询实体

1.Querydsl支持代码自动完成,因为是纯Java API编写查询,因此主流Java IDE对起的代码自动完成功能支持几乎可以发挥到极致(因为是纯Java代码,所以支持很好)

2.Querydsl几乎可以避免所有的SQL语法错误(当然用错了Querydsl API除外,因为不写SQL了,因此想用错也难)

3.Querydsl采用Domain类型的对象和属性来构建查询,因此查询绝对是类型安全的,不会因为条件类型而出现问题

4.Querydsl采用纯Java API的作为SQL构建的实现可以让代码重构发挥到另一个高度

5.Querydsl的领一个优势就是可以更轻松的进行增量查询的定义

使用

在Spring环境下,可以通过两种风格来使用QueryDSL。

一种是使用

JPAQueryFactory

的原生QueryDSL风格, 另一种是基于Spring Data提供的

QueryDslPredicateExecutor<T>

的Spring-data风格。

使用

QueryDslPredicateExecutor<T>

可以简化一些代码,使得查询更加优雅。 而

JPAQueryFactory

的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。

依赖

<dependencies><!-- QueryDSL框架依赖 --><dependency><groupId>com.querydsl</groupId><artifactId>querydsl-apt</artifactId></dependency><dependency><groupId>com.querydsl</groupId><artifactId>querydsl-jpa</artifactId></dependency></dependencies>

添加maven插件(pom.xml)

添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:“Q”+对应实体名)。

上文引入的依赖中

querydsl-apt

即是为此插件服务的。

注:在使用过程中,如果遇到query type无法自动生成的情况,用maven更新一下项目即可解决(右键项目->Maven->Update Project)。

<project><build><plugins><plugin><!--因为QueryDsl是类型安全的,所以还需要加上Maven APT plugin,使用 APT 自动生成Q类:--><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/java</outputDirectory><processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor></configuration></execution></executions></plugin></plugins></build></project>

补充:

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>

生成查询实体

idea 工具 为maven project 自动添加了对应的功能。添加好依赖和 plugin 插件后,就可以生成查询实体了。

打开右侧的 Maven Projects,如下图所示:

在这里插入图片描述

双击 clean 清除已编译的 target

双击 compile 命令执行,执行完成后会在 pom.xml 配置文件内配置生成目录内生成对应实体的 QueryDSL 查询实体。

生成的查询实体如下图所示:

在这里插入图片描述

JPAQueryFactory 风格

QueryDSL 在支持JPA的同时,也提供了对 Hibernate 的支持。可以通过

HibernateQueryFactory

来使用。

装配 与 注入

SpringBoot注解方式装配

/**
 * 方式一。使用Spring的@Configuration注解注册实例进行容器托管
 */@ConfigurationpublicclassQueryDslConfig{@BeanpublicJPAQueryFactoryjpaQueryFactory(EntityManager em){returnnewJPAQueryFactory(em);}}/**
 * 方式二。在Dao类中初始化
 */// 实体管理@AutowiredprivateEntityManager entityManager;// 查询工厂privateJPAQueryFactory queryFactory;// 初始化JPA查询工厂@PostConstruct// Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)publicvoidinit(){
        queryFactory =newJPAQueryFactory(entityManager);}

注入

@AutowiredprivateJPAQueryFactory queryFactory;

更新、删除

JPAQueryFactory 更新

在Querydsl JPA中,更新语句是简单的

update-set/where-execute

形式。

execute()执行后返回的是被更新的实体的数量。

注意:使用QueryDsl更新实体时需要添加事务

@Test@TransactionalpublicvoidtestUpdate(){QStudent qStudent =QStudent.student;Long result = queryFactory.update(qStudent).set(qStudent.name,"haha")// 可以用if条件判断更新值来确定字段是否.set().setnull(qStudent.age)// 设置null值.where(qStudent.id.eq(111L)).execute();assertThat(result,equalTo(1L));}

JPAQueryFactory 删除

删除语句是简单的

delete-where-execute

形式。

注意:使用QueryDsl删除实体时需要添加事务

@Test@TransactionalpublicvoidtestDelete(){QStudent qStudent =QStudent.student;//删除指定条件的记录Long result = queryFactory.delete(qStudent).where(qStudent.id.eq(111L)).execute();assertThat(result,equalTo(1L));//删除所有记录。即不加where条件Long totalResult = queryFactory.delete(qStudent).execute();System.out.println(totalResult);}

查询

表达式工具类

Expressions 表达式工具类

// when-then 条件表达式函数。when传参必须为名为eqTrue或eqFalse的PredicateTcases().when(Predicate).then(T a).otherwise(T b)DateExpression<Date>currentDate()// 返回当前日历(年-月-日)的 DateExpressionTimeExpression<Time>currentTime()// 返回当前时刻(时:分:秒)的 TimeExpressionDateTimeExpression<Date>currentTimestamp()// 返回当前时间(年-月-日 时:分:秒)的 DateTimeExpression// exprs 均为名为eqTrue的Predicate ,则返回名为eqTrue的Predicate,否则返回eqFalse的PredicateBooleanExpressionallOf(BooleanExpression... exprs)// exprs 至少存在一个名为eqTrue的Predicate,则返回名为eqTrue的Predicate,否则返回eqFalse的PredicateBooleanExpressionanyOf(BooleanExpression... exprs)// 转类型为 BooleanExpression。特别注意:asBoolean(Boolean).isTrue() 才是可用PredicateBooleanExpressionasBoolean(Boolean)// asBoolean(true)  <==等价==>  booleanPath("true")NumberExpressionasNumber(T)StringrExpressionasString(T)DateExpressionasDate(T)TimeExpressionasTime(T)DateTimeExpressionasDateTime(T)// 自定义语法StringTemplatestringTemplate(String template,Object... args)NumberTemplate<T>numberTemplate(Class<?extendsT> cl,String template,Object... args)BooleanTemplatebooleanTemplate(String template,ImmutableList<?> args)

MathExpressions 数学表达式工具类

NumberExpression<A>round(Expression<A> num)// 四舍五入取整NumberExpression<A>round(Expression<A> num,int s)// 四舍五入保留 s 位小数NumberExpression<Double>asin(Expression<A> num)// 返回num的反正弦值。-1 <= num <= 1,否则返回nullNumberExpression<Double>acos(Expression<A> num)// 返回num的反余弦值。-1 <= num <= 1,否则返回null// 慎用!qdsl-jpa底层是调用random()函数,MySQL没有该函数,只有rand()函数,会报错,解决方案为使用QDSL-SQL查询NumberExpression<Double>random()// 返回0到1内的随机值NumberExpression<Double>random(int seed)// 返回一个指定的0到1内的随机值

表达式方法

注意:在select()中查询出的结果使用表达式方法处理过后,若要封装到实体类中,则都需要使用 .as(alias) 起别名指定封装到实体类中的哪个字段。

SimpleExpression 简单表达式 extends DslExpression extends Expression

// 给查询字段取别名Tas(alias)BooleanExpressioneq(T right)// 等于 equalBooleanExpressioneqAll(T... right)BooleanExpressioneqAny(T... right)BooleanExpressionne(T right)// 不等于    not equal BooleanExpressionneAll(T... right)BooleanExpressionneAny(T... right)BooleanExpressionin(T... right)BooleanExpressionnotIn(T... right)BooleanExpressionisNotNull()BooleanExpressionisNull()// 相当于java中的switch语句。两种写法Twhen(A).then(B).otherwise(C)// 该字段的查询结果等于参数则返回null,不等于则返回查询结果。Field == A ? null : FieldSimpleExpression<T>nullif(A)// 符合过滤条件的的总条数。        select count(table.id) from tableNumberExpression<Long>count()

ComparableExpressionBase extends SimpleExpression

// 设置默认值。返回 Field, A, B ... 顺序中第一个非null的值,若都为null则返回null// 注意:使用该方法兜底Oracle数据库的null为空字符串时会失效,因为Oracle会把空字符串当作nullTcoalesce(A,B...)

NumberExpression 数值表达式 extends ComparableExpressionBase

NumberExpression<T>add(A)// 加    NumberExpression<T>subtract(A)// 减  NumberExpression<T>multiply(A)// 乘  NumberExpression<T>divide(A)// 除  NumberExpression<T>mod(A)// 返回余数NumberExpression<T>floor()// 向下取整NumberExpression<T>ceil()// 向上取整NumberExpression<T>round()// 四舍五入取整NumberExpression<T>max()// 返回指定数值列的最大值NumberExpression<T>min()// 返回指定数值列的最小值NumberExpression<T>sqrt()// 返回指定数值列的平方根NumberExpression<T>sum()// 返回指定数值列(或分组相同数值列)的总数NumberExpression<T>avg()// 返回指定数值列(或分组相同数值列)的平均数NumberExpression<T>abs()// 返回指定数值列的绝对值NumberExpression<T>negate()// 返回指定数值列的相反数StringExpressionstringValue()// 返回字符串表达式// 数据类型转换为数字类型。type为数字基本类型的包装类.class。实体类接收字段需与type的类型一致。NumberExpression<T>castToNum(Class<A> type)

ComparableExpression extends ComparableExpressionBase

BooleanExpressionlt(T right)// 小于 less thanBooleanExpressionltAll(T... right)BooleanExpressionltAny(T... right)BooleanExpressiongt(T right)// 大于 greater thanBooleanExpressiongtAll(T... right)BooleanExpressiongtAny(T... right)BooleanExpressionloe(T right)// 小于等于 less than or equalBooleanExpressionloeAll(T... right)BooleanExpressionloeAny(T... right)BooleanExpressiongoe(T right)// 大于等于 greater than or equalBooleanExpressiongoeAll(T... right)BooleanExpressiongoeAny(T... right)BooleanExpressionbetween(from,to)// from和to之间  [from, to]BooleanExpressionnotBetween(from,to)

BooleanExpression 布尔表达式 extends LiteralExpression (extends ComparableExpression) implements Predicate

BooleanExpressionisTrue()// 计算结果若为true,则返回名为eqTrue的Predicate,否则返回名为eqFalse的PredicateBooleanExpressionisFalse()// 计算结果若为false,则返回名为eqTrue的Predicate,否则返回名为eqFalse的PredicateBooleanExpressionnot()// 返回相反的结果BooleanExpressioneq(Boolean right)BooleanExpressionand(Predicate right)BooleanExpressionandAnyOf(Predicate... predicates)BooleanExpressionor(Predicate right)BooleanExpressionorAllOf(Predicate... predicates)

StringExpressions 字符串表达式 extends LiteralExpression extends ComparableExpression

StringExpressioncontains(String str)// 包含参数字符串BooleanExpressionisEmpty()// 判断是否为空BooleanExpressionisNotEmpty()// 正则匹配查询BooleanExpressionmatches(Expression<String> regex)// 模糊查询。% 为通配符,_ 表一个字符,可以传参escape指定转义字符BooleanExpressionlike(String str)BooleanExpressionlike(String str,char escape)BooleanExpressionendsWith(str)// 判断字符串的后缀是否为str。注意:必须使用boolean数据类型的字段接收BooleanExpressionstartsWith(str)// 判断字符串的前缀是否为str。注意:必须使用boolean数据类型的字段接收// 将字母转换大小写StringExpressiontoLowerCase()StringExpressiontoUpperCase()StringExpressionlower()StringExpressionupper()StringExpressiontrim()// 去掉字符串两端的空格StringExpressionsubstring(int beginIndex)// 截取子字符串从索引位置至末尾StringExpressionconcat(str)// 拼接 strStringExpressionappend(str)// 在末尾添加 strStringExpressionprepend(str)// 在前面添加 strNumberExpression<Integer>length()// 返回字符串长度NumberExpression<Integer>locate(str)// 返回 str 的位置(从1开始),没有返回0NumberExpression<Integer>indexOf(str)// 返回 str 索引(从0开始),没有返回-1SimpleExpression<Character>charAt(int i)// 返回参数索引位置的字符。实体类接收字段需为char或CharacterSS

select() 和 fetch() 的常用写法

注意:使用fetch()查询时,数据库没有符合该条件的数据时,返回的是空集合,而不是null。

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.bean(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();

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 =newBooleanBuilder();// 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 builder2 =newBooleanBuilder();
builder2.or(qm.status.eq("0013"));
builder2.or(qm.status.eq("0014"));
builder.and(builder2);List<MemberDomain> memberConditionList = queryFactory.selectFrom(qm).where(builder2).fetch();

自定义封装查询的结果集

方法一:使用Projections的Bean方法

JPAQueryFactory查询工厂的select方法可以将Projections方法返回的QBean作为参数,通过Projections的bean方法来构建返回的结果集映射到实体内,有点像Mybatis内的ResultMap的形式,不过内部处理机制肯定是有着巨大差别的!

bean方法第一个参数需要传递一个实体的泛型类型作为返回集合内的单个对象类型,如果QueryDSL查询实体内的字段与DTO实体的字段名字不一样时,可以采用as方法来处理,为查询的结果集指定的字段添加别名,这样就会自动映射到DTO实体内。

return queryFactory
            .select(Projections.bean(PersonIDCardDto.class,QIDCard.iDCard.idNo,QPerson.person.address,QPerson.person.name.as("userName")))// 使用别名对应dto内的userName.from(QIDCard.iDCard,QPerson.person).where(predicate).fetch();

底层原理:

  1. 使用数据封装类的无参构造方法创建对象(如果类上有使用@Builder注解导致@Data无参构造方法被覆盖,则会报错,可以再加上 @AllArgsConstructor,@NoArgsConstructor 注解)
  2. 使用setter方法封装数据给字段(会校验数据封装字段和Entity对应字段的数据类型是否一致,不一致则会报错)

常见问题:

  • Entity中时间字段的数据类型为 java.util.Date,数据封装类中时间字段的数据类型为 java.sql.Date 或具有指定时间格式的String类型,数据类型不一致,导致数据无法封装成功 - 方案1:修改数据封装类或Entity中时间的数据类型,使其类型一致- 方案2:数据封装类中新增util.Date类型字段,手动重写其setter方法,处理数据后赋值到原sql.Date类型字段上。注意:查询封装数据到字段时 as(“新增的util.Date字段”)- 方案3:数据封装类中新增util.Date类型字段,先将数据封装到该字段,再通过getter、setter方法处理数据后赋值到原sql.Date类型字段上。

方法二:使用Projections的fields方法

return queryFactory
            .select(Projections.fields(PersonIDCardDto.class,QIDCard.iDCard.idNo,QPerson.person.address,QPerson.person.name)).from(QIDCard.iDCard,QPerson.person).where(predicate).fetch();

方法三:使用Projections的constructor方法,注意构造方法中参数的顺序

return queryFactory
            .select(Projections.constructor(PersonIDCardDto.class,QPerson.person.name,QPerson.person.address,QIDCard.iDCard.idNo)).from(QIDCard.iDCard,QPerson.person).where(predicate).fetch();

方式四:使用集合的stream转换

从方法开始到fetch()结束完全跟QueryDSL没有任何区别,采用了最原始的方式进行返回结果集,但是从fetch()获取到结果集后处理的方式就有所改变了。

fetch()方法返回的类型是泛型List(List),List继承了Collection,完全存在使用Collection内非私有方法的权限,通过调用stream方法可以将集合转换成Stream泛型对象,该对象的map方法可以操作集合内单个对象的转换,具体的转换代码可以根据业务逻辑进行编写。

在map方法内有个lambda表达式参数tuple,通过tuple对象get方法就可以获取对应select方法内的查询字段。

注意:tuple只能获取select内存在的字段,如果select内为一个实体对象,tuple无法获取指定字段的值。

/**
     * 使用java8新特性Collection内stream方法转换dto
     */publicList<GoodDTO>selectWithStream(){//商品基本信息QGoodInfoBean goodBean =QGoodInfoBean.goodInfoBean;//商品类型QGoodTypeBean goodTypeBean =QGoodTypeBean.goodTypeBean;return queryFactory
                    .select(
                        goodBean.id,
                        goodBean.price,
                        goodTypeBean.name,
                        goodTypeBean.id).from(goodBean,goodTypeBean)//构建两表笛卡尔集.where(goodBean.typeId.eq(goodTypeBean.id))//关联两表.orderBy(goodBean.order.desc())//倒序.fetch().stream()//转换集合内的数据.map(tuple ->{//创建商品dtoGoodDTO dto =newGoodDTO();//设置商品编号
                        dto.setId(tuple.get(goodBean.id));//设置商品价格
                        dto.setPrice(tuple.get(goodBean.price));//设置类型编号
                        dto.setTypeId(tuple.get(goodTypeBean.id));//设置类型名称
                        dto.setTypeName(tuple.get(goodTypeBean.name));//返回本次构建的dtoreturn dto;})//返回集合并且转换为List<GoodDTO>.collect(Collectors.toList());}

排序、分页

排序

.asc()// 升序.desc()// 降序.asc().nullsFirst()// 升序,空值放前面.asc().nullsLast()// 降序,空值放前面
//排序List<MemberDomain> orderList = queryFactory.selectFrom(qm).orderBy(qm.name.asc()).fetch();

分页

.limit(long limit)// 限制查询结果返回的数量。即一页多少条记录(pageSize).offset(long offset)// 跳过多少行。offset = ( pageNum - 1 ) * pageSize        // pageNum:第几页
QMemberDomain qm =QMemberDomain.memberDomain;//写法一JPAQuery<MemberDomain> query = queryFactory
                                        .selectFrom(qm).orderBy(qm.age.asc());// 查询总条数。fetchCount时,orderBy不会被执行long total = query.fetchCount();// 获取过滤后的查询结果集List<MemberDomain> list0= query.offset(2).limit(5).fetch();//写法二。fetchResults()自动实现count查询和结果查询,并封装到QueryResults<T>中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("offset:"+results.getOffset());// 跳过多少条符合过滤条件的查询结果
    logger.debug("limit:"+results.getLimit());// 限制查询结果返回的条数

写法一和二都会发出两条sql进行查询,一条查询count,一条查询具体数据。

写法二的

getTotal()

等价于写法一的

fetchCount

无论是哪种写法,在查询count的时候,orderBy、limit、offset这三个都不会被执行。可以大胆使用。

子查询

// 子查询作为where条件内容@TestpublicvoidselectJPAExpressions(){List<MemberDomain> subList = queryFactory
                                            .selectFrom(qm).where(qm.status.in(JPAExpressions.select(qm.status).from(qm))).fetch();}// 子查询作为select查询字段@TestpublicvoidselectJPAExpressions(){QUserAddress ua =QUserAddress.userAddress;QUser u =QUser.user;List<UserAddressDTO> list = queryFactory
            .select(Projections.bean(UserAddressDTO.class, ua.addressee
                    ,Expressions.asNumber(JPAExpressions.select(u.id.count()).from(u).where(u.id.ne(ua.userId))).longValue()// asNumber接收子查询结果后需要指定数值的数据类型.as("lon")//                    , Expressions.asString(    // asString接收子查询结果后不用指定数据类型//                        JPAExpressions.//                            select(u.username)//                            .from(u)//                            .where(u.id.eq(ua.userId))//                    )//                        .as("password"))).from(ua).where(ua.id.eq(38)).fetch();}

联表动态查询

// JPA查询工厂@AutowiredprivateJPAQueryFactory queryFactory;/**
     * 关联查询示例,查询出城市和对应的旅店
     */@TestpublicvoidfindCityAndHotel(){QTCity qtCity =QTCity.tCity;QTHotel qtHotel =QTHotel.tHotel;JPAQuery<Tuple> jpaQuery = queryFactory
            .select(qtCity, qtHotel).from(qtCity).leftJoin(qtHotel).on(qtHotel.city.longValue().eq(qtCity.id.longValue()));// 分离式 添加查询条件
        jpaQuery.where(QTCity.tCity.name.like("shanghai"));// 获取查询结果List<Tuple> result = jpaQuery.fetch();// 对多元组取出数据,这个和select时的数据相匹配for(Tuple row : result){System.out.println("qtCity:"+ row.get(qtCity));System.out.println("qtHotel:"+ row.get(qtHotel));System.out.println("--------------------");}}

联表一对多查询封装

方式一:查询结果返回类型为List

List<UserAddressDTO> list = queryFactory
                .from(u).join(ua).on(ua.userId.eq(u.id)).where(u.id.eq(31)).transform(GroupBy.groupBy(u.id).list(Projections.bean(UserAddressDTO.class,
                            u.id, 
                            u.username,GroupBy.list(Projections.bean(UserAddress.class,
                                    ua.address, 
                                    ua.city, 
                                    ua.district
                            )).as("userAddresses"))));

方式二:查询结果返回类型为Map

map的key为分组字段,一般为主键ID

Map<Integer,UserAddressDTO> map = queryFactory
                .from(u).join(ua).on(ua.userId.eq(u.id)).where(u.id.eq(31)).transform(GroupBy.groupBy(u.id).as(Projections.bean(UserAddressDTO.class,
                            u.id, 
                            u.username,GroupBy.list(Projections.bean(UserAddress.class,
                                ua.address, 
                                ua.city, 
                                ua.district
                            )).as("userAddresses"))));

实体类:

@Data@Builder@AllArgsConstructor@NoArgsConstructorpublicclassUserAddressDTO{privateInteger id;privateString username;privateString password;privateString phone;privateList<UserAddress> userAddresses;}

使用聚合函数

//聚合函数-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中的函数了。

使用 Template 实现自定义语法

QueryDSL并没有对数据库的所有函数提供支持,好在它提供了Template特性。

可以使用Template来实现各种QueryDSL未直接支持的语法。

Template的局限性:

  由于Template中使用了

{}

来作为占位符(内部序号从0开始),而正则表达式中也可能使用了

{}

,因而会产生冲突。

QMemberDomain qm =QMemberDomain.memberDomain;//使用booleanTemplate充当where子句或where子句的一部分List<MemberDomain> list = queryFactory
                                .selectFrom(qm).where(Expressions.booleanTemplate("{0} = \"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子句中使用stringTemplateString id = queryFactory
                .select(qm.id).from(qm).where(Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')", 
                    qm.registerDate).eq("2018-03-19")).fetchFirst();

QueryDslPredicateExecutor 风格

通常使用Repository来继承

QueryDslPredicateExecutor<T>

接口。通过注入Repository来使用。

Repository 接口

Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作。

publicinterface tityRepository extendsJpaRepository<City,Integer>,QuerydslPredicateExecutor<city>{}
QueryDslPredicateExecutor<T>

接口提供了

findOne()

findAll()

count()

exists()

四个方法来支持查询。并可以使用更优雅的

BooleanBuilder

来进行条件分支管理。

  • count()会返回满足查询条件的数据行的数量
  • exists()会根据所要查询的数据是否存在返回一个boolean值

findOne()、findAll()

findOne

从数据库中查出一条数据。没有重载方法。

Optional<T>findOne(Predicate var1);

JPAQuery

fetchOne()

一样,当根据查询条件从数据库中查询到多条匹配数据时,会抛

NonUniqueResultException

。使用的时候需要慎重。

findAll()

findAll是从数据库中查出匹配的所有数据。提供了以下几个重载方法。

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);

使用示例:

QMemberDomain qm =QMemberDomain.memberDomain;// QueryDSL 提供的排序实现OrderSpecifier<Integer> order =newOrderSpecifier<>(Order.DESC, qm.age);Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order);QMemberDomain qm =QMemberDomain.memberDomain;// Spring Data 提供的排序实现Sort sort =newSort(newSort.Order(Sort.Direction.ASC,"age"));Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"), sort);

单表动态分页查询

单表动态查询示例:

//动态条件QTCity qtCity =QTCity.tCity;//SDL实体类//该Predicate为querydsl下的类,支持嵌套组装复杂查询条件Predicate predicate = qtCity.id.longValue().lt(3).and(qtCity.name.like("shanghai"));//分页排序Sort sort =newSort(newSort.Order(Sort.Direction.ASC,"id"));PageRequest pageRequest =newPageRequest(0,10,sort);//查找结果Page<TCity> tCityPage = tCityRepository.findAll(predicate, pageRequest);

Querydsl SQL 查询

Querydsl SQL 模块提供与 JDBC API 的集成。可以使用更多的 JDBC SQL方法。比如可以实现 from 的查询主体为子查询出来的临时表unionunion All 等Querydsl-JPA限制的相关操作。还可以根据 JDBC API 获取数据库的类型使用不同的数据库语法模板。

依赖及配置

依赖:

<dependency><groupId>com.querydsl</groupId><artifactId>querydsl-sql</artifactId><version>${querydsl.version}</version></dependency><!-- joda-time为querydsl-sql中必需的新版本的时间日期处理库 --><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.10.5</version></dependency>

yaml配置:

logging:level:com.querydsl.sql: debug        # 打印日志

SQLQuery 的 Q 类

需要自己创建编写(可以基于apt 插件生成的 JPA 的Q类改造),并放到主目录(src)启动类下的包里。

  • 使用 extends RelationalPathBase<Entity> 的Q类。推荐需要将数据库表名传入构造方法的table参数里,path 可以传别名,所有的property参数为实体类的属性名(驼峰命名),addMetadata()ColumnMetadata.named("FeildNmae")FeildNmae 为数据库字段名。使用该Q类查询所有字段数据时(即select(Q类))可以自动映射封装结果集。
  • 使用extends EntityPathBase<Entity>的Q类。需要将传入构造方法的variable参数改成数据库表名,并且将所有的property参数改成相对应的数据库字段名注意:使用 extends EntityPathBase<Entity> 的实体Q类,直接 select(Q类) 会报错,无法自动映射封装结果集,需要使用Projections.bean(Entity.class,Expression<?>... exprs) 手动封装结果集。
/**
 * extends RelationalPathBase<Entity> 的Q类示例
 */publicclassQEmployeeextendsRelationalPathBase<Employee>{privatestaticfinallong serialVersionUID =1394463749655231079L;publicstaticfinalQEmployee employee =newQEmployee("EMPLOYEE");publicfinalNumberPath<Integer> id =createNumber("id",Integer.class);publicfinalStringPath firstname =createString("firstname");publicfinalDatePath<java.util.Date> datefield =createDate("datefield",java.util.Date.class);publicfinalPrimaryKey<Employee> idKey =createPrimaryKey(id);publicQEmployee(String path){super(Employee.class,PathMetadataFactory.forVariable(path),"PUBLIC","EMPLOYEE");addMetadata();}publicQEmployee(PathMetadata metadata){super(Employee.class, metadata,"PUBLIC","EMPLOYEE");addMetadata();}protectedvoidaddMetadata(){addMetadata(id,ColumnMetadata.named("ID").ofType(Types.INTEGER));addMetadata(firstname,ColumnMetadata.named("FIRSTNAME").ofType(Types.VARCHAR));addMetadata(datefield,ColumnMetadata.named("DATEFIELD").ofType(Types.DATE));}}
/**
 * extends EntityPathBase<Entity> 的Q类示例
 */publicclassQUserAddressSextendsEntityPathBase<UserAddress>{privatestaticfinallong serialVersionUID =-1295712525L;publicstaticfinalQUserAddressS userAddress =newQUserAddressS("tb_user_address");publicfinalNumberPath<Integer> id =createNumber("id",Integer.class);publicfinalStringPath address =createString("address");publicfinalDateTimePath<java.util.Date> createTime =createDateTime("create_time",java.util.Date.class);publicQUserAddressS(String variable){super(UserAddress.class,forVariable(variable));}publicQUserAddressS(Path<?extendsUserAddress> path){super(path.getType(), path.getMetadata());}publicQUserAddressS(PathMetadata metadata){super(UserAddress.class, metadata);}}

SQLQueryFactory 方式

装配及基本使用

装配

@Configuration@Slf4jpublicclassQueryDslConfig{@BeanpublicSQLQueryFactorysqlQueryFactory(DataSource druidDataSource){SQLTemplates t;try(Connection connection = druidDataSource.getConnection()){
            t =newSQLTemplatesRegistry().getTemplates(connection.getMetaData());}catch(Exception e){
            log.error("", e);
            t =SQLTemplates.DEFAULT;}com.querydsl.sql.Configuration configuration =newcom.querydsl.sql.Configuration(t);
        configuration.addListener(newSQLBaseListener(){@Overridepublicvoidend(SQLListenerContext context){if(context !=null&&!DataSourceUtils.isConnectionTransactional(context.getConnection(), druidDataSource)){// 若非事务连接SQLCloseListener.DEFAULT.end(context);}}});
        configuration.setExceptionTranslator(newSpringExceptionTranslator());// 创建SQLQueryFactory,且数据库连接由spring管理returnnewSQLQueryFactory(configuration,()->DataSourceUtils.getConnection(druidDataSource));}}

注入

@AutowiredprivateSQLQueryFactory sqlQueryFactory;

SQLQueryFactory 基本使用

/**
     * 子查询作为临时表传入from()中
     */@TestpublicvoidselectBySqlQueryFactory(){// 使用 extends RelationalPathBase<Entity> 的QEntity,自动映射封装QUserAddressSql uaSql =QUserAddressSql.userAddress;// 子查询SQLQuery<Tuple> q =SQLExpressions.select(// 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致
                uaSql.addressee
                , uaSql.userId
            ).from(uaSql);List<Tuple> fetch = sqlQueryFactory
            .select(// 查询字段须是临时表中的字段别名,且类型一致Expressions.template(String.class,"q.addressee").as("addressee"),Expressions.numberTemplate(Integer.class,"q.user_id").as("userId")).from(q,Expressions.stringPath("q"))// 子查询作为临时表.fetch();System.out.println(fetch);}/**
     * 子查询结果集 union
     */@TestpublicvoidselectBySqlQueryFactory(){// 使用 extends EntityPathBase<Entity> 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收QUserAddressSql uaSql =QUserAddressSql.userAddress;QUserSql uSql =QUserSql.user;SQLQuery<Tuple> a =SQLExpressions.select(uaSql.userId.as("user_id"), uaSql.phone).from(uaSql).where(uaSql.userId.eq(30));SQLQuery<Tuple> b =SQLExpressions.select(uSql.id.as("user_id"), uSql.phone).from(uSql).where(uSql.id.eq(29).or(uSql.id.eq(30)));Union<Tuple> union = sqlQueryFactory.query().union(a, b);long count = sqlQueryFactory
            .from(union,Expressions.stringPath("q")).fetchCount();List<UserAddressDTO> list = sqlQueryFactory
            .from(union,Expressions.stringPath("q")).orderBy(Expressions.numberPath(Integer.class,"user_id").desc(),Expressions.stringTemplate("phone").desc()).offset(0).limit(5).transform(GroupBy.groupBy(Expressions.template(String.class,"q.user_id")).list(Projections.bean(UserAddressDTO.class,Expressions.template(Integer.class,"q.user_id").as("userId"),GroupBy.list(Projections.bean(UserAddress.class,Expressions.stringTemplate("q.phone").as("phone"))).as("userAddresses"))));System.out.println(count);
        list.forEach(s ->System.out.println(JSON.toJSONString(s)));}

SQLExpression 表达式工具类

// 合并多张表记录。union为去重合并,unionAll为不去重合并static<T>Union<T>union(SubQueryExpression<T>... sq)static<T>Union<T>union(List<SubQueryExpression<T>> sq)static<T>Union<T>unionAll(SubQueryExpression<T>... sq)static<T>Union<T>unionAll(List<SubQueryExpression<T>> sq)// 调用函数查询序列staticSimpleExpression<Long>nextval(String sequence)static<TextendsNumber>SimpleExpression<T>nextval(Class<T> type,String sequence)// 使用示例:SQL写法:select seq_process_no.nextval from dual;Long nextvalReturn = sqlQueryFactory.select(SQLExpressions.nextval("序列名")).fetchOne;// 将多列记录聚合为一列记录。delimiter为分隔符。Oracle数据库专属,其他数据库报错staticWithinGroup<Object>listagg(Expression<?> expr,String delimiter)// 使用示例:SQLExpression.listagg(qEntity.name,",").withinGroup.OrderBy(qEntity.name.asc()).getValue.as("Name")// 将多列记录聚合为一列记录。separator为分隔符。MySQL、PostgreSQL都可用,PostgreSQL会根据模板翻译成String_agg函数staticStringExpressiongroupConcat(Expression<String> expr,String separator)staticStringExpressiongroupConcat(Expression<String> expr)static<T>RelationalFunctionCall<T>relationalFunctionCall(Class<?extendsT> type,String function,Object... args)static<DextendsComparable>DateExpression<D>date(DateTimeExpression<D> dateTime)static<DextendsComparable>DateExpression<D>date(Class<D> type,DateTimeExpression<?> dateTime)static<DextendsComparable>DateTimeExpression<D>dateadd(DatePart unit,DateTimeExpression<D> date,int amount)static<DextendsComparable>DateExpression<D>dateadd(DatePart unit,DateExpression<D> date,int amount)// 获取两个日期的时间间隔(end-start)static<DextendsComparable>NumberExpression<Integer>datediff(DatePart unit,DateTimeExpression<D> start,DateTimeExpression<D> end)

JPASQLQuery 方式

使用 JPASQLQuery 作为查询引擎时,使用的

QEntity(extends EntityPathBase<Entity>)

,传入构造方法的

variable

参数可以不为数据库表名(因为 JPASQLQuery 可以找到映射的真实表名,仅把此参数作为表别名),但所有的

property

参数仍必需为相对应的数据库字段名

故并不能直接使用 apt 插件生成 的 jpa 使用的

Q类

,仍需要使用改造版的

Q类(extends EntityPathBase<Entity>)

@TestpublicvoidselectBySqlQueryFactory(){// 使用 extends EntityPathBase<Entity> 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收QUserAddress ua =QUserAddress.userAddress;// jpa+sql的查询工具,本例使用的oracle的sql模板JPASQLQuery<?> jpasqlQuery =newJPASQLQuery<Void>(em,newOracleTemplates());// 子查询SQLQuery<Tuple> q =SQLExpressions.select(// 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致。如直接不使用QEntity的属性,则需手动指定Expressions.stringPath("addressee").as("addressee"),Expressions.numberPath(Integer.class,"user_id").as("user_id")).from(ua);List<Tuple> fetch = jpasqlQuery
            .select(// 查询字段须是临时表中的字段名或别名,且类型一致。结果集字段需添加别名手动映射封装Expressions.template(String.class,"q.addressee").as("addressee"),Expressions.numberTemplate(Integer.class,"q.user_id").as("userId")).from(q,Expressions.stringPath("q"))// 子查询作为临时表.fetch();System.out.println(fetch);}
标签: java mybatis spring

本文转载自: https://blog.csdn.net/footless_bird/article/details/129359394
版权归原作者 墨鸦_Cormorant 所有, 如有侵权,请联系我们删除。

“JPA 之 QueryDSL-JPA 使用指南”的评论:

还没有评论