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注解注册实例进行容器托管 */ @Configuration public class QueryDslConfig { @Bean public JPAQueryFactory jpaQueryFactory(EntityManager em){ return new JPAQueryFactory(em); } } /** * 方式二。在Dao类中初始化 */ // 实体管理 @Autowired private EntityManager entityManager; // 查询工厂 private JPAQueryFactory queryFactory; // 初始化JPA查询工厂 @PostConstruct // Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法) public void init(){ queryFactory = new JPAQueryFactory(entityManager); }
注入
@Autowired private JPAQueryFactory queryFactory;
更新、删除
JPAQueryFactory 更新
在Querydsl JPA中,更新语句是简单的 update-set/where-execute
形式。
execute()执行后返回的是被更新的实体的数量。
注意:使用QueryDsl更新实体时需要添加事务
@Test @Transactional public void testUpdate() { 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 @Transactional public void testDelete() { 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的Predicate T cases().when(Predicate).then(T a).otherwise(T b) DateExpression<Date> currentDate() // 返回当前日历(年-月-日)的 DateExpression TimeExpression<Time> currentTime() // 返回当前时刻(时:分:秒)的 TimeExpression DateTimeExpression<Date> currentTimestamp() // 返回当前时间(年-月-日 时:分:秒)的 DateTimeExpression // exprs 均为名为eqTrue的Predicate ,则返回名为eqTrue的Predicate,否则返回eqFalse的Predicate BooleanExpression allOf(BooleanExpression... exprs) // exprs 至少存在一个名为eqTrue的Predicate,则返回名为eqTrue的Predicate,否则返回eqFalse的Predicate BooleanExpression anyOf(BooleanExpression... exprs) // 转类型为 BooleanExpression。特别注意:asBoolean(Boolean).isTrue() 才是可用Predicate BooleanExpression asBoolean(Boolean) // asBoolean(true) <==等价==> booleanPath("true") NumberExpression asNumber(T) StringrExpression asString(T) DateExpression asDate(T) TimeExpression asTime(T) DateTimeExpression asDateTime(T) // 自定义语法 StringTemplate stringTemplate(String template, Object... args) NumberTemplate<T> numberTemplate(Class<? extends T> cl, String template, Object... args) BooleanTemplate booleanTemplate(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,否则返回null NumberExpression<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
// 给查询字段取别名 T as(alias) BooleanExpression eq(T right) // 等于 equal BooleanExpression eqAll(T... right) BooleanExpression eqAny(T... right) BooleanExpression ne(T right) // 不等于 not equal BooleanExpression neAll(T... right) BooleanExpression neAny(T... right) BooleanExpression in(T... right) BooleanExpression notIn(T... right) BooleanExpression isNotNull() BooleanExpression isNull() // 相当于java中的switch语句。两种写法 T when(A).then(B).otherwise(C) // 该字段的查询结果等于参数则返回null,不等于则返回查询结果。Field == A ? null : Field SimpleExpression<T> nullif(A) // 符合过滤条件的的总条数。 select count(table.id) from table NumberExpression<Long> count()
ComparableExpressionBase extends SimpleExpression
// 设置默认值。返回 Field, A, B ... 顺序中第一个非null的值,若都为null则返回null // 注意:使用该方法兜底Oracle数据库的null为空字符串时会失效,因为Oracle会把空字符串当作null T coalesce(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() // 返回指定数值列的相反数 StringExpression stringValue() // 返回字符串表达式 // 数据类型转换为数字类型。type为数字基本类型的包装类.class。实体类接收字段需与type的类型一致。 NumberExpression<T> castToNum(Class<A> type)
ComparableExpression extends ComparableExpressionBase
BooleanExpression lt(T right) // 小于 less than BooleanExpression ltAll(T... right) BooleanExpression ltAny(T... right) BooleanExpression gt(T right) // 大于 greater than BooleanExpression gtAll(T... right) BooleanExpression gtAny(T... right) BooleanExpression loe(T right) // 小于等于 less than or equal BooleanExpression loeAll(T... right) BooleanExpression loeAny(T... right) BooleanExpression goe(T right) // 大于等于 greater than or equal BooleanExpression goeAll(T... right) BooleanExpression goeAny(T... right) BooleanExpression between(from, to) // from和to之间 [from, to] BooleanExpression notBetween(from, to)
BooleanExpression 布尔表达式 extends LiteralExpression (extends ComparableExpression) implements Predicate
BooleanExpression isTrue() // 计算结果若为true,则返回名为eqTrue的Predicate,否则返回名为eqFalse的Predicate BooleanExpression isFalse() // 计算结果若为false,则返回名为eqTrue的Predicate,否则返回名为eqFalse的Predicate BooleanExpression not() // 返回相反的结果 BooleanExpression eq(Boolean right) BooleanExpression and(Predicate right) BooleanExpression andAnyOf(Predicate... predicates) BooleanExpression or(Predicate right) BooleanExpression orAllOf(Predicate... predicates)
StringExpressions 字符串表达式 extends LiteralExpression extends ComparableExpression
StringExpression contains(String str) // 包含参数字符串 BooleanExpression isEmpty() // 判断是否为空 BooleanExpression isNotEmpty() // 正则匹配查询 BooleanExpression matches(Expression<String> regex) // 模糊查询。% 为通配符,_ 表一个字符,可以传参escape指定转义字符 BooleanExpression like(String str) BooleanExpression like(String str, char escape) BooleanExpression endsWith(str) // 判断字符串的后缀是否为str。注意:必须使用boolean数据类型的字段接收 BooleanExpression startsWith(str) // 判断字符串的前缀是否为str。注意:必须使用boolean数据类型的字段接收 // 将字母转换大小写 StringExpression toLowerCase() StringExpression toUpperCase() StringExpression lower() StringExpression upper() StringExpression trim() // 去掉字符串两端的空格 StringExpression substring(int beginIndex) // 截取子字符串从索引位置至末尾 StringExpression concat(str) // 拼接 str StringExpression append(str) // 在末尾添加 str StringExpression prepend(str) // 在前面添加 str NumberExpression<Integer> length() // 返回字符串长度 NumberExpression<Integer> locate(str) // 返回 str 的位置(从1开始),没有返回0 NumberExpression<Integer> indexOf(str) // 返回 str 索引(从0开始),没有返回-1 SimpleExpression<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 = 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 builder2 = new BooleanBuilder(); 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 */ public List<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 -> { //创建商品dto GoodDTO dto = new GoodDTO(); //设置商品编号 dto.setId(tuple.get(goodBean.id)); //设置商品价格 dto.setPrice(tuple.get(goodBean.price)); //设置类型编号 dto.setTypeId(tuple.get(goodTypeBean.id)); //设置类型名称 dto.setTypeName(tuple.get(goodTypeBean.name)); //返回本次构建的dto return dto; }) //返回集合并且转换为List<GoodDTO> .collect(Collectors.toList()); }