Hibernate 14 编写查询 - 查询多列和关系查询

Hibernate 14 编写查询 - 查询多列和关系查询

继续将JPQL转换成编程方式的查询, 这次是几个更高阶一点的问题. 搞完这部分, 日常编写查询应该是没有什么问题了. 多列查询 - 笛卡尔积 多列查询 - 组装DTO和使用DISTINCT 多列查询 - 分组 连表查询 - 隐式连接 连表查询 - 显式连接 连表查询 - 控制Fetch策略 连表查询

继续将JPQL转换成编程方式的查询, 这次是几个更高阶一点的问题. 搞完这部分, 日常编写查询应该是没有什么问题了.
  1. 多列查询 - 笛卡尔积
  2. 多列查询 - 组装DTO和使用DISTINCT
  3. 多列查询 - 分组
  4. 连表查询 - 隐式连接
  5. 连表查询 - 显式连接
  6. 连表查询 - 控制Fetch策略
  7. 连表查询 - 无关系映射的连表查询
  8. Hibernate标识符比较的特殊之处
  9. 子查询

多列查询 - 笛卡尔积

多列查询, 书里的技术术语管这个叫做投影. 其本质来说, 就不是直接按照对象, 而是选出多个列, 而列可能跨表也可能不跨表. 首先看如果从多个表中选择对象, 即创建两个表的笛卡尔积, 考虑这个笛卡尔积, 实际上相当于两个表中的所有对象互相组合一遍, JPA对于这种组合, 返回的依然是一个列表, 只不过这个列表的每一个元素都是一个数组, 放着两个元素. 这种查询使用多个Root对象, 以及特殊的写法. 有两种写法.
//写法一, 使用 cb.tuple()方法,传入多个Root对象,泛型是一个javax.persistence.Tuple对象
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> criteriaQuery1 = cb.createQuery(Tuple.class);
Root<Sender> root1 = criteriaQuery1.from(Sender.class);
Root<MessageVersion> root2 = criteriaQuery1.from(MessageVersion.class);

criteriaQuery1.select(cb.tuple(root1, root2));

TypedQuery<Tuple> query1 = em.createQuery(criteriaQuery1);
for (Tuple t : query1.getResultList()) {
    System.out.println(t.get(0) + "|" + t.get(1));
}

//写法二, 使用multiSelect(), 传入多个root对象, 泛型是Object[]类型
CriteriaQuery<Object[]> criteriaQuery2 = cb.createQuery(Object[].class);
Root<Sender> root3 = criteriaQuery2.from(Sender.class);
Root<MessageVersion> root4 = criteriaQuery2.from(MessageVersion.class);
criteriaQuery2.multiselect(root3, root4);

TypedQuery<Object[]> query2 = em.createQuery(criteriaQuery2);
for (Object[] t : query2.getResultList()) {
    System.out.println(t[0] + "|" + t[1]);
}
例子中是两个, 实际上可以传更多的root对象, tuple和数组也很类似, 通过索引拿结果. 这里依然要注意CriteriaQuery的泛型, 要与使用的select/multiSelect方法中的参数返回的类型一致. 这里是取笛卡尔积, 实际上还可以取其中的列组成笛卡尔积, 取列的时候, 使用multiSelect比较方便:
//JPQL查询, 查Sender和MessageVersion的笛卡尔积, SQL语句就是 SELECT * FROM sender, messageversion;
TypedQuery<Object[]> JPQLQuery = em.createQuery("SELECT s.name,m.currentDate FROM Sender s, MessageVersion m", Object[].class);

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> criteriaQuery = cb.createQuery(Object[].class);
Root<Sender> root1 = criteriaQuery.from(Sender.class);
Root<MessageVersion> root2 = criteriaQuery.from(MessageVersion.class);
//可以选择多个不同类型的列, 来自相同或者不同的Root对象都可以
criteriaQuery.multiselect(
        root1.<String>get("name"),
        root2.<Date>get("currentDate")
);

TypedQuery<Object[]> query = em.createQuery(criteriaQuery);
for (Object[] t : query.getResultList()) {
    System.out.println(t[0] + "|" + t[1]);
}
有了上边的方法, 就可以随意选择不同的列了, 不管是来自于同一个表, 还是跨表组成笛卡尔积, 都没有问题了.

