Hibernate与JPA2.0标准查询

Hibernate与JPA2.0标准查询

本文档主要对JPA2.0标准查询做了汇总:

  1. 简单动态查询
  2. sum等函数查询
  3. oracle自带函数处理,并排序
  4. group by查询
  5. 关联查询一:笛卡尔积
  6. 关联查询二:@ManyToOne端关联查询
  7. 关联查询三:@OneToMany端关联查询
  8. 子查询和in
  9. 子查询和exists
  10. distinct去重
  11. 设置参数

为什么使用JPA2.0的标准查询

在使用Spring data jpa查询的时候,可以使用多种查询方式:

  1. JPA2.0标准查询
  2. @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 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 spec = this.where("01010001001");
        List bills = this.repo.findAll(spec);
        for (Bill bill : bills) {
            this.logger.info("查询结果:{}", bill);
        }
    }

    private Specification where(String acctNo) {
        
        return new Specification() {
            @Override
            public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder cb) {
                List predicates = new ArrayList();
                // 账号
                if (StringUtils.isNotBlank(acctNo)) {
                    predicates.add(cb.equal(root.get("acctNo"), acctNo));
                }
                return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
            }
        };
    }

解析

1. 整体流程

ReReceiptRepository类实现了JpaSpecificationExecutor接口

repo.findAll(spec)内部实现原理:

EntityManager em;
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(domainClass);
// 这是from语句 
Root 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 query = em.createQuery(query)
// 获取List结果 
List list = query.getResultList();
  • EntityManager: 用于和持久性上下文关联。持久性上下文管理实体实例及其生命周期。所以可以使用EntityManager的API对实体实例进行增删改查。
  • EntityManager.getCriteriaBuilder()方法:可以获取CriteriaBuilder,用于创建CriteriaQuery
  • CriteriaBuilder:用于构造标准查询(CriteriaQuery)表达式谓词复合选择排序
  • CriteriaQuery(标准查询):定义了顶级查询的功能。其中泛型指定了返回的类型。顶级查询可以从接口的方法中可以看出:select、where、group by、having、order by、distinct。这些是基础的查询功能。
  • Expression(表达式):可以用于查询的类型,解决了与Java泛型不兼容的问题。
  • Predicate(谓词):即andor,同时也是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:

  1. EntityManager建立CriteriaBuilderEntityManager.getCriteriaBuilder()
  2. CriteriaBuilder建立CriteriaQuery(标准查询): query = builder.createQuery(domainClass)
  3. 组装标准查询sql:query.select(root).from(domainClass).where(predicate).orderBy(order)
  4. 创建一个TypedQuery用于执行条件查询:em.createQuery(query)
  5. 执行查询,并返回结果:typedQuery.getResultList()
2. where子句

了解完整体流程,现在关注下where子句的写法:Expression(表达式)Predicate(谓词)

predicates.add(cb.equal(root.get("acctNo"), acctNo));

CriteriaBuilder可以用于创建Expression(表达式),并提供了很多方法和数据库函数:包括equal、sum、avg等等,也就是说CriteriaBuilder实际上可以作为一个查询构造工厂。

CriteriaBuilder的查询构造方法基本上都需要传入实体域,例如:cb.equal(Path path, object), 表示实体域与某个值相等。实体域可以通过root.get("acctNo")方法获取Path实体域。

当然也可以使用元组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 query = cb.createQuery(BigDecimal.class);
        // 3. 设置from语句
        Root root = query.from(Bill.class);
        // 4. 获取字段的表达式
        Path amount = root.get("amount");
        // 5. 设置select语句, 使用sum函数
        query.select(cb.sum(amount));
        // 6. 设置where查询子句
        Specification spec = this.where("01095012010000001719");
        Predicate predicate = spec.toPredicate(root, query, cb);
        query.where(predicate);
        // 7. 创建查询
        TypedQuery typeQuery = em.createQuery(query);
        // 8. 获取单笔查询结果
        BigDecimal sumAmount = typeQuery.getSingleResult();
        this.logger.info("获取汇总金额是:{}", sumAmount);
        
    }

解析

sum函数avg函数max函数等查询返回值都是统计值,可能还需要一些实体域,所以按需设置select()方法

select(root)是返回实体所有字段,而sum函数只需要汇总值,这里的金额是BigDecimal类型的,所以在建CriteriaQuery查询时确定好类型:CriteriaQuery, 在创建TypeQuery时,也需要确定类型:TypedQuery

sum函数是在select语句中设置的:query.select(cb.sum(amount));

这里的amount是实体域,所以需要用root去获取指定的实体域:Path 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 query = build.createQuery(Detail.class);
        // 设置from语句
        Root root = query.from(Detail.class);
        // 设置select语句, root也是表达式
        query.select(root);
        // 设置where查询子句
        Specification spec = this.where3("01095012010000001719");
        Predicate predicate = spec.toPredicate(root, query, build);
        query.where(predicate);
        // 获取字段的表达式
        Path billsq = root.get("billsq");
        // 设置Oracle的TO_NUMBER函数
        // function(function name, 返回类型, 表达式);
        Expression billsq_to_number = build.function("TO_NUMBER", BigDecimal.class, billsq);
        // 设置排序
        Path trandt = root.get("trandt");
        Order trandt_asc = build.asc(trandt);
        Order billsq_asc = build.asc(billsq_to_number);
        // 排序组合
        List orders = new ArrayList<>();
        orders.add(trandt_asc);
        orders.add(billsq_asc);
        query.orderBy(orders);
        // 创建查询
        TypedQuery typeQuery = em.createQuery(query);
        // 获取查询结果
        List 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说明。这里只需传入一个需要排序的实体域:billsqPath

返回值:返回值是一个表达式类型,且泛型类型是第二个参数设置的值。

多个字段排序

query.orderBy(orders):排序可以传入一个orderorder的集合

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 query = build.createQuery(Object[].class);
        // 设置from语句
        Root root = query.from(Bill.class);
        // 设置查询返回的字段reType, count(1), 这里可以使用任何数据库函数
        Path reType = root.get("reType");
        Expression count = build.count(reType);
        // 设置select语句
        query.multiselect(reType, count);
        // 设置where子句
        Specification spec = this.where("01095012010000001719");
        Predicate predicate = spec.toPredicate(root, query, build);
        query.where(predicate);
        // 设置group by查询
        query.groupBy(reType);
        // 创建查询
        TypedQuery typeQuery = em.createQuery(query);
        // 获取返回结果
        List objects = typeQuery.getResultList();
        for (Object[] object : objects) {
            this.logger.info("查询结果: 类型: {}, 数量:{}", object[0], object[1]);
        }
    }

解析

返回值设置

因为group by的返回值按需组合,有时候需要使用number类型的统计值,无法单纯的使用实体域。所以有两种方式设置返回值,都可以无视类型返回。

    1. 使用Object[]数组,本例使用这种方式,数组值与select语句放入的顺序一致:query.multiselect(reType, count);
    1. 使用元组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 query = build.createQuery(Tuple.class);
        // 设置BillPrint from语句
        Root rootBillPrint = query.from(BillPrint.class);
        // 设置Bill from语句
        Root rootBill = query.from(Bill.class);
        // 设置关联的条件
        Predicate predicate = build.equal(rootBillPrint.get("recSeq"), rootBill.get("recSeq"));
        query.where(predicate);
        // 设置select语句
        query.multiselect(rootBill, rootBillPrint);
        TypedQuery typeQuery = em.createQuery(query);
        List 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: RootRootwhere子句设置关联字段相等:build.equal(rootBillPrint.get("recSeq"), rootBill.get("recSeq"));

但是这种关联是笛卡尔积形式的关联,Hibernate翻译出来的 A a cross join B b, 并非A a inner join B b

更无法使用特定化的left join onright join on

查询6:关联查询,@ManyToOne端为主的关联查询,即以 多 的那一方 去关联 一 的那一方

目前只支持inner join onleft 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 query = build.createQuery(BillPrint.class);
        // 设置BillPrint from语句
        Root rootBillPrint = query.from(BillPrint.class);
        // 设置关联的实体,第二个参数可选,默认是JoinTye.INNER,用root与关联
        rootBillPrint.join("bill", JoinType.LEFT);
        // 设置select语句 只有设置非全量字段时,才可以使用multiselect
        query.select(rootBillPrint);
        TypedQuery typeQuery = em.createQuery(query);
        List list = typeQuery.getResultList();
        for (BillPrint billPrint : list) {
            this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}", 
                billPrint.getAcctNo(), billPrint.getRecSeq(), billPrint.getReType());
        }
        this.logger.info("查询数量:{}", list.size());
    }

解析

@ManyToOne注解

如果使用left joininner join, 则必须需要两表关联的注解,且关联注解分为4种:

    1. @ManyToOne:多对一
    1. @OneToOne:一对一
    1. @OneToMany:一对多
    1. @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 query = build.createQuery(Bill.class);
        // 设置BillPrint from语句
        Root 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 typeQuery = em.createQuery(query);
        List 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 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 query = build.createQuery(Bill.class);
        // 设置from语句
        Root root = query.from(Bill.class);
        // 设置select语句
        query.select(root);
        // 设置where查询子句
        // 建立子查询Subquery
        Subquery subQuery = query.subquery(BigDecimal.class);
        // 建立子查询from语句
        Root 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 typeQuery = em.createQuery(query);
        List list = typeQuery.getResultList();
        for (Bill bill : list) {
            this.logger.info("查询结果:账号:{}, 主键: {}, 类型:{}", 
                    bill.getAcctNo(), bill.getRecSeq(), bill.getReType());
        }
        this.logger.info("查询总笔数:{}", list.size());
    }

解析

子查询

创建子查询步骤:

  1. 创建Subquery: Subquery subQuery = query.subquery(BigDecimal.class);,泛型即返回类型
  2. 设置子查询from:Root subRoot = subQuery.from(BillPrint.class);
  3. 设置子查询的select:subQuery.select(subRoot.get("recSeq"));
  4. 设置子查询的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 query = build.createQuery(Bill.class);
        // 设置from语句
        Root root = query.from(Bill.class);
        // 设置select语句
        query.select(root);
        // 设置where查询子句
        // 建立子查询Subquery
        Subquery subQuery = query.subquery(BigDecimal.class);
        // 建立子查询from语句
        Root 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 typeQuery = em.createQuery(query);
        List 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 query = build.createQuery(BigDecimal.class);
        // 设置from语句
        Root root = query.from(BillPrint.class);
        // 设置select语句
        query.select(root.get("recSeq"));
        // 设置distinct去重, true表示重复数据删除,false表示重复数据保存
        query.distinct(true);
        TypedQuery typeQuery = em.createQuery(query);
        List 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 query = build.createQuery(BillPrint.class);
        // 设置from语句
        Root root = query.from(BillPrint.class);
        // 设置select语句
        query.select(root);
        // 当需要非表达式的变量放入表达式的方法时,就使用参数
        // 创建String的表达式
        ParameterExpression partten = build.parameter(String.class); 
        // 将参数表达式放入to_date函数的第二个参数中
        Expression 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 typeQuery = em.createQuery(query);
        // 给参数设置值
        typeQuery.setParameter(partten, "yyyymmdd");
        // 获取查询结果
        List 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步:

  1. 声明指定类型的参数表达式:ParameterExpression partten = build.parameter(String.class);
  2. 将参数表达式partten作为正常的表达式使用:build.function("TO_DATE", Date.class, root.get("printDate"), partten);
  3. 使用TypeQuery给参数表达式赋值:typeQuery.setParameter(partten, "yyyymmdd");

这样就可以将String类型转换为参数表达式, 可以作为正常的表达式类型,在标准查询中使用

参考资料

本文档参照Hibernate官网和JPA2.0的jdk文档:

Hibernate官网

JPA2.0官网

你可能感兴趣的:(Hibernate与JPA2.0标准查询)