Querydsl-JPA 框架(推荐)
官网:http://www.querydsl.com/static/querydsl/4.1.3/reference/html_single/
参考:
概述及依赖、插件、生成查询实体
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();
底层原理:
- 使用数据封装类的无参构造方法创建对象(如果类上有使用@Builder注解导致@Data无参构造方法被覆盖,则会报错,可以再加上 @AllArgsConstructor,@NoArgsConstructor 注解)
- 使用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());
}
排序、分页
排序
.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条件内容
@Test
public void selectJPAExpressions() {
List<MemberDomain> subList = queryFactory
.selectFrom(qm)
.where(qm.status.in(
JPAExpressions.select(qm.status).from(qm)))
.fetch();
}
// 子查询作为select查询字段
@Test
public void selectJPAExpressions() {
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查询工厂
@Autowired
private JPAQueryFactory queryFactory;
/**
* 关联查询示例,查询出城市和对应的旅店
*/
@Test
public void findCityAndHotel() {
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
@NoArgsConstructor
public class UserAddressDTO {
private Integer id;
private String username;
private String password;
private String phone;
private List<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子句中使用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();
QueryDslPredicateExecutor 风格
通常使用Repository来继承QueryDslPredicateExecutor<T>
接口。通过注入Repository来使用。
Repository 接口
Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作。
public interface tityRepository extends JpaRepository<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 = new OrderSpecifier<>(Order.DESC, qm.age);
Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order);
QMemberDomain qm = QMemberDomain.memberDomain;
// Spring Data 提供的排序实现
Sort sort = new Sort(new Sort.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 = new Sort(new Sort.Order(Sort.Direction.ASC,"id"));
PageRequest pageRequest = new PageRequest(0,10,sort);
//查找结果
Page<TCity> tCityPage = tCityRepository.findAll(predicate, pageRequest);
Querydsl SQL 查询
Querydsl SQL 模块提供与 JDBC API 的集成。可以使用更多的 JDBC SQL方法。比如可以实现 from 的查询主体为子查询出来的临时表、union、union 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类示例
*/
public class QEmployee extends RelationalPathBase<Employee> {
private static final long serialVersionUID = 1394463749655231079L;
public static final QEmployee employee = new QEmployee("EMPLOYEE");
public final NumberPath<Integer> id = createNumber("id", Integer.class);
public final StringPath firstname = createString("firstname");
public final DatePath<java.util.Date> datefield = createDate("datefield", java.util.Date.class);
public final PrimaryKey<Employee> idKey = createPrimaryKey(id);
public QEmployee(String path) {
super(Employee.class, PathMetadataFactory.forVariable(path), "PUBLIC", "EMPLOYEE");
addMetadata();
}
public QEmployee(PathMetadata metadata) {
super(Employee.class, metadata, "PUBLIC", "EMPLOYEE");
addMetadata();
}
protected void addMetadata() {
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类示例
*/
public class QUserAddressS extends EntityPathBase<UserAddress> {
private static final long serialVersionUID = -1295712525L;
public static final QUserAddressS userAddress = new QUserAddressS("tb_user_address");
public final NumberPath<Integer> id = createNumber("id", Integer.class);
public final StringPath address = createString("address");
public final DateTimePath<java.util.Date> createTime = createDateTime("create_time", java.util.Date.class);
public QUserAddressS(String variable) {
super(UserAddress.class, forVariable(variable));
}
public QUserAddressS(Path<? extends UserAddress> path) {
super(path.getType(), path.getMetadata());
}
public QUserAddressS(PathMetadata metadata) {
super(UserAddress.class, metadata);
}
}
SQLQueryFactory 方式
装配及基本使用
装配
@Configuration
@Slf4j
public class QueryDslConfig {
@Bean
public SQLQueryFactory sqlQueryFactory(DataSource druidDataSource){
SQLTemplates t;
try(Connection connection = druidDataSource.getConnection()){
t = new SQLTemplatesRegistry().getTemplates(connection.getMetaData());
}catch (Exception e){
log.error("", e);
t = SQLTemplates.DEFAULT;
}
com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(t);
configuration.addListener(new SQLBaseListener(){
@Override
public void end(SQLListenerContext context) {
if (context != null && !DataSourceUtils.isConnectionTransactional(context.getConnection(), druidDataSource)){
// 若非事务连接
SQLCloseListener.DEFAULT.end(context);
}
}
});
configuration.setExceptionTranslator(new SpringExceptionTranslator());
// 创建SQLQueryFactory,且数据库连接由spring管理
return new SQLQueryFactory(configuration, () -> DataSourceUtils.getConnection(druidDataSource));
}
}
注入
@Autowired
private SQLQueryFactory sqlQueryFactory;
SQLQueryFactory 基本使用
/**
* 子查询作为临时表传入from()中
*/
@Test
public void selectBySqlQueryFactory(){
// 使用 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
*/
@Test
public void selectBySqlQueryFactory(){
// 使用 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)
// 调用函数查询序列
static SimpleExpression<Long> nextval(String sequence)
static <T extends Number> 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数据库专属,其他数据库报错
static WithinGroup<Object> listagg(Expression<?> expr, String delimiter)
// 使用示例:
SQLExpression.listagg(qEntity.name, ",").withinGroup.OrderBy(qEntity.name.asc()).getValue.as("Name")
// 将多列记录聚合为一列记录。separator为分隔符。MySQL、PostgreSQL都可用,PostgreSQL会根据模板翻译成String_agg函数
static StringExpression groupConcat(Expression<String> expr, String separator)
static StringExpression groupConcat(Expression<String> expr)
static <T> RelationalFunctionCall<T> relationalFunctionCall(Class<? extends T> type, String function, Object... args)
static <D extends Comparable> DateExpression<D> date(DateTimeExpression<D> dateTime)
static <D extends Comparable> DateExpression<D> date(Class<D> type, DateTimeExpression<?> dateTime)
static <D extends Comparable> DateTimeExpression<D> dateadd(DatePart unit, DateTimeExpression<D> date, int amount)
static <D extends Comparable> DateExpression<D> dateadd(DatePart unit, DateExpression<D> date, int amount)
// 获取两个日期的时间间隔(end-start)
static <D extends Comparable> 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>)
。
@Test
public void selectBySqlQueryFactory(){
// 使用 extends EntityPathBase<Entity> 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收
QUserAddress ua = QUserAddress.userAddress;
// jpa+sql的查询工具,本例使用的oracle的sql模板
JPASQLQuery<?> jpasqlQuery = new JPASQLQuery<Void>(em, new OracleTemplates());
// 子查询
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);
}
拓展了解
获取数据库的类型
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
@Slf4j
public class DataSourceUtil {
@Autowired
private DataSource dataSource;
// 数据库类型名称
private static String databaseProductName;
/**
* 初始化静态成员变量
*/
@PostConstruct
public void init(){
try(Connection connection = dataSource.getConnection()) {
databaseProductName = connection.getMetaData().getDatabaseProductName();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 获取数据库类型名称
*/
public static String getDatabaseProductName(){
return databaseProductName;
}
}
Jpa 表名大小写转换、字段名规避数据库关键字
在linux下,mysql的表名是区分大小写的,如果不能通过修改mysql配置取消表名区分大小写,则可以通过在Hibernate将转化的SQL语句发送给数据库执行之前转换大小写。
如果数据库字段名与数据库关键字(保留字)同名导致sql语句执行失败,也可通过该自定义类处理
import com.duran.ssmtest.utils.DataSourceUtil;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl;
import org.hibernate.cfg.ImprovedNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class MyPhysicalNamingStrategyStandardImpl extends PhysicalNamingStrategyStandardImpl {
private static final long serialVersionUID = 1L;
// mysql关键字列表
private static final String mysqlKey = "ADD,ALL,ALTER,ANALYZE,AND,AS,ASC,ASENSITIVE" +
",BEFORE,BETWEEN,BIGINT,BINARY,BLOB,BOTH,BY,CALL,CASCADE,CASE,CHANGE,CHAR,CHARACTER" +
",CHECK,COLLATE,COLUMN,CONDITION,CONNECTION,CONSTRAINT,CONTINUE,CONVERT,CREATE,CROSS" +
",CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURRENT_USER,CURSOR,DATABASE,DATABASES" +
",DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DEC,DECIMAL,DECLARE,DEFAULT,DELAYED" +
",DELETE,DESC,DESCRIBE,DETERMINISTIC,DISTINCT,DISTINCTROW,DIV,DOUBLE,DROP,DUAL,EACH,ELSE" +
",ELSEIF,ENCLOSED,ESCAPED,EXISTS,EXIT,EXPLAIN,FALSE,FETCH,FLOAT,FLOAT4,FLOAT8,FOR,FORCE" +
",FOREIGN,FROM,FULLTEXT,GOTO,GRANT,GROUP,HAVING,HIGH_PRIORITY,HOUR_MICROSECOND,HOUR_MINUTE" +
",HOUR_SECOND,IF,IGNORE,IN,INDEX,INFILE,INNER,INOUT,INSENSITIVE,INSERT,INT,INT1,INT2,INT3" +
",INT4,INT8,INTEGER,INTERVAL,INTO,IS,ITERATE,JOIN,KEY,KEYS,KILL,LABEL,LEADING,LEAVE,LEFT" +
",LIKE,LIMIT,LINEAR,LINES,LOAD,LOCALTIME,LOCALTIMESTAMP,LOCK,LONG,LONGBLOB,LONGTEXT,LOOP" +
",LOW_PRIORITY,MATCH,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MIDDLEINT,MINUTE_MICROSECOND,MINUTE_SECOND" +
",MOD,MODIFIES,NATURAL,NOT,NO_WRITE_TO_BINLOG,NULL,NUMERIC,ON,OPTIMIZE,OPTION,OPTIONALLY,OR" +
",ORDER,OUT,OUTER,OUTFILE,PRECISION,PRIMARY,PROCEDURE,PURGE,RAID0,RANGE,READ,READS,REAL" +
",REFERENCES,REGEXP,RELEASE,RENAME,REPEAT,REPLACE,REQUIRE,RESTRICT,RETURN,REVOKE,RIGHT,RLIKE" +
",SCHEMA,SCHEMAS,SECOND_MICROSECOND,SELECT,SENSITIVE,SEPARATOR,SET,SHOW,SMALLINT,SPATIAL" +
",SPECIFIC,SQL,SQLEXCEPTION,SQLSTATE,SQLWARNING,SQL_BIG_RESULT,SQL_CALC_FOUND_ROWS" +
",SQL_SMALL_RESULT,SSL,STARTING,STRAIGHT_JOIN,TABLE,TERMINATED,THEN,TINYBLOB,TINYINT" +
",TINYTEXT,TO,TRAILING,TRIGGER,TRUE,UNDO,UNION,UNIQUE,UNLOCK,UNSIGNED,UPDATE,USAGE,USE" +
",USING,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VALUES,VARBINARY,VARCHAR,VARCHARACTER,VARYING,WHEN" +
",WHERE,WHILE,WITH,WRITE,X509,XOR,YEAR_MONTH,ZEROFILL";
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
// 驼峰命名策略转换表名
String tableName = ImprovedNamingStrategy.INSTANCE.tableName(name.getText());
// 将entity中的表名全部转换成大写
tableName = tableName.toUpperCase();
return Identifier.toIdentifier(tableName);
}
@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) {
// 驼峰命名策略转换字段名
String colnumName = ImprovedNamingStrategy.INSTANCE.columnName(name.getText());
// 将entity中的字段名全部转换成大写
colnumName = colnumName.toUpperCase();
// 如果entity字段名与mysql关键字同名,则在entity字段名加上``
if ("mysql".equalsIgnoreCase(DataSourceUtil.getDatabaseProductName())){
if (Arrays.asList(StringUtils.split(mysqlKey, ",")).contains(colnumName.toUpperCase())){
colnumName = "`" + colnumName + "`";
}
}
return Identifier.toIdentifier(colnumName);
}
}
如果自定义的MyPhysicalNamingStrategyStandardImpl类未加@Component注解(将类交给spring容器管理),则需要在 application.properties里面添加如下配置(hibernate版本 >= 5.0):
# 值为自定义的MyPhysicalNamingStrategyStandardImpl类的全限定类名
spring.jpa.hibernate.naming.physical-strategy=com.test.config.MyPhysicalNamingStrategyStandardImpl
需要注意的是,如果hibernate版 < 5.0,则配置里的内容应是
spring.jpa.hibernate.naming-strategy=com.test.config.strategy.MyImprovedNamingStrategy
同时自定义的MyImprovedNamingStrategy类继承ImprovedNamingStrategy,并且重写相应的方法。
Spring JPA Junit 关闭自动回滚
使用 JPA 配合Hibernate ,采用注解默认是开启了LayzLoad也就是懒加载,所以当操作为增删改时需在Junit的单元测试上加上@Transactional
注解,这样Spring会自动为当前线程开启Session,这样在单元测试里面懒加载才不会因为访问完Repository之后,出现session not found.
但若在单元测试上面加了@Transactional 会自动回滚事务,需要在单元测试上面加上@Rollback(false)
,才能修改数据库。
@Test
@Transactional
@Rollback(false) //关闭自动回滚
public void saveTest() {
ProductCategory category = new ProductCategory();
category.setCategoryname("快乐");
category.setCategorytype(6);
ProductCategory save = categoryRepository.save(category);
System.out.println(save.toString());
延迟加载与立即加载(FetchType)
通常可以在@OneToMany中用LAZY、在@ManyToOne/Many中用EAGER,但不绝对,看具体需要。
- FetchType.LAZY:延迟加载,在查询实体A时,不查询出关联实体B,在调用getxxx方法时,才加载关联实体,但是注意,查询实体A时和getxxx必须在同一个Transaction中,不然会报错:no session。即会表现为两次单独的SQL查询(非联表查询)
- FetchType.EAGER:立即加载,在查询实体A时,也查询出关联的实体B。即会表现为一次查询且是联表查询
默认情况下,@OneToOne、@ManyToOne是LAZY,@OneToMany、@ManyToMany是EAGER。
有两个地方用到延迟加载:relationship(@OneToMany等)、attribute(@Basic)。后者一般少用,除非非常确定字段很少访问到。
时间类型的精度问题
如MySQL的DATETIME类型,默认是精确到秒的,故存入的时间戳的毫秒会被舍弃并根据四舍五入加入到秒(如1s573ms变成2s、1s473ms变成1s),从而保存进去与查出来的也会不一致。
外键关联、关联删除
外键关联
相关注解:@ManyToOne/@OneToMany/@OneToOne 、 @JoinColumn/@PrimaryKeyJoinColumn、@MapsId,用法及区别见:https://www.cnblogs.com/chiangchou/p/mappedBy.html
- (1)@JoinColumn用来指定外键,其name属性指定该注解所在Entity对应的表的一个列名
- (2)@ManyToOne等用来指定对应关系是多对一等数量对应关系
通过@ManyToOne等注解
指定数量对应关系时,须在多的一方标注(@ManyToOne),一的一方注不注均可。(以下以School、Student为例,为一对多关系)
- 若只用
@ManyToOne等注解
没用@JoinColumn注解
,则在生成表时会自动生成一张关联表来关联School、Student,表中包含School、Studeng的id - 若在用了
@ManyToOne等注解
的基础上用了@JoinColumn注解
则不会自动生成第三张表,而是会在多的一方生成一个外键列。列名默认为 ${被引用的表名}_id (可以通过@JoinColumn
的name属性指定列名)。 - 上法的缺点是在 insert 多的一方后会再执行一次 update 操作来设置外键的值(即使 在inser t时已经指定了),避免额外update 的方法:在一的一方不使用
@JoinColumn
,而是改为指定@OneToMan等注解
的mappedBy属性。注意:
@JoinColumn注解
和@ManyToOne等注解
的mappedBy属性不能同时存在,会报错。
关联删除
假设有user、admin两表,admin.user_id 与 user.id 对应。当要删除userId为"xx"一条admin表记录时:
1.若业务逻辑中未使用 JPA软删除:
1.1 若后者通过外键关联前者,则直接从user删除id为"xx"的记录即可,此时会级联删除admin表的相应记录。当然要分别从两表删除记录也可,此时须保证先从admin表再从user表删除;
1.2 若无外键关联,则需要分别从user、admin删除该记录,顺序先后无关紧要;
2.若使用了软删除,对于软删除操作外键将不起作用(因为物理上并未删除记录),因此此时也只能分别从两表软删除记录。但不同的是,此时须先从admin再从user表删除记录。
若顺序相反,会发现user表的记录不会被软删除。猜测原因为:内存中存在userEntity、adminEntity且adminEntity.userByUserId引用了userEntity,导致delete userEntity时发现其被adminEntity引用了从而内部取消执行了delete操作。
在实际业务中一般都会启用软删除,所以物理删除的场景很少,从而上面1.1、1.2的场景很少。综上,在涉及到关联删除时,最好按拓扑排序的顺序(先引用者再被引用者)依次删除各Entity记录。
示例:
进行如下设置后,JPA会自动生成为student表生成两个外键约束:student表school_id关联school表id自动、student表id字段关联user表id字段。
//StudentEntity
//get set ...
@Column(name = "id")
private String sId;
@Column(name = "school_id")
private String schoolId;
@ManyToOne
@JoinColumn(name = "school_id", referencedColumnName = "id", insertable = false, updatable = false)//school.school_id字段外键关联到school.id字段;多个字段对应数据库同一字段时会报错,通过添加insertable = false, updatable = false即可
private SchoolEntity schoolBySchoolId;
@OneToOne
@JoinColumn(name = "id", referencedColumnName = "id", insertable = false, updatable = false) //student.id字段外键关联到user.id字段。也可用@PrimaryKeyJoinColumn
@MapsId(value = "id")
private UserEntity userByUserId;
对于外键属性(如上面student表的school_id),当该属性不是当前表的主键时,通过 @OneToOne/@ManyToOne + @JoinColumn 定义即可成功地在数据库中自动生成产生外键约束。但当该属性也是当前表的主键时(如为student.id定义外键来依赖user.id字段),单靠@OneToOne + @JoinColumn并不能自动产生外键约束,此时可通过加@MapIds来解决。
总结:
通过@ManyToOne/@OneToMany/@OneToOne + @JoinColumn/@PrimaryKeyJoinColumn定义外键,是否需要@MapsId视情况而定。
外键场景有两种:
外键属性不是当前表的主键(如上面student表的school_id字段不是主键)
外键属性也是当前表的属性(如上面student表的id字段是主键)
基于这两种场景,各注解使用时的组合及效果如下:
说明:
使用注解组合后是否会自动为表生成外键约束?打钩的表示会、打叉的表示不会、半勾半叉的表示会但是生成的不是预期的(如场景1中期望school_id关联了school id自动,但一种结果是id关联了user id、另一种是自动产生了school_by_school_id字段并关联到了school id,显然都不符合期望)。
结论:
1、外键属性不是主键的场景(第一种),用 @OneToOne/@ManyToOne + @JoinColumn 即可,为了简洁推荐不用@MapIds,示例见上面的school_id关联school id设置。
2、外键属性是主键的场景(第二种),用 @OneToOne + @JoinColumn + @MapsId,示例见上面的student id关联user id设置。
虽从表中可见场景二有三种组合都可以达到目标,但为了符合业务语义(主键嘛,当然是唯一的,因此是一对一)且为了和场景一的尽可能统一,我们采用这个的组合。
实践发现,使用@MapsId时,要求外键字段、被关联的字段 的数据库列名得相同且都得为"id"。why?如何避免?TODO
通过 JPA 定义表结构的关联关系(如共用部分字段等)
这里以实际项目中课程、实验、步骤与其翻译数据的表结构关联方案设计为例:
多语言表(翻译表)与原表(主表)关联方案设计,需求:字段(列)复用以免重复代码定义、同一个列的定义如是否为空在不同表中可不一样(如有些字段主表中非空但翻译表中可空),有如下方案:
- 无关联,重复定义。pass
- 有关联
-
- 通过@MappeSuperclass,不同子类可以完全继承父类列定义且分别对应不同表,表结构完全相同,但不能覆盖父类的定义。pass
- 通过@Inheritance,三种策略:
-
- SINGLE_TABLE:父、子类对应同一张表。源课程和翻译课程id一样,违背主键唯一约束。pass
- JOINED:父、子类对应不同表且子类自动加与父类主键一样的字段与父类主键关联,但父表中除主键之外的所有字段无法在子表中再出现。pass
- TABLE_PER_CLASS:父、子类对应不同表且表定义完全相同,无外键,但同一字段在不同表中字段定义无法不同。pass
- 定义个普通父类,子类继承父类并分别进行@Column定义:不同子类对应不同表,不同表含有的字段及定义可不一样。selected
Web 支持
参阅:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web
Basic Web Support(Domain class、Pageable、Sort)
domain类(即被Spring Data CrudRepository管理的domain类,如Entity类)及Pageable、Sort可以直接作为handler方法的形参,框架会自动解析请求参数组装成相应的实参,示例:
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(@PathVariable("id") User user, Model model) {
model.addAttribute("user", user);
return "userForm";
}
}
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(Model model, Pageable pageable) {
model.addAttribute("users", repository.findAll(pageable));
return "users";
}
}
对于domain类,会自动根据request的"id"参数调用repository的findById查得对象。request示例:/user?id=2
对于Pageable,会根据request"page"、"size"参数组装对象;request示例:/users?page=0&size=2
对于Sort,会根据request的"sort"参数组装对象,该参数值须遵循规则: property,property(,ASC|DESC)(,IgnoreCase) 。request示例:/users?sort=firstname&sort=lastname,asc&sort=city,ignorecase
内部原理:第一者是由 DomainClassConverter 类完成的,后两者是由 HandlerMethodArgumentResolver 完成的。
Querydsl Web Support
可以直接将Querydsl的Predicate作为handler方法的形参,框架会自动(默认只要Querydsl在classpath上就会生效)根据请求参数组装创建Predicate实例。示例:
@Controller
class UserController {
@Autowired UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate,
Pageable pageable, @RequestParam MultiValueMap<String, String> parameters) {
model.addAttribute("users", repository.findAll(predicate, pageable));
return "index";
}
}
SpringDataJpa 和 mybatis 的比较
- spring data jpa实现了jpa(java persistence api)功能,即可以实现pojo转换为关系型数据库记录的功能,通俗来讲就是可以不写任何的建表sql语句了。jpa是spring data jpa功能的一个子集。
而mybatis并没有jpa功能,建表语句还是要自己写的。
- spring data jpa是全自动框架,不需要写任何sql。
而mybatis是半自动框架,需要自己写sql,mybatis-plus为mybatis赋能,使其也可以基本上不需要写任何模板sql。
- debug模式下看生成的sql,mybatis下的sql可读性很好,而spring data jpa下的查询sql可读性并不好。
- spring data jpa的insert与update都调用同一个方法save,如果带有主键id(如果启用了乐观锁,那么还有version字段),那么就是更新,否则就是新增,所以addOrUpdate是一个接口;
而mybatis中提供insert方法和updateById方法。
由于spring data jpa调用同一个方法,所以其要执行两条sql,先执行查询,再执行插入/更新。
另外就是返回值,spring data jpa的返回值是Employee对象,而mybatis的返回值是影响的行数,当然mybatis也可以得到新增后的id,返回新增后的对象
- spring data jpa的dynamic sql是使用JpaSpecificationExecutor,而mybatis中是使用xml来构造dynamic sql。
当执行分页查询的时候,spring data jpa实际上是调用了两个sql语句,通过count获得总记录数,即当用到Pageable的时候会执行一条count语句,这可能是很昂贵的操作,因为count操作在innodb中要扫描所有的叶子节点,通过limit来获得分页记录
mybatis获得总记录数好像并不是通过执行count语句来获得的,可能是通过游标cursor的方式来获得的,通过druid监控,其只执行一条sql语句
- spring data jpa支持自己来写sql语句,有两种方式:
1)@Query或@Modifying配合@Query
2)通过entityManager
但要注意的是:如果自己写sql语句,那么有些拦截器可能并不能起作用,如@PreUpdate
相对来说,mybatis就比较简单,直接在mapper xml中写sql就可以了