多列查询 - 组装DTO和使用DISTINCT

在实际开发中, 很多时候需要DTO, 在Controller层和DAO层传递数据的时候, 很多时候使用DTO. JPA支持直接将查询出的结果组装到一个类中, 这个类不需要是Entity或者其他的映射类. 比如我们从MessageVersion类中取出currentDate和text两个字段, 然后组装到一个包含这两个字段的对象中. 首先创建一个DTO类:
import java.util.Date;

public class MessageSummary {

    private Date date;

    private String text;

    public MessageSummary(Date date, String text) {
        this.date = date;
        this.text = text;
    }

    @Override
    public String toString() {
        return "MessageSummary{" +
                "date=" + date +
                ", text='" + text + '\'' +
                '}';
    }
}
如果要自行组装的话, 需要先查出MessageVersion的两个字段, 然后遍历结果集, 创建MessageSummary的对象. JPA可以替代我们完成这个工作:
CriteriaBuilder cb = em.getCriteriaBuilder();
//注意查询的泛型, 这个泛型直接就是最终结果, 最终结果是要直接得到DTO类
CriteriaQuery<MessageSummary> criteriaQuery = cb.createQuery(MessageSummary.class);
Root<MessageVersion> root = criteriaQuery.from(MessageVersion.class);

//cb.construct函数, 第一个参数是DTO类, 之后的参数是查询的列, 顺序需要和DTO类的构造器一致
criteriaQuery.select(cb.construct(MessageSummary.class, root.<Date>get("currentDate"), root.<String>get("text")));

//最后直接查询得到DTO类
TypedQuery<MessageSummary> query = em.createQuery(criteriaQuery);

for (MessageSummary ms : query.getResultList()) {
    System.out.println(ms);
}

使用DISTINCT

在没有使用多列和多个表的笛卡尔积之前, 只是查询对象, 由于对象中包含唯一标识符, 所以不用担心重复问题. 在多列查询之后, 就有可能碰到某一列重复的问题, SQL语句的DISTINCT就是此时派上用场, 看一下JPA中如何使用DISTINCT. MessageVersion的Text字段中存在重复项, 现在只需要查出Text字段中的不重复项, 需要如下编写代码:
CriteriaQuery<String> criteriaQuery = cb.createQuery(String.class);
Root<MessageVersion> root = criteriaQuery.from(MessageVersion.class);

criteriaQuery.select(root.<String>get("text"));

criteriaQuery.distinct(true);

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

for (String ms : query.getResultList()) {
    System.out.println(ms);
}
这个控制相比SQL的DISTINCT, 粒度要稍微差了一些, 不过在查询值组成的列表的时候, 还是比较方便的.

多列查询 - 分组

分组中主要是GROUP BY 和HAVING子句的编写, 还需要了解Hibernate的一个特有问题, 即编写查询的时候一定要详细到具体的列, 而不是把路径引用停留在关系对象上.

分组 GROUP BY

分组是肯定需要使用聚合函数的. 关于函数的使用在上一节使用函数的时候, 就说明过了, select中可以加上函数的调用结果, WHERE子句中也可以使用函数. 最后查询结果的泛型类型要与函数的返回类型相匹配. 而不属于JPA标准的函数, 都通过cb.function()来使用. 这里写几个聚合函数的示例, 为了看分组:
CriteriaBuilder cb = em.getCriteriaBuilder();

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

//count与count去重, 任选其一
criteriaQuery.select(cb.count(root.<String>get("text")));
criteriaQuery.select(cb.countDistinct(root.<String>get("text")));

TypedQuery<Long> query = em.createQuery(criteriaQuery);
//如果取单个结果, 就是泛型类型
System.out.println(query.getSingleResult());
//虽然是一个结果, 也可以取List, 只包含一个元素
System.out.println(query.getResultList());
单独使用聚合函数, 就会将结果聚集为一行, 等于将所有元素看做一个组. 根据分组的原则, 即SELECT之后出现在聚集函数之外的列, 必须是GROUP BY之后出现的列, 所以要么单独使用, 要么就需要进行分组. 分组就是编写GROUP BY 子句和HAVING子句. 先在multiSelect中选择要分组的列或者聚合函数, 然后再单独调用CriteriaQuery.groupBy()函数. 这里先看一个不涉及关系的查询, 即同一个表内的分组:
//统计MessageVersion表中按照text字段分组的数量:

//JPQL查询
List<Object[]> result = em.createQuery("SELECT count(s.text), s.text FROM MessageVersion as s GROUP BY s.text ORDER BY s.text desc ", Object[].class).getResultList();

CaveatEmptorUtil.printObjectArray(result);

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> criteriaQuery = cb.createQuery(Object[].class);
Root<MessageVersion> root = criteriaQuery.from(MessageVersion.class);

criteriaQuery.multiselect(cb.count(root.<String>get("text")), root.<String>get("text"));
criteriaQuery.groupBy(root.<String>get("text"));
criteriaQuery.orderBy(cb.desc(root.<String>get("text")));

TypedQuery<Object[]> query = em.createQuery(criteriaQuery);
和SQL的要求一样, SELECT后边除了被聚集的列之外, 必须出现在GROUP BY后边, 这里没有被聚集的列就是root.<String>get("text").

Hibernate独特的分组问题

关于分组有个问题, 就是JPA在根据分组的时候, Hibernate不会自动将SELECT后边的对象拆分成id, 但在判断相等的时候会拆分. 看一下这个问题. Sender类中有一个@ManyToOne, 多对一对应到MessageVersion类, 如果按照所属的MessageVersion类来统计Sender的数量, 写出的SQL语句如下:
SELECT Count(id), messageversion_id FROM sender GROUP BY messageversion_id ORDER BY messageversion_id
现在将其转换成JPQL语句: 如果按照之前的面向对象的写法, 很自然是用对象来映射外键, 所以可能写出如下的查询:
CaveatEmptorUtil.printObjectArray(em.createQuery("SELECT count(s), s.messageVersion FROM Sender as s GROUP BY s.messageVersion ORDER BY s.messageVersion", Object[].class).getResultList());
写这个查询实际上是期待Hibernate会自动将s.messageVersion转换成s.messageVersion.id, 但这与判断相等不同, Hibernate不会转换成s.messageVersion.id, 而是会将s.messageVersion看成需要查询另外一个表中的所有字段. 所以Hibernate生成的SQL是:
Hibernate:
    select
        count(sender0_.id) as col_0_0_,
        sender0_.messageVersion_id as col_1_0_,
        messagever1_.id as id1_5_,
        messagever1_.currentDate as currentD2_5_,
        messagever1_."text" as text3_5_,
        messagever1_."version" as version4_5_
    from
        Sender sender0_
    inner join
        MessageVersion messagever1_
            on sender0_.messageVersion_id=messagever1_.id
    group by
        sender0_.messageVersion_id
红色部分很显然, 不仅拆出了id字段, 还拆出了MessageVersion类的全部字段, 很显然, 这个SQL语句无法执行, 因为在聚合函数之外的列, 没有出现在GROUP BY子句中. 针对这种情况, 一定要明确的指出id, .操作符最后的路径, 不能停留在一个对象, 而是一定要是具体类型的字段才可以. 实际能够执行的JPQL查询如下:
em.createQuery("SELECT count(s), s.messageVersion.id FROM Sender as s GROUP BY s.messageVersion.id ORDER BY s.messageVersion.id", Object[].class);
转换成JPA查询如下:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object[]> criteriaQuery = cb.createQuery(Object[].class);
Root<Sender> rootSender = criteriaQuery.from(Sender.class);
//multiselect不能只到rootSender.<MessageVersion>get("messageVersion"), 必须连用get方法, 继续玩往下找到id
criteriaQuery.multiselect(cb.count(rootSender.<Long>get("id")), rootSender.<MessageVersion>get("messageVersion").<Long>get("id"));
//GROUP BY子句中依然是同样的查询方法
criteriaQuery.groupBy(rootSender.<MessageVersion>get("messageVersion").<Long>get("id"));
criteriaQuery.orderBy(cb.asc(rootSender.<MessageVersion>get("messageVersion").<Long>get("id")));

TypedQuery<Object[]> query = em.createQuery(criteriaQuery);
如果这里仅仅就使用到rootSender.<MessageVersion>get("messageVersion"), 生成的SQL语句实际上会使用一个CROSS JOIN, 结果导致查询的结果不正确.

