Hibernate与JPA2.0标准查询
本文档主要对JPA2.0标准查询做了汇总:
- 简单动态查询
- sum等函数查询
- oracle自带函数处理,并排序
- group by查询
- 关联查询一:笛卡尔积
- 关联查询二:@ManyToOne端关联查询
- 关联查询三:@OneToMany端关联查询
- 子查询和in
- 子查询和exists
- distinct去重
- 设置参数
为什么使用JPA2.0的标准查询
在使用Spring data jpa查询的时候,可以使用多种查询方式:
- JPA2.0标准查询
- @Query查询
使用@Query字符串sql查询固然更加灵活,但没有类型检查和规范,容易出错
JPA2.0标准查询的优势在于无需类型检查,写法更加规范,而且了解用法之后也很灵活
定义实体
JavaBean通过Hibernate的注解,可以映射为数据库实体
下面定义简单的JavaBean和数据库实体
- 账单表:与账单打印表是
一对多
关系,即一条账单可能有多次打印记录
/**
* 账单表.
* @author hhy
*
*/
@Data
@Entity
@Table(name = "BILL")
public class Bill {
/**
* 序号.
*/
@Id
@Column(name = "REC_SEQ", nullable = false, precision = 20)
private BigDecimal recSeq;
@OneToMany(mappedBy="bill")
List<BillPrint> billPrintList = new ArrayList<>();
/**
* 账单类型.
*/
@Column(name = "RE_TYPE", nullable = false, length = 30)
private String reType;
/**
* 金额.
*/
@Column(name = "AMOUNT", nullable = true, precision = 20, scale = 2)
private BigDecimal amount;
/**
* 账号.
*/
@Column(name = "ACCT_NO ", length = 30, nullable = false)
private String acctNo;
}
- 账单打印表:与账单表是
多对一
关系,即多笔打印记录可能对应同一个账单数据
/**
* 账单打印表.
*
* @author hdw
*
*/
@Data
@Entity
@Table(name = "BILL_PRINT")
public class BillPrint {
/**
* 序号.
*/
@Id
@Column(name = "REP_SEQ", nullable = false, precision = 20)
private BigDecimal repSeq;
/**
* 账号.
*/
@Column(name = "ACCT_NO ", length = 30, nullable = false)
private String acctNo;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name = "REC_SEQ",
referencedColumnName = "REC_SEQ",
insertable = false,
updatable = false
)
private Bill bill;
/**
* 账单表序号.
*/
@Column(name = "REC_SEQ", nullable = false, precision = 20)
private BigDecimal recSeq;
/**
* 打印日期.
*/
@Column(name = "PRINT_DATE", nullable = true, length = 10)
private String printDate;
/**
* 打印渠道.
*/
@Column(name = "PRINT_CHANNEL", nullable = true, length = 10)
private String printChannel;
}
- 明细表
/**
* 明细表.
* @author hhy
*
*/
@Data
@Entity
@Table(name = "DETAIL")
public class Detail {
/**
* 明细序号.
*/
@Id
@Column(name = "DETAIL_SEQ", nullable = false, precision = 20)
private BigDecimal detailSeq;
/**
* 对公账号.
*/
@Column(name = "ACCT_NO", nullable = false, length = 30)
private String acctNo;
/**
* 户名.
*/
@Column(name = "ACCT_NAME", nullable = false, length = 200)
private String acctName;
/**
* 明细流水.
*/
@Column(name = "BILLSQ ", nullable = true, length = 30)
private String billsq;
/**
* 交易日期.
*/
@Column(name = "TRANDT ", nullable = true, length = 30)
private String trandt;
}
Hibernate注解解析
实体上注解
实体映射的是数据库表
- @Entity: JavaBean要作为Hibernate的实体,需要加上该注解,表示这是一个实体,可以映射为数据库表
- @Table(name = "BILL"): 该注解可以指定映射的数据库表名,如果没有添加该注解,Hibernate会使用默认策略,将实体类名作为表名
- @Data:给每个字段设置get set方法
字段上注解
字段即实体域,映射的是数据库表中的列
- @Id :主键列,唯一标识
- @Column(name = "REC_SEQ", nullable = false, precision = 20):可以指定映射列的属性,包括列名、是否可为空、字段长度、精度、默认值、是否唯一性等等
查询1:简单动态查询
如果有账号则使用账号查询,没有,则全量查询
select * from bill a (where acct_no = ?)
@Autowired
private ReReceiptRepository repo;
@Autowired
private EntityManager em;
private void query1() {
Specification<Bill> spec = this.where("01010001001");
List<Bill> bills = this.repo.findAll(spec);
for (Bill bill : bills) {
this.logger.info("查询结果:{}", bill);
}
}
private Specification<Bill> where(String acctNo) {
return new Specification<Bill>() {
@Override
public Predicate toPredicate(Root<Bill> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
// 账号
if (StringUtils.isNotBlank(acctNo)) {
predicates.add(cb.equal(root.<String>get("acctNo"), acctNo));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
}
解析
1. 整体流程
ReReceiptRepository类实现了JpaSpecificationExecutor接口
repo.findAll(spec)内部实现原理:
![](/qrcode.jpg)
EntityManager em;
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<S> query = builder.createQuery(domainClass);
// 这是from语句
Root<U> root = query.from(domainClass);
// 这是构建where子句
Predicate predicate = spec.toPredicate(root, query, builder);
if (predicate != null) {
query.where(predicate);
}
// 这是select语句
query.select(root);
// 如果有排序则进行排序
if (sort != null) {
query.orderBy(toOrders(sort, root, builder));
}
// 建立查询
TypedQuery<S> query = em.createQuery(query)
// 获取List结果
List<T> list = query.getResultList();
-
EntityManager
: 用于和持久性上下文关联。持久性上下文管理实体实例及其生命周期。所以可以使用EntityManager
的API对实体实例进行增删改查。 -
EntityManager.getCriteriaBuilder()
方法:可以获取CriteriaBuilder
,用于创建CriteriaQuery
-
CriteriaBuilder
:用于构造标准查询(CriteriaQuery)
,表达式
,谓词
,复合选择
,排序
。 -
CriteriaQuery(标准查询)
:定义了顶级查询的功能。其中泛型指定了返回的类型。顶级查询可以从接口的方法中可以看出:select、where、group by、having、order by、distinct
。这些是基础的查询功能。 -
Expression(表达式)
:可以用于查询的类型,解决了与Java泛型不兼容的问题。 -
Predicate(谓词)
:即and
、or
,同时也是Expression(表达式)
的一种,同时可以将Expression(表达式)
组合起来,形成where子句中内容 -
Selection(选择)
: 查询结果返回的项。 -
Order(排序)
:设置查询排序的对象。 -
Root
: query.from(domainClass)方法返回的就是一个Root类型,query.select(root)方法查询root得到实体类,相当于一个实体的查询表达式类型,可以使用root.get()方法,获取这个实体内的实体域查询表达式:Path
-
query.from(domainClass)
: 可以设置from语句,表示从哪一个实体中查询,并返回实体的查询表达式:Root
-
query.where(predicate)
: 设置where子句,where子句中放入的是Predicate(谓词)
或Expression(表达式)
,表示根据谓词连接的条件限制,或表达式的条件限制设置查询。 -
query.select(root)
:设置select语句,即查询的返回项。该值需要与CriteriaQuery(标准查询)
中泛型设置保持关联。select语句中若放入Root
,表示返回值是该实体。可以使用multiselect()
方法,表示返回值是该实体。可以使用multiselect()方法的参数是可变参数类型,表示指定的返回项是多个实体域。 -
query.orderBy(order)
: 设置排序,放入的是Order(排序对象)
。 -
em.createQuery(query)
: 创建一个TypedQuery用于执行条件查询。同时该方法有多个重载方法,可以执行持久化查询、条件更新查询、条件删除查询等。 -
TypedQuery
: 用于控制条件查询 -
typedQuery.getResultList()
: TypedQuery的getResultList()方法执行select查询语句,并返回List结果,TypedQuery可以获取单笔结果,第一笔结果,有对应的API可以使用。
现在可以组合一条简单的sql:
- 由
EntityManager
建立CriteriaBuilder
:EntityManager.getCriteriaBuilder()
- 由
CriteriaBuilder
建立CriteriaQuery(标准查询)
:query = builder.createQuery(domainClass)
- 组装标准查询sql:query.select(root).from(domainClass).where(predicate).orderBy(order)
- 创建一个TypedQuery用于执行条件查询:
em.createQuery(query)
- 执行查询,并返回结果:
typedQuery.getResultList()
2. where子句
了解完整体流程,现在关注下where子句的写法:Expression(表达式)
和Predicate(谓词)
predicates.add(cb.equal(root.<String>get("acctNo"), acctNo));
CriteriaBuilder
可以用于创建Expression(表达式)
,并提供了很多方法和数据库函数:包括equal、sum、avg等等,也就是说CriteriaBuilder
实际上可以作为一个查询构造工厂。
CriteriaBuilder
的查询构造方法基本上都需要传入实体域,例如:cb.equal(Path<X> path, object), 表示实体域与某个值相等。实体域可以通过root.get("acctNo")方法获取Path<X>实体域。
当然也可以使用元组Tuple
Predicate
可以将多个查询表达式连接,谓词本质是and、or的连接表达式,将多个Expression(表达式)
连接起来。
3. 实现动态查询
query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
这里按照需要,动态将所需的多个Predicate
放入谓词集合List中,query.where().getRestriction()方法可以返回一个组合的Predicate
。
但where()方法中的参数是可变参数,可以传入数组,所以predicates.toArray(new Predicate[predicates.size()])
将List转换成数组
查询2:sum查询
统计某一个账号的所有金额
select sum(a.amount) from bill a where a.acct_no=?
// sum查询
private void query2() {
// 1. 建查询build
CriteriaBuilder cb = em.getCriteriaBuilder();
// 2. 建查询query
CriteriaQuery<BigDecimal> query = cb.createQuery(BigDecimal.class);
// 3. 设置from语句
Root<Bill> root = query.from(Bill.class);
// 4. 获取字段的表达式
Path<BigDecimal> amount = root.get("amount");
// 5. 设置select语句, 使用sum函数
query.select(cb.sum(amount));
// 6. 设置where查询子句
Specification<Bill> spec = this.where("01095012010000001719");
Predicate predicate = spec.toPredicate(root, query, cb);
query.where(predicate);
// 7. 创建查询
TypedQuery<BigDecimal> typeQuery = em.createQuery(query);
// 8. 获取单笔查询结果
BigDecimal sumAmount = typeQuery.getSingleResult();
this.logger.info("获取汇总金额是:{}", sumAmount);
}
解析
sum函数
、avg函数
、max函数
等查询返回值都是统计值,可能还需要一些实体域,所以按需设置select()
方法
select(root)
是返回实体所有字段,而sum函数
只需要汇总值,这里的金额是BigDecimal类型的,所以在建CriteriaQuery
查询时确定好类型:CriteriaQuery<BigDecimal>
, 在创建TypeQuery
时,也需要确定类型:TypedQuery<BigDecimal>
sum函数
是在select语句中设置的:query.select(cb.sum(amount));
这里的amount
是实体域,所以需要用root去获取指定的实体域:Path<BigDecimal> amount = root.get("amount")
这里没有使用分组,所以只有一条结果,返回单笔结果:typeQuery.getSingleResult();
查询3:使用oracle数据库自带的函数进行排序
查询指定账号的所有交易记录,并按日期和流水号升序排序。其中流水号是varchar类型,需要转换成number类型再排序。
select * from detail a where a.acct_no = ? order by a.trandt, to_number(a.billsq) asc;
// 使用oracle数据库自带的函数进行排序
private void query3() {
// 建查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建查询query
CriteriaQuery<Detail> query = build.createQuery(Detail.class);
// 设置from语句
Root<Detail> root = query.from(Detail.class);
// 设置select语句, root也是表达式
query.select(root);
// 设置where查询子句
Specification<Detail> spec = this.where3("01095012010000001719");
Predicate predicate = spec.toPredicate(root, query, build);
query.where(predicate);
// 获取字段的表达式
Path<String> billsq = root.get("billsq");
// 设置Oracle的TO_NUMBER函数
// function(function name, 返回类型, 表达式);
Expression<BigDecimal> billsq_to_number = build.function("TO_NUMBER", BigDecimal.class, billsq);
// 设置排序
Path<String> trandt = root.get("trandt");
Order trandt_asc = build.asc(trandt);
Order billsq_asc = build.asc(billsq_to_number);
// 排序组合
List<Order> orders = new ArrayList<>();
orders.add(trandt_asc);
orders.add(billsq_asc);
query.orderBy(orders);
// 创建查询
TypedQuery<Detail> typeQuery = em.createQuery(query);
// 获取查询结果
List<Detail> details = typeQuery.getResultList();
if (details.size() == 0) {
this.logger.info("查询结果为空");
}
for (Detail detail : details) {
this.logger.info("查询结果为:{}", detail);
}
}
解析
oracle自带函数实现
to_number(a.billsq)
这个是oracle自带的to_number函数,如果要用oracle自带函数,可以使用function方法
build.function("TO_NUMBER", BigDecimal.class, billsq);
第一个参数是函数的名称:TO_NUMBER
第二个参数是这个函数执行后的返回类型:BigDecimal.class
第三个参数是实际上是可变参数,对应着函数需要传入的参数。所以传入多个参数值或参数数组,这里to_number函数只需要一个参数,而这个参数必须是表达式类型。如果是字段,需要转换为实体域Path
,如果是非表达式类型,且无法转换为表达式类型的,需要使用表达式参数
,在案例11说明。这里只需传入一个需要排序的实体域:billsq
的Path
返回值:返回值是一个表达式类型
,且泛型类型是第二个参数设置的值。
多个字段排序
query.orderBy(orders)
:排序可以传入一个order
或order的集合
Order
需要使用build
创建, build.asc(trandt)
升序排序,build.desc(trandt)
降序排序。其中参数必须是表达式类型,所以可以直接使用实体域Path
,指定需要排序的实体域,也可以放入build.function()
, 函数执行的返回值也是表达式类型,表示将某一个实体域使用特定函数处理后再排序。
查询4:group by查询
统计指定账号各类型账单的数量
select re_type,count(1) from bill where acct_no='01095012010000001719' group by re_type;
private void query4() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query
CriteriaQuery<Object[]> query = build.createQuery(Object[].class);
// 设置from语句
Root<Bill> root = query.from(Bill.class);
// 设置查询返回的字段reType, count(1), 这里可以使用任何数据库函数
Path<String> reType = root.get("reType");
Expression<Long> count = build.count(reType);
// 设置select语句
query.multiselect(reType, count);
// 设置where子句
Specification<Bill> spec = this.where("01095012010000001719");
Predicate predicate = spec.toPredicate(root, query, build);
query.where(predicate);
// 设置group by查询
query.groupBy(reType);
// 创建查询
TypedQuery<Object[]> typeQuery = em.createQuery(query);
// 获取返回结果
List<Object[]> objects = typeQuery.getResultList();
for (Object[] object : objects) {
this.logger.info("查询结果: 类型: {}, 数量:{}", object[0], object[1]);
}
}
解析
返回值设置
因为group by
的返回值按需组合,有时候需要使用number类型的统计值,无法单纯的使用实体域。所以有两种方式设置返回值,都可以无视类型返回。
-
- 使用Object[]数组,本例使用这种方式,数组值与select语句放入的顺序一致:
query.multiselect(reType, count);
- 使用Object[]数组,本例使用这种方式,数组值与select语句放入的顺序一致:
-
- 使用
元组Tuple
, 下一个案例使用了元组Tuple
。
- 使用
group by 语句
query.groupBy(reType);
方法可以设置按哪一个实体域Path
分组,其后也可以设置having()
方法,进行分组处理。
查询5:关联查询之多根笛卡尔积形式
账单表和账单打印表关联查询,获取笛卡尔积
select * from re_receipt a cross join re_receipt_print b on a.rec_seq=b.rec_seq
private void query5() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query, 多根形式
CriteriaQuery<Tuple> query = build.createQuery(Tuple.class);
// 设置BillPrint from语句
Root<BillPrint> rootBillPrint = query.from(BillPrint.class);
// 设置Bill from语句
Root<Bill> rootBill = query.from(Bill.class);
// 设置关联的条件
Predicate predicate = build.equal(rootBillPrint.get("recSeq"), rootBill.get("recSeq"));
query.where(predicate);
// 设置select语句
query.multiselect(rootBill, rootBillPrint);
TypedQuery<Tuple> typeQuery = em.createQuery(query);
List<Tuple> list = typeQuery.getResultList();
for (Tuple tuple : list) {
this.logger.info("查询结果:{}", tuple);
}
this.logger.info("查询数量:{}", list.size());
}
解析
返回值
关联查询返回的是两个实体,这里使用Tuple
合并成一个实体返回,其返回字段顺序与select语句设置的顺序一致
多根关联
最简单的关联语句方式就是 from A a, B b where a.id = b.id
按这个思路关联两个实体,创建了两个Root: Root<BillPrint>
和 Root<Bill>
,where子句
设置关联字段相等:build.equal(rootBillPrint.get("recSeq"), rootBill.get("recSeq"));
但是这种关联是笛卡尔积形式的关联,Hibernate翻译出来的 A a cross join B b, 并非A a inner join B b
更无法使用特定化的left join on
和right join on
查询6:关联查询,@ManyToOne端为主的关联查询,即以 多 的那一方 去关联 一 的那一方
目前只支持inner join on
和 left join on, right join on
不支持
有@ManyToOne
时,Hibernate在oracle的创建表时,会创建外键
select * from bill_print a left join bill b on a.rec_seq=b.rec_seq
private void query6() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query, 多根形式
CriteriaQuery<BillPrint> query = build.createQuery(BillPrint.class);
// 设置BillPrint from语句
Root<BillPrint> rootBillPrint = query.from(BillPrint.class);
// 设置关联的实体,第二个参数可选,默认是JoinTye.INNER,用root与关联
rootBillPrint.join("bill", JoinType.LEFT);
// 设置select语句 只有设置非全量字段时,才可以使用multiselect
query.select(rootBillPrint);
TypedQuery<BillPrint> typeQuery = em.createQuery(query);
List<BillPrint> list = typeQuery.getResultList();
for (BillPrint billPrint : list) {
this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}",
billPrint.getAcctNo(), billPrint.getRecSeq(), billPrint.getReType());
}
this.logger.info("查询数量:{}", list.size());
}
解析
@ManyToOne注解
如果使用left join
或inner join
, 则必须需要两表关联的注解,且关联注解分为4种:
-
-
@ManyToOne
:多对一
-
-
-
@OneToOne
:一对一
-
-
-
@OneToMany
:一对多
-
-
-
@ManyToMany
:多对多
-
BillPrint表
使用@ManyToOne
注解,表示多个账单打印都对应一条账单数据, Hibernate内部使用外键作为关联
在实体BillPrint
中可以看到
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name = "REC_SEQ",
referencedColumnName = "REC_SEQ",
insertable = false,
updatable = false
)
private Bill bill;
@JoinColumn
注解作用:是指定外键列
的字段名
和关联实体
的外键连接
的字段名
name:
指定外键列的字段名。如果不设置name
,会用默认规则生成外键列,默认规则是:属性名(reReceipt)+ "_" + 关联实体主键
本例中必须设置,因为BillPrint
中已经设置好了关联的字段recSeq
,如果不设置,Hibernate不知道使用该字段作为外键,反而使用自己默认生成的外键字段, 这时如果数据库中没有默认字段,则会报错
referencedColumnName:
指定关联实体的外键连接的字段名。如果不设置,会默认使用关联实体的主键,本例中可不设置
insertable = false, updatable = false
这两个属性表示:在持久化程序中是否包含该列(Bill bill),默认为true
意思是 如果没有设置(使用默认),或设置为true,在插入和修改操作中,这一列也会添加进去
Hibernate考虑你没有设置外键,使用默认策略自己生成外键并在创建数据库中添加进去。在插入和修改时,默认也肯定要更新外键数据的。
但是本例中我们已经指定了外键字段,不需要自己生成,也不存在自己生成的外键字段,所以更不存在插入修改时更新这个自己生成的外键数据。
结论:当设置了指定的外键列,则insertable和updatable必须设置为false
为什么@ManyToOne(fetch=FetchType.LAZY)要设置fetch=FetchType.LAZY
Hibernate的数据获取策略:
Hibernate有两种数据获取策略:EAGER(立即获取),LAZY(延迟获取)
而一般简单查询的默认获取策略是LAZY延迟获取,当字段需要时采取获取
而关联查询@ManyToOne注解时,属性fetch的默认值是EAGER(立即获取)
当查询本实体时,会立即查询关联的实体,即使没有用到关联实体的属性,而且使用的查询方式都是主键查询。这样无疑会大大增加数据库jdbc的连接,产生多余的N条查询语句
这就是Hibernate的N+1问题。
例:select * from bill_print;
Hibernate系统内部:select * from bill_print;
查询出N条数据,同时用N条数据中的关联键去查询关联实体:select * from bill where rec_seq=?
这样的sql的数量很多,取决于你查询出多少条数据,有多少种关联主键,即:? = select distinct rec_seq from bill_print;
因为JPA2.0的标准规定的@ManyToOne的默认获取数据策略是EAGER(立即获取),而Hibernate是实现该标准的工具,所以必须规定默认值为EAGER(立即获取)
Hibernate建议我们在@ManyToOne的获取策略中显式声明为LAZY(延迟获取),以防止N+1问题
查询7:关联查询,@OneToMany端为主的关联查询,即以 一 的那一方 去关联 多 的那一方
目前只支持inner join on 和 left join on, right join on 不支持
从写法上和执行结果上看,@OneToMany和@ManyToOne都是一样的。但是right join on的不支持,所以需要区别对待
实际上,因为当前@OneToMany设置的双向关联,并非是单向关联,幕后都是用@ManyToOne端的外键列去关联,所以表现都一样。
如果@OneToMany设置的是单向关联,则必须有一个连接表,用于关联查询时,从关联表去关联查询。Hibernate在创建表时也会创建一个连接表
private void query7() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query, 多根形式
CriteriaQuery<Bill> query = build.createQuery(Bill.class);
// 设置BillPrint from语句
Root<Bill> rootBill = query.from(Bill.class);
rootBill.join("billPrintList", JoinType.LEFT);
// 设置select语句
query.select(rootBill);
Predicate predicate = build.equal(rootBill.get("acctNo"), "01095012010000001719");
query.where(predicate);
TypedQuery<Bill> typeQuery = em.createQuery(query);
List<Bill> list = typeQuery.getResultList();
for (Bill bill : list) {
this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}",
bill.getAcctNo(), bill.getRecSeq(), bill.getReType());
}
this.logger.info("查询数量:{}", list.size());
}
解析
@OneToMany注解
@OneToMany(mappedBy="bill")
List<BillPrint> billPrintList = new ArrayList<>();
如果关联的实体有@ManyToOne
注解,则这个@OneToMany
是双向的,否则,是单向的
如果是单向的@OneToMany
关联,则Hibernate内部会创建一个中间表,用来连接两张表,所以关联查询时,是三张表的关联查询
如果是双向的@OneToMany
关联,即关联实体有@ManyToOne
注解,则关联关系实际上由子实体控制,数据库只需要在子实体方建立外键联系即可。
如果是双向的,则需要mappedBy
明确在关联子实体上的@ManyToOne的属性
查询8:使用子查询和in
007渠道打印多少笔账单
select * from bill where rec_seq in (select rec_seq from bill_print where print_channel = '007');
private void query8() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query
CriteriaQuery<Bill> query = build.createQuery(Bill.class);
// 设置from语句
Root<Bill> root = query.from(Bill.class);
// 设置select语句
query.select(root);
// 设置where查询子句
// 建立子查询Subquery
Subquery<BigDecimal> subQuery = query.subquery(BigDecimal.class);
// 建立子查询from语句
Root<BillPrint> subRoot = subQuery.from(BillPrint.class);
// 建立子查询的select语句,查询字段为recSeq
subQuery.select(subRoot.get("recSeq"));
// 建立子查询的where子句
subQuery.where(build.equal(subRoot.get("printChannel"), "007"));
// recSeq in (subQuery);
query.where(root.get("recSeq").in(subQuery));
TypedQuery<Bill> typeQuery = em.createQuery(query);
List<Bill> list = typeQuery.getResultList();
for (Bill bill : list) {
this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}",
bill.getAcctNo(), bill.getRecSeq(), bill.getReType());
}
this.logger.info("查询总笔数:{}", list.size());
}
解析
子查询
创建子查询步骤:
- 创建Subquery:
Subquery<BigDecimal> subQuery = query.subquery(BigDecimal.class);
,泛型即返回类型 - 设置子查询from:
Root<BillPrint> subRoot = subQuery.from(BillPrint.class);
- 设置子查询的select:
subQuery.select(subRoot.get("recSeq"));
- 设置子查询的where:
subQuery.where(build.equal(subRoot.get("printChannel"), "007"));
创建子查询步骤其实与创建一般标准查询差别不大,区别:
标准查询的CriteriaQuery是由build.createQuery(domain.class)方法创建的
子查询的Subquery是由query.subquery(domain.class)方法创建的
in 语句
in语句是 where 字段 in ();
所以在where子句中,实体域Paht in (表达式),所以是Path.in()方法,且参数是表达式,而子查询实际上也是表达式,其返回值若是实体域可以直接作为in的参数;
查询9:使用子查询 exists
007渠道打印多少笔账单
select * from bill a where EXISTS (select 1 from bill_print b wherea.rec_seq=b.rec_seq and b.print_channel='007');
private void query9() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query
CriteriaQuery<Bill> query = build.createQuery(Bill.class);
// 设置from语句
Root<Bill> root = query.from(Bill.class);
// 设置select语句
query.select(root);
// 设置where查询子句
// 建立子查询Subquery
Subquery<BigDecimal> subQuery = query.subquery(BigDecimal.class);
// 建立子查询from语句
Root<BillPrint> subRoot = subQuery.from(BillPrint.class);
// 建立子查询的select语句,查询字段为recSeq
subQuery.select(subRoot.get("recSeq"));
// 建立子查询的where子句
subQuery.where(build.and(build.equal(subRoot.get("printChannel"), "007"), build.equal(subRoot.get("recSeq"), root.get("recSeq"))));
query.where(build.exists(subQuery));
TypedQuery<Bill> typeQuery = em.createQuery(query);
List<Bill> list = typeQuery.getResultList();
for (Bill bill : list) {
this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}",
bill.getAcctNo(), bill.getRecSeq(), bill.getReType());
}
this.logger.info("查询总笔数:{}", list.size());
}
解析
exists
exists
语句在where子句中,使用查询构造工厂build建立:build.exists()
方法,且参数一般都是子查询Subquery
。
exists
子查询中,select语句并不重要,select 1 都可以,但是select语句需要放入表达式,1是Integer
类型,并非表达式类型,本例使用实体域subRoot.get("recSeq")
替代。也可以使用参数表达式
,将1变成表达式放入select语句中。
查询10:使用distinct去重
查询已打印的账单主键
select distinct(rec_seq) from bill_print a;
private void query10() {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query
CriteriaQuery<BigDecimal> query = build.createQuery(BigDecimal.class);
// 设置from语句
Root<BillPrint> root = query.from(BillPrint.class);
// 设置select语句
query.select(root.get("recSeq"));
// 设置distinct去重, true表示重复数据删除,false表示重复数据保存
query.distinct(true);
TypedQuery<BigDecimal> typeQuery = em.createQuery(query);
List<BigDecimal> list = typeQuery.getResultList();
for (BigDecimal recSeq : list) {
this.logger.info("查询结果: 主键: {}", recSeq);
}
this.logger.info("查询总笔数:{}", list.size());
}
解析
distinct方法
distinct
去重一般都在select语句中设置:select distinct xxx
所以query.distinct()
方法在query
上,一般在紧跟在query.select()
方法后面
方法的参数是Boolean值
,true
表示重复数据删除,即去重。而false
表示重复数据保存,相当于不使用distinct
设置参数
查询某一段时间内打印的账单,使用between and
,但数据库中的字段是varchar
,需要转换为Date
类型
select * from bill_print a where to_date(a.print_date) between ? and ?;
private void query11() throws ParseException {
// 建立查询build
CriteriaBuilder build = em.getCriteriaBuilder();
// 建立查询query
CriteriaQuery<BillPrint> query = build.createQuery(BillPrint.class);
// 设置from语句
Root<BillPrint> root = query.from(BillPrint.class);
// 设置select语句
query.select(root);
// 当需要非表达式的变量放入表达式的方法时,就使用参数
// 创建String的表达式
ParameterExpression<String> partten = build.parameter(String.class);
// 将参数表达式放入to_date函数的第二个参数中
Expression<Date> print_date = build.function("TO_DATE", Date.class, root.get("printDate"), partten);
//设置where子句
// 设置起始时间,结束时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
query.where(build.between(print_date, sdf.parse("20180901"), sdf.parse("20181001")));
TypedQuery<BillPrint> typeQuery = em.createQuery(query);
// 给参数设置值
typeQuery.setParameter(partten, "yyyymmdd");
// 获取查询结果
List<BillPrint> list = typeQuery.getResultList();
for (BillPrint billPrint : list) {
this.logger.info("查询结果:账号:{}, 主键: {}", billPrint.getAcctNo(),billPrint.getRecSeq());
}
this.logger.info("查询总笔数:{}", list.size());
}
解析
设置参数
将数据库字段由varchar类型转换为date类型,使用oracle自带的函数:to_date(a.print_date,'yyyymmdd');
之前说过使用数据库自带函数使用build.function()
方法。但这里有点问题,方法的第三个参数传入的是函数to_date()
的参数,而这个参数需要传入两个值,第一个值是实体域Path
,第二个值是一个字符串pattern
,即yyyymmdd
之前说过,function()
方法的第三个参数一定要表达式类型,不可以是字符串或其他类型,所以需要将字符串yyyymmdd
转换成表达式类型
。
这里就用到参数表达式
参数表达式
3步:
- 声明指定类型的参数表达式:
ParameterExpression<String> partten = build.parameter(String.class);
- 将参数表达式
partten
作为正常的表达式使用:build.function("TO_DATE", Date.class, root.get("printDate"), partten);
- 使用
TypeQuery
给参数表达式赋值:typeQuery.setParameter(partten, "yyyymmdd");
这样就可以将String类型转换为参数表达式
, 可以作为正常的表达式类型,在标准查询中使用
参考资料
本文档参照Hibernate官网和JPA2.0的jdk文档: