Hibernate 13 编写查询 - 单体查询与集合查询

Hibernate 13 编写查询 - 单体查询与集合查询

看完了查询接口, 知道是怎么一回事之后, 就要来看看除了映射之外的核心, 就是如何编写查询. 在JPA中编写查询, 可以通过两种方式, 一种是编写JPQL/HQL语言, 另外一种是编程方式. 这两种方式中, 可以移植的是编程方式 说白了, 就是如何将SQL语句转换成JPQL语句和编程方式. 这里先聚

看完了查询接口, 知道是怎么一回事之后, 就要来看看除了映射之外的核心, 就是如何编写查询. 在JPA中编写查询, 可以通过两种方式, 一种是编写JPQL/HQL语言, 另外一种是编程方式. 这两种方式中, 可以移植的是编程方式 说白了, 就是如何将SQL语句转换成JPQL语句和编程方式. 这里先聚焦于查询, 也就是SELECT开头的SQL语句.
  1. SQL语句转换成JPQL和编程方式的方法
  2. CriteriaBuilder的条件API
  3. CriteriaBuilder的集合和函数
  4. Hibernate特有的函数和操作非JPA标准的函数的方法
  5. 排序

SQL语句转换成JPQL和编程方式的方法

想要知道如何编写查询, 首先就要知道的是SQL语句如何转换成JPQL和编程方式. 一个没有带有子查询的SQL语句如下:
SELECT sender.id, sender.name FROM sender WHERE sender.id between 30 and 60 ORDER BY sender.id desc;
将一个SQL转换成JPQL的核心, 就是用对象名替代关系名称, 用"对象.属性名"替代"关系.列名". 所以转换后的结果是:
SELECT s.id, s.name FROM Sender as s WHERE s.id between 30 and 60 ORDER BY s.id desc
不过这里要注意的是, 查询多列的时候, 结果集中的每一个数据对象并不是一个Sender对象, 而是一个Object[]类型. 上边这一条查询在实际执行的时候, 是这样的:
TypedQuery<Object[]> query =
            em.createQuery("SELECT s.id, s.name FROM Sender as s WHERE s.id between 30 and 60 ORDER BY s.id desc", Object[].class);

List<Object[]> results = query.getResultList();

for (Object[] s : results) {
    System.out.println(s[0] + " | " + s[1]);
}
这是因为Hibernate会将查到的每一个id和name组装成一个数组, 当成一行返回的结果. 可以看到, JPQL和SQL在实际的写法上非常相似, 只是要注意, JPQL要求一定要使用别名. 注意, JPQL是不可移植的, 所以接下来, 要看看将SQL/JPQL转换成JPA标准的编程方式. 这里先要引入几个概念:
  1. 查询根, 所谓查询根, 就是FROM子句, 即东西到底从哪里来. 这个在之前已经使用过, 就是使用Root<Sender> root = query1.from(Sender.class);
  2. 约束, 即WHERE, 这个通过调用 CriteriaQuery对象的.where()方法来实现, 方法中的参数是JPA的一些条件API.
  3. 排序, 即ORDER BY, 这个需要链式调用JPA的排序API
虽然现在还没有具体学, 但是先直接将上边这句翻译成编程方式的例子来看看:
//CriteriaBuilder是创建查询和查询条件的对象
CriteriaBuilder cb = em.getCriteriaBuilder();
//创建编程方式的查询对象, 注意泛型, 这个泛型类型就是最后结果的类型
CriteriaQuery<Object[]> criteriaQuery = cb.createQuery(Object[].class);
//创建查询根, 这里单独创建, 是为了后边需要使用这个对象, 这行语句很像起一个别名
Root<Sender> root = criteriaQuery.from(Sender.class);
//这一行虽然还没学, 但是看过去的顺序和JPQL/SQL相同, 每个关键字函数的内部的所有API都使用CriteriaBuilder或者查询根来创建.
criteriaQuery.multiselect(root.get("id"), root.get("name")).where(cb.between(root.<Long>get("id"), 30L, 60L)).orderBy(cb.desc(root.get("id")));
//用同样的泛型来创建查询
TypedQuery<Object[]> query = em.createQuery(criteriaQuery);

List<Object[]> result = query.getResultList();

for (Object[] s : result) {
    System.out.println(s[0] + " | " + s[1]);
}
虽然还没学,但基本的东西看了一遍应该就了解了, 其实编程方式的关键就是一创建查询根, 二使用JPA的API来进行各种条件和其他设置. 基本上与JPQL和SQL是对应的. CriteriaBuilder是核心的类, 通过其创建CriteriaQuery对象以及所有的子句中的表达式, SQL子句关键式则是CriteriaQuery的同名方法. 此外CriteriaQuery方法还需要创建根, 所有的SQL列名都是使用根的.get("columnName"). 还有一大特点是类似root.<Long>get("id")这样的代码, 泛型写在get()方法之前, 表面这列的数据类型, 这一点要注意. 知道了这些对应关系之后, 剩下的就是来看看一下API.