HAVING

SQL中HAVING是对分组以后的结果进行操作, 和WHERE不同. 看一个例子, 将MessageVersion按照text字段进行分组, 然后查找以c开头的text的数量.
//JPQL查询
em.createQuery("SELECT count(m),m.text FROM MessageVersion m GROUP BY m.text having m.text like 'c%' ORDER BY m.text", Object[].class)
    .getResultList();

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Object[]> criteriaQuery = cb.createQuery(Object[].class);
Root<MessageVersion> rootMV = criteriaQuery.from(MessageVersion.class);

criteriaQuery.multiselect(cb.count(rootMV.<String>get("text")), rootMV.<String>get("text"));
criteriaQuery.groupBy(rootMV.<String>get("text"));

//子句都是criteriaQuery的方法, having也不例外. having的参数也是一个Predicate, 所以可以用条件查询API
criteriaQuery.having(cb.like(rootMV.<String>get("text"), "c%"));

criteriaQuery.orderBy(cb.asc(rootMV.<String>get("text")));
通过观察实际生成的SQL语句, 可以发现两种查询实际生成的SQL几乎相同.

连表查询 - 隐式连接

应该说之前的查询, 还都是比较简单的. 当然也涉及到了一些关系查询的内容, 知道了一定要引用到具体字段, 而不是关系对象. 现在继续来看看比较复杂的内容, 就是连表查询. 首先看的是隐式连接, 也就是内连接, 因为在映射的过程中指定了关系映射的属性, 所以在查询的时候, 会自然的使用连接.
//SQL语句 SELECT sender.* FROM sender INNER JOIN messageversion m on sender.messageversion_id = m.id WHERE m.text = 'cony'

//查询Sender对应的MessageVersion类的text=cony的查询
TypedQuery<Sender> joinSender = em.createQuery("SELECT s FROM Sender as s WHERE s.messageVersion.text = 'cony'", Sender.class);

//生成的SQL语句是:
Hibernate:
    select
        sender0_.id as id1_6_,
        sender0_.messageVersion_id as messageV3_6_,
        sender0_.name as name2_6_
    from
        Sender sender0_ cross
    join
        MessageVersion messagever1_
    where
        sender0_.messageVersion_id=messagever1_.id
        and messagever1_."text"='cony'
可以看到这里就使用了连接, 实际上是一个隐式的内连接. 编写对应的JPA查询, 也不需要使用特别的连接API, 只需要连续选择属性即可:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Sender> criteriaQuery = cb.createQuery(Sender.class);
Root<Sender> root = criteriaQuery.from(Sender.class);

criteriaQuery.select(root).where(
        cb.equal(
                root.<MessageVersion>get("messageVersion").<String>get("text"), "cony"
        )
);

TypedQuery<Sender> query = em.createQuery(criteriaQuery);
这里如果仔细查看生成的SQL语句, 生成的是cross join. 只不过由于使用了id相等的条件, 从CROSS JOIN中去掉了不应该有的结果, 最后和内连接是一样的. 因为内连接就是相当于id相等, 然后不相等的列根本就不存在. 如果是多个关系, 可以继续使用多个get来获取, 每使用一个, 就会多一层连表.

连表查询 - 显式连接

相比隐式连接, 显式需要使用一个特别的类Join, 和Root对象的API协同来使用. 比如显式编写上边的内连接:
CriteriaBuilder cb = em.getCriteriaBuilder();

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

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

//Join对象, 也是一个根, 表示将Sender和MessageVersion进行内连接的对象
//这里的join函数表示Sender通过Sender的属性messageVersion指向的表进行连表. join函数有很多重载, 第二个参数可以传入Join类型. 这里仅使用单参数表示内连接.
Join<Sender, MessageVersion> join = root.join("messageVersion");

criteriaQuery.select(root).where(
        cb.equal(
                join.<String>get("text"), "cony"
        )
);
仔细观察上边的显式连接与隐式连接的区别, 很显然多了一个Join对象, 但是在取列的时候, 少了一个连续的get, Join对象可以认为是一个连了表之后的结果,其中包含了text字段, 所以直接引用即可. 如果使用外连接来查询, 就需要在.join()方法中传入连接类型. 比如我们知道, 左连接中, 即使右侧不符合条件的, 也会显示出来. 不过外连接通常用于动态抓取, 而不是普通查询. 看一个内外连接的区别:
//SQL 查询SELECT * FROM messageversion inner join sender s on messageversion.id = s.messageversion_id ORDER BY messageversion.id

//JPA查询, 内连接无需显式编写连接语句
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);
Root<MessageVersion> messageVersionRoot = criteriaQuery.from(MessageVersion.class);
Root<Sender> senderRoot = criteriaQuery.from(Sender.class);

criteriaQuery.multiselect(messageVersionRoot, senderRoot).where(
        criteriaBuilder.equal(messageVersionRoot.get("id"),senderRoot.get("messageVersion"))
);

criteriaQuery.orderBy(criteriaBuilder.asc(messageVersionRoot.<Long>get("id")));
现在将连接方式修改一下, 看看结果:
// SQL查询 SELECT * FROM messageversion LEFT JOIN sender s on messageversion.id = s.messageversion_id ORDER BY messageversion.id

//JPQL查询
em.createQuery("SELECT m,s FROM MessageVersion as m LEFT JOIN Sender s on m = s.messageVersion ORDER BY m.id", Object[].class);

CaveatEmptorUtil.printObjectArray(em.createQuery("SELECT m,s FROM MessageVersion as m LEFT JOIN Sender s on m = s.messageVersion ORDER BY m.id", Object[].class).getResultList());

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);
Root<MessageVersion> messageVersionRoot = criteriaQuery.from(MessageVersion.class);
Join<MessageVersion, Sender> join = messageVersionRoot.join("senders", JoinType.LEFT);
criteriaQuery.multiselect(messageVersionRoot, join);

CaveatEmptorUtil.printObjectArray(em.createQuery(criteriaQuery).getResultList());
对比之后可以发现结果的不同. 这里有如下几点值得注意:
  1. join()方法由于是在FROM子句中JOIN, 所以这个方法是Root对象的方法.
  2. 哪个Root对象调用join方法, 这个Root对象就是FROM之后的表, join参数中的对象, 则相当于XXX JOIN之后的表.
  3. 只要使用了JOIN, 默认会自带 ON roo1.id = root2.relation_id 这个条件, 因为Hiberante知道具体的关系映射, 所以无需再写其他条件.
  4. 如果还需要额外的ON条件, 需要使用Join对象的.on方法, 其中参数是一个Predicate表达式.
  5. FROM A XXX JOIN B, Join的泛型需要先写A,再写B, 然后从A的关系映射中获取B. 之后传入JoinType来创建Join对象
  6. 查询的时候, 如果需要查询多个表, multiselect中必须先传入A, 在传入Join<A,B>, 这样才表示连表查询.
再来写一个带额外的ON条件, ON中的条件是连接条件:
// s.name小于等于8的项目不参加连接
// SQL查询 SELECT * FROM messageversion LEFT JOIN sender s on messageversion.id = s.messageversion_id and length(s.name) > 8 ORDER BY messageversion.id;

//JPA查询, 相比原来的多了红色部分
CaveatEmptorUtil.printObjectArray(em.createQuery("SELECT m,s FROM MessageVersion as m LEFT JOIN Sender s on (m = s.messageVersion) and length(s.name) > 8 ORDER BY m.id", Object[].class).getResultList());

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);
Root<MessageVersion> messageVersionRoot = criteriaQuery.from(MessageVersion.class);
Join<MessageVersion, Sender> join = messageVersionRoot.join("senders", JoinType.LEFT);

//Join对象的ON方法, 注意只需要编写上边红色的条件, 而不需要编写m = s.messageVersion对应的语句,因为这是隐含的.
join.on(criteriaBuilder.gt(
        criteriaBuilder.length(join.<String>get("name")), 8
        )
);
criteriaQuery.multiselect(messageVersionRoot, join);
criteriaQuery.orderBy(criteriaBuilder.asc(messageVersionRoot.<Long>get("id")));
知道了这个隐含的ON条件, 就可以来看连表操作的原则: 对于外连接, JPA和Hibernate都不支持没有关系映射的类. 也就是说想使用外连接连接两个类的话, 这两个类一定都要是Entity或者集合, 然后有关系映射或者集合映射, 否则无法连表. 因为通过关系id令其相等这个ON条件是内置的, 无法改变. 额外的ON条件, 只是附加于这个内置条件之上; 对于内连接, 不需要使用Join对象, 只要编写WHERE条件搭配multiselect使用或者通过.操作符引用到关系的具体属性即可.