CriteriaBuilder的条件API

CriteriaBuilder的条件API非常重要, JPQL所有的条件查询都可以转换成CriteriaBuilder的API.
CriteriaBuilder的条件API
JPQL操作符 条件查询API 说明
. 没有对应的API 这表示导航路径, 即对象名.属性名, 可以连用.
取反 nge() 参数是数值
+,- sum(),diff() 参数为两个参数
*,/ prod(),quot() 两个数值乘除
=, <>, <, >, >=, <= equal(), notEqual(), lessThan()=lt(), greaterThan()=gt(), greaterThanOrEqualTo()=ge(), lessThanOrEqualTo()=le() 二元比较运算符, 比较运算方法和上边的加减乘除方法仅能接受Number类型的参数, 包括Java的基本类型, 以及BigDecimal和其他SQL类型中对应的Java的数值类型.
BETWEEN,LIKE,IN,IS(NOT)NULL between(),like(),in(),isNull(), isNotNull 具有SQL语义的二元比较操作符, 注意, NOT运算可以在条件之后写上一个.not()
IS(NOT)EMPTY, MEMBER isEmtpy(), isNotEmpty(), isMember, isNotMember 用于持久化集合的二元操作符
NOT, AND, OR not(), and(), or() 逻辑运算符, 逻辑运算符可以单独编写Predicate对象, 来应对复杂逻辑.
然后看一些比较特别的查询.

多态

首先是多态查询,JPQL中的多态类, 不一定非要是映射的类, 也可以是这些类的基类或者共同的接口类. 比如:
TypedQuery<Object> query = em.createQuery("SELECT s FROM java.lang.Object as s", Object.class);
这条语句会查出数据库中所有的内容. JPA没有多态查询对应的标准接口. 条件查询的API的.from()函数中, 参数只能是映射的类, 不能是随便的类比如Object, 所以下边的代码会报错:
CriteriaQuery<Object> criteriaQuery = cb.createQuery(Object.class);
Root<Object> root = criteriaQuery.from(Object.class);
TypedQuery<Object> query1 = em.createQuery(criteriaQuery);
运行这段代码会在from()函数的地方报错: IllegalArgumentException: Not an entity: class java.lang.Object. 所以不能一次性查出所有类. 对于多态映射, 可行的操作办法是在.from()中指定父类或者抽象类的映射类, 然后使用IN和 Root对象的.type()来进行查询. 比如之前使用的BillingDetails抽象类和两个实现类BankAccount及CreditCard. 可以将根设置为BillingDetails, 然后通过in或者equal来选择类型. 比如查询某一个具体类型的类:
CriteriaBuilder cb = em.getCriteriaBuilder();

//在创建查询和根的时候, 都可以用抽象类
CriteriaQuery<BillingDetails> billingDetailsCriteriaQuery = cb.createQuery(BillingDetails.class);
Root<BillingDetails> root = billingDetailsCriteriaQuery.from(BillingDetails.class);

//在查询实际类型的时候, 通过equal()函数和type()函数,指定查询BankAccount类
billingDetailsCriteriaQuery.select(root).where(cb.equal(root.type(), BankAccount.class));

//所有的泛型依然是抽象类的类型
TypedQuery<BillingDetails> query = em.createQuery(billingDetailsCriteriaQuery);

System.out.println(query.getResultList());
查询多个具体类型的类的时候, 需要用到root对象的type()的in()函数, 注意并不是CriteriaBuilder的IN函数, 所以说这是比较特别的地方:
CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<BillingDetails> billingDetailsCriteriaQuery = cb.createQuery(BillingDetails.class);
Root<BillingDetails> root = billingDetailsCriteriaQuery.from(BillingDetails.class);

//准备一个类型列表
List<Class<?>> types = new ArrayList<>();
types.add(BankAccount.class);
types.add(CreditCard.class);

//这里比较特殊, 类型的IN函数可不是CriteriaBuilder的IN函数.
billingDetailsCriteriaQuery.select(root).where(root.type().in(types));

//其他所有地方和上边一样, 依然是使用抽象类作为泛型类型
TypedQuery<BillingDetails> query = em.createQuery(billingDetailsCriteriaQuery);
System.out.println(query.getResultList());
这个例子的运行结果就是查出来了BankAccount和CreditCard两个具体实现类对应的数据表的内容.

CriteriaBuilder的IN函数

马上来一下CriteriaBuilder的IN函数. 这个函数的使用方法是先获取列, 然后in的每一个数字, 使用.value()来加在后边, 看一个例子:
//JPQL查询语句是: SELECT s FROM Sender as s WHERE s.name in ('gugugu27','gugugu28')

CriteriaQuery<Sender> criteriaQuery = cb.createQuery(Sender.class);

Root<Sender> root = criteriaQuery.from(Sender.class);

criteriaQuery.select(root).where(
        cb.in(root.<String>get("name"))
                .value("gugugu27").value("gugugu28")
);

TypedQuery<Sender> = query = em.createQuery(criteriaQuery);

枚举

对于枚举映射, 在JPQL查询语句中, 必须使用限定全程才行, 转换到编程方式中, 在取列的时候传入枚举类的类型, 参数则使用具体的枚举:
// SELECT a FROM Animal as a WHERE a.animalType = cc.conyli.model.AnimalType.OWL

Root<Animal> root = criteriaQuery.from(Animal.class);
criteriaQuery.select(root).where(cb.equal(root.<AnimalType>get("animalType"),AnimalType.OWL);

逻辑表达式

逻辑表达式的返回值都是一个javax.persistence.criteria.Predicate对象, 可以作为另外一个逻辑表达式的参数. 所以很复杂的逻辑表达式, 可以拆分成简单一些的. 看一个例子:
CriteriaBuilder cb = em.getCriteriaBuilder();

//JPQL查询
System.out.println(em.createQuery("SELECT s FROM Sender as s WHERE (s.id>55L and s.id<60L) or s.id>130L", Sender.class).getResultList());

CriteriaQuery<Sender> criteriaQuery = cb.createQuery(Sender.class);
Root<Sender> root = criteriaQuery.from(Sender.class);

//第一个判断是AND
Predicate predicate1 = cb.and(cb.gt(root.<Long>get("id"), 55L), cb.lt(root.<Long>get("id"), 60L));

//用上边AND的结果, 再进行OR判断
Predicate predicate2 = cb.or(predicate1, cb.gt(root.<Long>get("id"), 130L));

criteriaQuery.select(root).where(predicate2);
TypedQuery<Sender> query = em.createQuery(criteriaQuery);
System.out.println(query.getResultList());
很复杂的逻辑就可以像例子里一样逐步组装. 其他函数的操作和SQL都一样, like()函数的字符串依然也可以用_和&来匹配单个和多个字符, 该转义就转义.

CriteriaBuilder的集合和函数

这里的集合指的是值类型的集合, 不是关系. 关系的操作其实和集合也很相似, 但是放到后边的连表来学习. 此外cb还有一些方法对应了SQL中的常见函数. 首先要解决的问题其实很简单, 就是从Root对象取集合的时候, 泛型类型用什么, 以之前用过的LinkedHashSetItem为例, 其中包含一个Set<String>集合. LinkedHashSetItem表里现在有三个记录, 其中一个的images是0个, 查找所有集合不是0的LinkedHashSetItem对象的写法如下:
//JPQL 查询
System.out.println(em.createQuery("SELECT l FROM LinkedHashSetItem as l where size(l.images)>=1 order by l.id").getResultList());

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<LinkedHashSetItem> criteriaQuery = cb.createQuery(LinkedHashSetItem.class);
Root<LinkedHashSetItem> root = criteriaQuery.from(LinkedHashSetItem.class);
//注意泛型, 要是对应的集合类型才可以
criteriaQuery.select(root).where(cb.ge(cb.size(root.<LinkedHashSet<String>>get("images")), 1L));

TypedQuery<LinkedHashSetItem> query = em.createQuery(criteriaQuery);
System.out.println(query.getResultList());
这里使用了cb.size()函数外加取集合时候的泛型. 还可以搭配参数等等来使用, 比如:
//JPQL查询, 先用参数, 再设置参数
TypedQuery<LinkedHashSetItem> query1 =
        em.createQuery("SELECT l FROM LinkedHashSetItem as l where :image MEMBER OF l.images order by l.id", LinkedHashSetItem.class);
query1.setParameter("image", "home1");

//先创建好查询, 最后设置参数, 套路就是之前的, 所以可以看到编程的方式还是很灵活的
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<LinkedHashSetItem> criteriaQuery = cb.createQuery(LinkedHashSetItem.class);
Root<LinkedHashSetItem> root = criteriaQuery.from(LinkedHashSetItem.class);
criteriaQuery.select(root).where(cb.isMember(cb.parameter(String.class, "item"), root.<LinkedHashSet<String>>get("images")));
TypedQuery<LinkedHashSetItem> query = em.createQuery(criteriaQuery);
query.setParameter("item", "home1");
上边使用了.size()函数, 函数也是CriteriaBuilder的API, JPA提供的标准API如下:
JPA 函数 API
函数 说明
upper(s), lower(s) 转换字符串到大写或者小写
concat(s1,s2) 拼接字符串
currentDate(), currentTime(), currentTimestamp() 返回时间
substring(s, offset, length) 从字符串s中取offset开始位置的,长度为length的子串, offset是偏移, 偏移是从1开始计算的.
trim(TrimSpec.BOTH/LEADING/TRAILING, s) 对字符串s去空白, 还有重载方法, 可以去掉两端指定的字符和字符串.
length(s) s是字符串, 返回字符串的长度.
locate(search, s, offset) search为要查找的字符串, s为所在的字符串, offset表示从偏移几开始搜索, 返回找到的偏移量
abs(n), sqrt(n), mod(divident, divisor) 参数都是数值, 取绝对值, 平方根, 模(余数)
treat(X as Type) 向下类型转换. 比如如果一个属性是多态的, 可以用此参数. 该函数不是必须的, Hibernate会自动进行向下转换.
size(c) c是一个集合, 返回集合中元素的数量. 如果为空返回0
index(orderedCollection) 参数是一个集合, 用于指定@OrderedCollection中元素的位置, 比如查询: SELECT i.name FROM Category c JOIN c.items i WHERE index(i)=0, 这个查询会返回每个Category对象的items集合中的第一个item的名称.
直接把函数计算的结果作为查询结果的例子如下:
//JPQL查询
TypedQuery<String> query = em.createQuery("SELECT concat(s.name,s.name) FROM Sender as s", String.class);

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = cb.createQuery(String.class);
Root<Sender> root = criteriaQuery.from(Sender.class);
//select可以直接使用函数之后的结果
criteriaQuery.select(cb.concat(root.<String>get("name"), root.<String>get("name")));

TypedQuery<String> query1 = em.createQuery(criteriaQuery);

System.out.println(query1.getResultList());
SELECT可以直接选择函数的结果, 这里还没有使用多列, 其实select()函数可以接受多个参数, 表示多列. 这个会在后边详述. 上边都是JPA标准的函数, 接下来看看Hibernate对于函数的特有功能

Hibernate特有的函数和操作非JPA标准的函数的方法

在JPA标准之外, Hibernate还提供了一些函数:
Hibernate查询函数API
函数 说明
bit_length(s) 返回s的二进制位数
second(d), minute(d), hour(d),day(d),month(d),year(d) d是时间类型的参数, 用于提取时间和日期
minelement(c), maxelement(c),minindex(c),maxindex(c),elements(c),indices(c) 返回一个集合的最大/小元素,索引以及所有的元素或者索引, 需要集合支持索引
str(x) 尝试将参数转换成字符串
对于除了JPA标准和Hibernate这些原生的函数之外, 想要使用不属于这些函数中的函数, 任何在JPQL语句中, Hibernate无法识别的函数, 都会原样传递给数据库, 让数据库判断是不是支持该函数. 比如PostgreSQL有一个函数叫做INITCAP, 用来将每个单词开头第一个字母设置为大写:
TypedQuery<String> query = em.createQuery("SELECT INITCAP(s.name) FROM Sender s", String.class);
将上边这个改写成编程方式如下:
TypedQuery<String> query = em.createQuery("SELECT INITCAP(s.name) FROM Sender s", String.class);

System.out.println(query.getResultList());

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<String> criteriaQuery = cb.createQuery(String.class);
Root<Sender> root = criteriaQuery.from(Sender.class);

criteriaQuery.select(cb.function("INITCAP", String.class, root.<String>get("name")));

TypedQuery<String> query1 = em.createQuery(criteriaQuery);
System.out.println(query1.getResultList());
cb.function()用于执行JPA标准和Hibernate支持之外的函数, 第一个字符串是函数的名称, 第二个是这个函数的返回类型, 之后是这个函数的各个参数. 这里用编程的方式执行了PostgreSQL的函数INITCAP.

排序

排序在最开始的例子已经展示过了, orderBy方法内部使用cb.desc()cb.asc()来排序. 这里值得一提的就是根据多个列排序, 只要按照顺序在orderBy方法内列出多个列即可:
Root<User> u = criteria.from(User.class);
    criteria.select(u).orderBy(
        cb.desc(u.get("activated")),
        cb.asc(u.get("username"))
);
注意对于null的排序, 可以通过hibernate.order_by.default_null_ordering在配置文件中设置, 可选的值有NONE, FIRST, LAST, 和很多数据库的标准一致. 编写单个查询基本上弄明白了, 剩下就是再看一下, 选择多列以及关系的查询.
LICENSED UNDER CC BY-NC-SA 4.0
Comment