连表查询 - 控制Fetch策略

外连接表我们知道是通过关系映射来实现的. 在之前的关系映射中, 我们知道最好都将FetchType设置为LAZY, 然后根据需要可以具体使用急抓取的查询, 即JPQL中的fetch join关键字. 在JPA编程方式的查询中, 也有对应的控制语句, 专门用来详细控制Fetch策略. 比如我们通过MessageVersion来急抓取所有对应的Senders:
//JPQL查询
em.createQuery("SELECT m FROM MessageVersion as m left join fetch m.senders").getResultList();


CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<MessageVersion> criteriaQuery = criteriaBuilder.createQuery(MessageVersion.class);
Root<MessageVersion> root = criteriaQuery.from(MessageVersion.class);

//Root对象的fetch, 这种fetch由于是直接查找关系对象, 所以第二个参数传入JoinType.LEFT
root.fetch("senders", JoinType.LEFT);
criteriaQuery.select(root);

CaveatEmptorUtil.printList(em.createQuery(criteriaQuery).getResultList());
这个查询实际上会返回一个MessageVersion的列表, 每个MessageVersion对象中的senders属性都已经初始化. 这里还使用了左连接的方式, MessageVersion的senders属性即使为空也会查出来结果. 如果不这么查询, 仅仅使用 Select m FROM MessageVersion m, 则其中的LAZY加载的部分并不会被初始化. 所以使用Join Fetch连接自己的关系对象是一个常见的JPA编程方式的套路. 不过要注意的是, 在这种查询中, JPA/Hibernate可以像之前学习的那样使用DISTINCT函数, 但是实际上SQL中并不会包含DISTINCT, 去重是在内存中进行的. 这里使用的是LEFT JOIN FETCH, 还有RIGHT JOIN FETCH, 这两种都是外连接. 如果使用 JOIN FETCH, 就是内连接. 如果将上边的root.fetch("senders", JoinType.LEFT);改成内连接, 则不会查询出senders数量是0的MessageVersion对象. 注意这里的细微区别. 还需要注意的一点是, 如果一个表有多个关联关系, 对一个Root对象反复调用.fetch来加载不同的关系, 会造成笛卡尔积, 所以需要根据实际情况选用. 如果急抓取一个集合, 则不能在数据库层面进行分页, 因为每个对象的关系对象的数量未知, 所以不能简单的使用分页. 比如有B对象多对一关联A对象, 不能在A连B查询的时候使用LIMIT, 因为不知道A能连到多少B. 正确的做法应该是先查出来每个A对应几个B, 然后再通过程序处理来显示. 这里还需要注意的是, 一般关系映射可能仅仅映射多对一的那一方面, 映射在那边, 就只能Root.get()对应的名称来获取, 没有映射的部分, 则不能直接连表. 这和SQL只有单方向外键就可以两方向连表是不同的.

连表查询 - 无关系映射的连表查询

之前的隐式和显式连表查询, 都来自于有关联关系映射的类. 如果两个不相关的类, 但是有字段相同, 这个时候就可以使用multiselect直接从这两个Root对象中进行选择, 然后搭配ON函数指定条件. 这实际上是内连接查询. User类中有username列, sender中有name列, 现在需要查询所有User中username与sender的name相等的对象, 只要二者的对应列相等, 就连接成为一行.
//SQL查询: SELECT * FROM sender, users WHERE sender.name = users.username ORDER BY sender.id
//或者写成: SELECT * FROM sender JOIN users on sender.name = users.username ORDER BY sender.id

//JPQL查询, 一个使用连表, 一个使用从笛卡尔积中使用WHERE, 本质相同
em.createQuery("SELECT s,u FROM Sender AS s JOIN User AS u ON s.name = u.username ORDER BY s.id", Object[].class);
em.createQuery("SELECT s,u FROM Sender AS s, User AS u WHERE s.name = u.username ORDER BY s.id", Object[].class);

//编程方式就无须使用连表, 直接按照WHERE条件来查询即可
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

CriteriaQuery<Object[]> criteriaQuery = criteriaBuilder.createQuery(Object[].class);

Root<Sender> senderRoot = criteriaQuery.from(Sender.class);
Root<User> userRoot = criteriaQuery.from(User.class);

//直接进行投影然后使用WHERE即可, 无须使用连表API
criteriaQuery
        .multiselect(senderRoot, userRoot)
        .where(
                criteriaBuilder.equal(
                        senderRoot.<String>get("name"),
                        userRoot.<String>get("username")
                )
        )
        .orderBy(criteriaBuilder.asc(senderRoot.<Long>get("id")));

Hibernate标识符比较的特殊之处

在之前的查询中是否还记得, JPQL中可以直接比较两个对象, 也可以比较两个具体的字段. 像上边例子中的:
em.createQuery("SELECT s,u FROM Sender AS s, User AS u WHERE s.name = u.username ORDER BY s.id", Object[].class);
红色部分就是在比较两个具体的字段. 如果.操作符后边的路径指向一个具体对象, 那么比较具体对象也是可以的, 此时引擎会比较两个对象的唯一标识符. Hibernate有一个特殊之处就是, 用@Id注解的成员变量, 无论其名称叫什么, 在JPQL中, 固定就叫做.id属性. 所以最好将Entity类的唯一标识符属性, 就起名称叫做id. 这个不是JPA的标准.

子查询

子查询是SQL的一大功能, 由于查询的结果是一个关系, 所以这个关系又可以用在其他需要关系的地方, SQL可以在SELECT, FROM和WHERE子句中使用子查询, 非常灵活. JPA仅仅支持WHERE子句中的子查询, 不支持其他位置的子查询, 这是因为无法传递闭包. ORM将结果映射到一个数据结构上, 其实就完成了一个子查询, 可以用业务来处理这些结果. 子查询可以与查询的其他位置相关, 也可以不相关 来编写一个子查询, 查出 MessageVersion的Senders大于1的所有MessageVersion对象. 用SQL可以很快的写出一个将聚合函数的结果当做标量的子查询:
SELECT * FROM messageversion WHERE (Select count(*) FROM sender WHERE sender.messageversion_id = messageversion.id) >1 ORDER BY messageversion.id;
对于messageversion表中每个记录, 去聚合计算对应的sender的数量,大于1的选出来. JPQL查询也很容易写出来,这里就用了之前提到的标识符比较可以直接比较对象的方式:
em.createQuery("SELECT m FROM MessageVersion as m WHERE (SELECT count(s) FROM Sender as s WHERE s.messageVersion = m) >1 ORDER BY m.id", MessageVersion.class);
用编程的方式, 需要解决的就是红色部分的子查询如何编写. 需要创建一个外层查询对象和一个内层查询对象, 根对象也需要由不同的查询对象进行创建, 这比原来的无脑用criteriaQuery对象要复杂一些:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

//创建外层查询对象
CriteriaQuery<MessageVersion> criteriaQuery = criteriaBuilder.createQuery(MessageVersion.class);

//MessageVersion的根, 用于外层查询
Root<MessageVersion> messageVersionRoot = criteriaQuery.from(MessageVersion.class);

//通过criteriaQuery创建一个子查询对象, 泛型类型是查询的结果类型
Subquery<Long> subquery = criteriaQuery.subquery(Long.class);

//创建子查询的根, 注意是用子查询对象的from, 而不是criteriaQuery
Root<Sender> senderRoot = subquery.from(Sender.class);

//子查询的根是senderRoot, 先设置子查询对象
//这里注意WHERE子句中 s.messageVersion = m 转换成编程方式的写法, 根对象可以直接认为就是那一个类的具体对象
subquery.select(criteriaBuilder.count(senderRoot)).where(
        criteriaBuilder.equal(
                senderRoot.<MessageVersion>get("messageVersion"),
                messageVersionRoot
        )
);

//再进行外层查询的设置, 注意subquery的泛型是Long, 其实就相当于一个Long类型, 用gt函数可以比较大小.
criteriaQuery.select(messageVersionRoot).where(
        criteriaBuilder.gt(subquery, 1L)
).orderBy(criteriaBuilder.asc(messageVersionRoot.get("id")));
注意这里实际上编写了两个查询, 先创建了外层查询对象, 然后通过外层查询对象的subquery方法创建了属于这个外层对象的子查询对象. 然后外层查询的根使用外层查询对象创建, 子查询的根则使用子查询对象来创建, 这里一定要注意红色部分, 不要写错了. 之后像普通查询一样设置子查询的各个查询条件, 设置完成之后, 再设置外层查询. 在外层查询中, 可以直接将子查询对象当成其结果类型(泛型类型)来使用. 代码虽然有点绕, 但整体还是比较清晰的. 这里还要注意的是, 这是一个相关子查询, 因为在设置子查询的时候, 使用到了外层的Root对象, 从SQL语句里也能看出来, 子查询使用了外层的MessageVersion的别名. 相关子查询不能拆分成完全独立的查询, 所以相关子查询在数据库中的实际运行类似于连表的开销. 不相关的子查询在编写之前, 应该考虑将其拆分成两个查询或者简单条件, 因为不会彼此引用. SQL中还有 ALL, ANY, SOME这几个限定比较, 可用于比较一个标量与一个向量. JPA也有对应的操作, 也是criteriaBuilder对象的方法. 上一个例子了查询了所有Sender数量大于1的MessageVersion的结果. 这里需要查出, MessageVersion对应的Sender中name长度全部小于5的MessageVersion对象. 先编写SQL, 这里需要使用到ALL限定符号:
SELECT *
FROM messageversion
WHERE 5 > all (SELECT length(sender.name) FROM sender WHERE sender.messageversion_id = messageversion.id)
ORDER BY messageversion.id;
可以看到子查询是查出一个向量, Sender表的name列的长度, 然后使用 5 > all 限定比较, 表示向量中的所有长度都必须小于5. JPQL的查询也很简单, 将SQL翻译一下即可:
em.createQuery("SELECT m FROM MessageVersion as m WHERE 5 > all (SELECT length(s.name) FROM Sender as s WHERE s.messageVersion = m) ORDER BY m.id", MessageVersion.class);
按照上边的套路, 来编写一个JPA查询:
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();

//外层查询对象
CriteriaQuery<MessageVersion> criteriaQuery = criteriaBuilder.createQuery(MessageVersion.class);
//使用外层查询对象创建外层Root
Root<MessageVersion> messageVersionRoot = criteriaQuery.from(MessageVersion.class);

//创建子查询, 子查询查的是length的结果,就是long类型
Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
//使用子查询创建子查询的Root对象
Root<Sender> senderRoot = subquery.from(Sender.class);

//先设置子查询
subquery
        .select(criteriaBuilder.length(senderRoot.<String>get("name")))
        .where(criteriaBuilder.equal(
                senderRoot.<MessageVersion>get("messageVersion"),
                messageVersionRoot
                )
        );

//再设置外层查询
criteriaQuery.select(messageVersionRoot)
        .where(
                criteriaBuilder.gt(
                        //这里一定要用literal,来转换, 否则不能直接和all进行比较
                        criteriaBuilder.literal(5),
                        criteriaBuilder.all(subquery)
                )
        )
        .orderBy(criteriaBuilder.asc(messageVersionRoot.get("id")));
这里有两个要注意的地方, 就是红色的部分, 一个是因为5是基本类型, 不能和后边的all方法返回的结果比较, 这里需要使用literal函数将字面量转换成一个Expression<?>. 还有就是all方法, 可以看到其接受的参数就是一个SubQuery对象. 这是all方法, 其他的方法是criteriaBuilder.any(SubQuery)criteriaBuilder.exists(SubQuery)方法. 到这里就把各种编写查询都看完了, 由于ORM的限制, 在连表操作方面, 不如SQL那么自如, 不过这也是因为ORM严格映射的关系, 只要遵循映射, 所有的查询还是都有办法编写出来的. 剩下的部分都是Hibernate中特有的内容, 这部分就暂时不看了. 最后看一下设计DAO方面的东西了, 持久化这一块内容到现在基本上算补完了, 后边回头再补一下PgSQL的一些内容, 就可以再回到Spring继续看了.
LICENSED UNDER CC BY-NC-SA 4.0
Comment