Hibernate 12 查询接口

Hibernate 12 查询接口

映射看完了, 抓取策略也看完了, 剩下就是最核心的查询了. 查询永远是数据库操作的核心, 相比其他的UPDATE DELETE优先度高很多. 今天已经二月了, 开始继续看吧. 今年的任务就是再搞一遍Spring框架之后, 还是回头老老实实学数据结构和算法, 然后来刷点题目, 提升基本功了. 女儿也快

映射看完了, 抓取策略也看完了, 剩下就是最核心的查询了. 查询永远是数据库操作的核心, 相比其他的UPDATE DELETE优先度高很多. 今天已经二月了, 开始继续看吧. 今年的任务就是再搞一遍Spring框架之后, 还是回头老老实实学数据结构和算法, 然后来刷点题目, 提升基本功了. 女儿也快到了可以学编程的年纪了, 当爹的我可不能落下了.
  1. 查询基础理论
  2. 创建查询 - Query 和 TypedQuery
  3. 创建查询 - CriteriaQuery
  4. 创建查询 - Hibernate的查询接口
  5. 准备查询
  6. 执行查询
  7. 命名查询
  8. 查询提示 - hints

查询基础理论

在JPA中使用查询有三个步骤:
  1. 创建查询
  2. 准备该查询, 包括传递参数, 设置提示和分页选项等
  3. 运行查询获取结果
针对这三个步骤, JPA都提供了标准的接口, Hibernate也有属于自己独特的接口, 这里先来看标准的接口, 也就是基于EntityManager的使用.

创建查询 - Query 和 TypedQuery

JPA提供了两大类查询接口, 一类是直接通过JPQL创建查询, 一类是通过类来创建类型安全的查询. 第一类的接口是Query和TypedQuery, 第二类的接口是CriteriaQuery, 包括带与不带泛型的两个版本, 对应第一类中的两个接口. 这两大类都支持条件查询和直接查询. 在之前的各种例子中一直使用的em.createQuery("SELECT m FROM MessageVersion m")方法返回的就是一个Query对象:
Query query = em.createQuery("SELECT m FROM MessageVersion m");
当然, 我们都知道这里还可以传入第二个参数, 也就是实体类的class, 这可以让结果类型安全, 其实这恰好返回的就是一个TypedQuery对象:
TypedQuery<MessageVersion> query = em.createQuery("SELECT m FROM MessageVersion m", MessageVersion.class);
List<MessageVersion> messageVersionList = query.getResultList();
这时候就类型安全了. Query和TypedQuery都支持条件查询, 也就是像JDBC一样, 避免SQL注入的占位符查询:
TypedQuery<MessageVersion> query = em.createQuery("SELECT m FROM MessageVersion m WHERE m.id = :id", MessageVersion.class).setParameter("id", 4L);

MessageVersion messageVersion = query.getSingleResult();
这就是Query和TypedQuery接口的简单介绍. 相比之下, 如果要使用这一类接口, 最好还是使用类型相对安全的TypedQuery.

创建查询 - CriteriaQuery

CriteriaQuery是用类的方式将JPQL转换成一系列类的操作, 从而避免了类型不安全. CriteriaQuery可以不使用泛型, 也可以使用,看一个不用泛型的例子如下:
public class BasicSearchInterfaces {

    public static EntityManager getEM() {
        EntityManagerFactory emf =
                CaveatEmptorUtil.getEntityManagerFactory();
        return emf.createEntityManager();
    }

    public static EntityManagerFactory getEMF() {
        return CaveatEmptorUtil.getEntityManagerFactory();
    }

    @Test
    public void testBasicInterfaces() {
        EntityManager em = getEM();
        em.getTransaction().begin();

        CriteriaBuilder cb = em.getCriteriaBuilder();

        //也可以从EMF中获取
        //CriteriaBuilder cb2 = getEMF().getCriteriaBuilder();

        //创建一个CriteriaQuery对象
        CriteriaQuery criteriaQuery = cb.createQuery();

        //.select表示查询, 然后传入.from()函数的结果, 这行语句从左向右念, 就很像SQL语句的 SELECT FROM 顺序
        criteriaQuery.select(criteriaQuery.from(MessageVersion.class));

        //依然使用em.createQuery重载方法来创建查询
        Query query = em.createQuery(criteriaQuery);

        //使用查询获取结果
        List<MessageVersion> messageVersionList = query.getResultList();

        System.out.println(messageVersionList);

        em.getTransaction().commit();
    }
}
当然, 在这么写的过程中, IDE就会提示CriteriaQuery是泛型类, 这里直接使用不带泛型的版本不好, 不带参数的cb.createQuery()实际返回的是CriteriaQuery<Object>, 所以还是赶快来看一下带泛型的版本:
CriteriaQuery<MessageVersion> criteriaQuery = cb.createQuery(MessageVersion.class);
criteriaQuery.select(criteriaQuery.from(MessageVersion.class));

//查看源码可以知道, 这个重载方法是: <T> TypedQuery<T> createQuery(CriteriaQuery<T> var1);
TypedQuery<MessageVersion> query = em.createQuery(criteriaQuery);

List<MessageVersion> messageVersionList = query.getResultList();
CriteriaQuery同样也支持条件查询, 不过写起来稍微有点绕:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<MessageVersion> criteriaQuery = cb.createQuery(MessageVersion.class);

//相当于单独抽出了.from()函数的结果, 可以将其认为是查到的那个结果
Root<MessageVersion> m = criteriaQuery.from(MessageVersion.class);

//这里要注意的是, 使用CriteriaBuilder创建一个WHERE条件, 这里用的是equal()方法, 第一个参数是属性名称, 第二个是值
//这行语句念起来也很像 SELECT ... FROM ... WHERE id = 4
criteriaQuery.select(m).where(cb.equal(m.get("id"), 4L));

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

MessageVersion messageVersion = query.getSingleResult();

System.out.println(messageVersion);
注意其中红色的部分, 生成WHERE条件用的是CriteriaBuilder对象的方法. 当然这里的属性名称("id")还是用字符串表示的, 想完全类型安全, 就要用元模型, 这个以后再研究.

创建查询 - Hibernate的查询接口

老套路, 看完了JPA标准, 自然要来看看Hibernate的查询接口, Hibernate的查询接口实际上比JPA标准还要早, 毕竟Hibernate引领了JPA诞生. Hibernate的查询接口是:
  1. org.hibernate.Query, 已过时, 被org.hibernate.query.Query替代
  2. org.hibernate.SQLQuery, 已过时, 被org.hibernate.query.NativeQuery替代
  3. org.hibernate.Criteria
第一个接口的使用方法和JPA标准很类似, 直接就用类型安全的查询:
Query<MessageVersion> query = session.createQuery("SELECT m FROM MessageVersion m", MessageVersion.class);
Query<MessageVersion> query1 = session.createQuery("SELECT m FROM MessageVersion m WHERE m.id = :id", MessageVersion.class).setParameter("id", 4L);
SQLQuery相比之下比较刺激一些, 用原生的SQL夹杂面向对象:
NativeQuery query = session.createSQLQuery("SELECT {m.*} FROM messageversion {m}").addEntity("m", MessageVersion.class);
System.out.println(query.getResultList());
NativeQuery其实是带有泛型, 但是IDE提示说由于createSQLQuery方法返回的就是不带泛型的NativeQuery对象, 所以无需使用泛型. 对于最后一个Criteria, 我看书上说这是一个古老的API, 结果写起来发现session.createCriteria()方法也已经过时, 这里就不再看了.

准备查询 - 绑定命名参数 和 绑定顺序参数

准备查询主要有如下工作:
  1. 绑定命名参数
  2. 绑定顺序参数
  3. 分页
  4. Hibernate特有的游标查询
先来看JPA标准的准备查询. 刚才在前边的创建查询中, 使用的JPQL语句: SELECT m FROM MessageVersion m WHERE m.id = :id中的:id就是命名参数, 用一个冒号加上参数名称来表示. 有了命名参数之后, 就要在查询中进行绑定. 绑定的方法是setParameter()方法. 在上边可以看到, setParameter()方法有两个参数,第一个是参数名称, 第二个是要绑定参数的值. setParameter()方法还有一个重载, 专门用于时间类型, 前两个参数不变, 最后一个参数指定具体的类型, 使用TemporalType中的三个类型来指定具体时间类型. 注意, 除了基本类型值以外, 绑定命名参数(包括其他参数), 用于关系对象的时候, 在绑定的时候可以直接传入具体的实体类对象的引用, 比如:
Query<Sender> senderQuery = em.createQuery("SELECT s FROM Sender s WHERE s.messageVersion = :mv order by s.id", Sender.class);
MessageVersion messageVersion = em.find(MessageVersion.class, 4L);
senderQuery.setParameter("mv", messageVersion);
System.out.println(senderQuery.getResultList());
这个例子会查出来Sender中所有MessageVersion的id为4的结果, 这是JPQL/HQL面向对象的优点, 实际生成的语句是比较id, 但是这里看上去像直接比较两个对象是否相等. 使用CriteriaQuery绑定参数的方法如下:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<MessageVersion> criteriaQuery = cb.createQuery(MessageVersion.class);
Root<MessageVersion> m = criteriaQuery.from(MessageVersion.class);

//注意红色部分, 实际上相当于将原来equal方法第二个参数, 从一个具体值替换成了一个 cb.parameter() 方法的结果, 这个方法第一个参数是参数类型, 第二个参数是参数的名称
criteriaQuery.select(m).where(cb.equal(m.get("id"), cb.parameter(Long.class, "number")));

//在准备查询的时候, 设置参数的名称时, 就需要使用cb.parameter()中指定的方法
TypedQuery<MessageVersion> query = em.createQuery(criteriaQuery).setParameter("number", 4L);
其实上边这个绑定参数的方法, 写成JPQL就是 SELECT m FROM MessageVersion m WHERE m.id = :number, 这么看就明白多了. 绑定顺序参数不太常用, 而且也不类型安全:
Query query = em.createQuery("SELECT m FROM MessageVersion m WHERE m.id> ?1 and m.id < ?2 order by m.id", MessageVersion.class);

query.setParameter(1, 4L);
query.setParameter(2, 6L);

System.out.println(query.getResultList());
其中的两个红色部分, 就是顺序参数, 这个顺序从1开始, 不是从0开始. 顺序参数设置的时候, 依然使用.setParameter()重载方法, 第一个参数是位置序号, int类型, 第二个就是参数绑定的值. 这个有点类似JDBC的占位符, 但要注意, Hibernate要求一定要带序号, 否则会警告这是一个易损查询. 不能同时使用绑定命名参数和绑定顺序参数. 如果要在二者之间选择其一, 推荐使用绑定命名参数的CriteriaQuery方式. 还有一个常用的功能是分页. 你可能会想在JPQL中使用LIMIT和OFFSET即可, 但实际尝试一番发现JPQL中不能使用LIMIT, Query实际上有两个专门的设置用来分页:
Query query = em.createQuery("SELECT s FROM Sender s ORDER BY s.id", Sender.class);

query.setFirstResult(10).setMaxResults(5);

System.out.println(query.getResultList());
这里的setFirstResult(10)相当于OFFSET 10, 跳过前10个结果; setMaxResults(5)相当于LIMIT 5, 所以这个查询就是按5个为一页, 从第三页开始的结果. 这个分页方式除了JPA标准的Query之外, 也能用于TypedQuery, 以及Hibernate自己的Query和NativeQuery. 使用这种方式, 就是采用编程式的获取页数, 要比编辑SQL语句方便的多. 还有一个Hibernate特有的游标查询, 实际上也是全部查出来数据, 然后可以自由的来反复查询结果.
@Test
public void testCursor() {
    SessionFactory sessionFactory = getSessionFactory();
    Session session = sessionFactory.getCurrentSession();

    session.getTransaction().begin();

    org.hibernate.query.Query<Sender> query = session.createQuery("SELECT s FROM Sender s order by s.id", Sender.class);

    //指定某种游标模式, 然后从Query中创建一个游标对象
    org.hibernate.ScrollableResults resultsWithCursor = query.scroll(ScrollMode.SCROLL_INSENSITIVE);

    //游标移动到最后
    resultsWithCursor.last();

    //当前的总数就是游标的位置+1, 可见游标是从位置0开始移动的, 这个索引像数组的索引.
    int total = resultsWithCursor.getRowNumber() + 1;
    System.out.println("总数量是: " + total);

    //可以关闭游标了, 其实此时所有数据都已经被查询出来
    resultsWithCursor.close();

    //可以反复多次执行查询
    query.setFirstResult(10).setMaxResults(5);
    System.out.println(query.getResultList());
    System.out.println("改变查询的分页");

    query.setFirstResult(5).setMaxResults(10);
    System.out.println(query.getResultList());

    session.getTransaction().commit();
    session.close();
    sessionFactory.close();

}
PostgreSQL是支持这种游标操作的. 同时也可以看出来面向对象的好处, 即可以查询可以先创建好, 之后通过不同的准备查询设置好参数, 然后可以复用.

执行查询

创建和准备查询并不会导致任何SQL语句被执行, 只有实际执行查询的时候, 才会触发SQL语句执行. 所谓执行查询, 就是在Query对象上调用各种方法来获取结果, 结果会由Hibernate自动按照映射来组装成对象, 如果查询结果是标量, Hibernate不会托管标量结果. 最常用的是将这个查询的全部结果包装进一个List中, 也就是前边用了无数次的getResultList()方法, 对于泛型的查询, 也需要用对应泛型类型的List来接着, 看一个标量的例子:
TypedQuery<String> query = em.createQuery("SELECT s.name FROM Sender s ORDER BY s.id", String.class);

List<String> names = query.getResultList();

System.out.println(names);
这里查询的是s.name, 即每个Sender对象的name属性, 是一个字符串, 所以TypedQuery的泛型是String, 查询的结果集自然也需要用对应的List<String>来接着这个结果. 这个查出来的List<String>并不会被Hibernate托管, 即如果查出来之后修改names中的任意元素, 不会影响数据库. 如果查出来的是Sender的集合, 就会被托管. 很多时候知道结果必定是单一的, 比如根据唯一标识符查询对象, 或者查询一个明知道是标量的结果, 这个时候就可以改用getSingleResult()方法, 如果结果不唯一, 则会报javax.persistence.NonUniqueResultException异常; 如果没有结果, 则会报javax.persistence.NoResultException. 所以使用getSingleResult()一定要注意处理异常, 而不像getResultList()即使没有结果, 也会返回一个空的List. 所以视情况灵活而定, 看一个查询标量的例子, 使用了聚合函数:
TypedQuery<Long> query = em.createQuery("SELECT count(s) FROM Sender s", Long.class);

Long numbers = query.getSingleResult();
上边是JPA的内容, 下边都是Hibernate特有的内容了. 先看Hibernate特有的游标查询, 除了上边的游标查询实例, 还可以具体操纵游标来移动位置:
org.hibernate.query.Query<Sender> query = session.createQuery("SELECT s FROM Sender s order by s.id", Sender.class);

org.hibernate.ScrollableResults resultsWithCursor = query.scroll(ScrollMode.SCROLL_INSENSITIVE);

//移动到第六行
resultsWithCursor.setRowNumber(5);

//取出第六行的数据, 这里需要强制转换类型, 这是固定写法, 只能get(0), 不能get(1)
Sender sender = (Sender) resultsWithCursor.get(0);

resultsWithCursor.close();
这里可以来看一下游标的三种模式了:
  1. ScrollMode.SCROLL_INSENSITIVE, 游标对于数据库的变化不敏感, 即查出来的结果集不会再变动(没关闭游标的情况下), 这样就没有脏读也没有不可重复读, 幻读的问题.
  2. ScrollMode.SCROLL_SENSITIVE, 没关闭游标的情况下, 如果有新提交数据, 结果集也会有变化. 不过由于Hibernate的一级缓存本身就提供了不可重复读的隔离, 即不会加载新数据, 所以这个情况仅仅会影响查询结果是标量集的情况.
  3. ScrollMode.FORWARD_ONLY, 不能像上边的例子一样自由的在不同的行号间跳跃.
不过游标模式不是对所有数据库都可用, 知道如何操作游标基本也够用了, 尤其是取出一片数据进行处理的时候, 还是比较方便的. 来看一下Hibernate特有的在结果集中迭代.
org.hibernate.query.Query<Sender> query = session.createQuery("SELECT s FROM Sender s", Sender.class);

Iterator<Sender> senderIterator = query.iterate();

while (senderIterator.hasNext()) {
    System.out.print(senderIterator.next());
}

//必须要手工关闭
Hibernate.close(senderIterator);
在执行query.iterate()的时候, Hibernate会读取一次所有Sender对象的唯一标识符. 之后在每次迭代的时候, 才会去读取具体的每一个Sender对象. 在执行完之后, 必须使用Hibernate.close()方法来关闭迭代器, 这个一定要注意. 查询的基本套路就是上边说的这样, JPA的标准查询和处理已经能够满足基本上99%的情况, 而且提供了JPQL和类型安全等各种方法. 对于比较简单的查询, 可以编写JPQL语句进行查询, 对于复杂的查询, 建议使用CriteriaQuery, 使用编程的方式来进行操作. 分页则采用编程的方式, 很好处理.

命名查询

其实还有外部查询, 外部查询有三种, 一种是通过注解定义, 一种是将查询写在XML文件中, 一种是编程的方式. 这三种方式里, 编程的方式就是注解定义的后台操作, 一般写在XML文件里的不太用了, 来看看注解方式就行了, 编程方式知道有这个情况就行了. 首先需要知道的是, 给一个查询命名, 意味着Hibernate内部肯定维护了一个查询的命名空间, 给一个查询命名的时候, 就会将名称放在这个命名空间中. JPA提供了@NamedQueries@NamedQuery注解, 放在一个Entity类前边, 定义一系列的命名查询. 看如下例子:
@NamedQueries(
        {

                @NamedQuery(
                        name = "query1",
                        query = "SELECT s FROM Sender s order by s.id"
                ),
                @NamedQuery(
                        name = "query2",
                        query = "SELECT s FROM Sender s order by s.id desc"
                )

        }
)

@Entity
public class Sender {

}
注解中的两个名称query1和query2, 在Hibernate读取注解的时候, 就会添加到查询命名空间中, 之后使用em.createNamedQuery()就可以创建这些预先命名的查询:
TypedQuery<Sender> firstQuery = em.createNamedQuery("query1", Sender.class);
TypedQuery<Sender> secondQuery = em.createNamedQuery("query2", Sender.class);
方法的第一个参数是命名查询的名称, 也就是写在注解中的查询名称, 第二个依然是类型, 这样就可以创建一个TypedQuery. 如果一个Entity在写的时候, 就会知道常用的查询, 那么就可以使用注解方式来提前写好外部查询, 这样使用起来就非常方便.

查询提示 - hints

查询提示应该算是准备查询的过程, 方法和其他准备查询一样, 都是调用query的某些方法. 查询提示调用的是query.setHint(). query.setHint()的第一个参数是提示的名称, 类型是字符串, 第二个参数是提示的值. 有如下提示可以用:
Hints
类型 提示名 可选值
JPA标准 javax.persistence.query.timeout int类型, 以毫秒表示的过期时间, 和JDBC的setQueryTimeout()是相同的作用.
javax.persistence.cache.retrieveMode USE | BYPASS, 字符串类型. USE表示使用二级缓存, BYPASS表示不使用二级缓存
javax.persistence.cache.storeMode USE | BYPASS | REFRESH, 字符串类型. 是否将查询结果存放在二级缓存中. USE表示保存, BYPASS表示不保存, REFRESH表示每次都更新二级缓存.
Hibernate原生 org.hibernate.flushMode org.hibernate.FlushMod中的几个枚举类型. 控制持久化上下文在执行查询的何时进行更新. 看下边详细解释.
org.hibernate.readOnly true | false, 布尔值, 表示是否执行脏检查, 设置成true就表示只读, 不会再进行脏检查.
org.hibernate.fetchSize int类型, 设置一个查询获取的结果数量, 默认是10, 相当于在查询之前先执行PreparedStatement.setFetchSize()
org.hibernate.comment 字符串, 设置一个SQL注释, 一般用于日志.
这些提示中唯一一个需要详细解释的就是org.hibernate.flushMode, 有三种:
  1. AUTO, 在每次执行查询前, 都会刷新持久化上下文. 比如查出一个结果集, 修改结果集, 再进行查询. 第二次查询之前, 会先刷新持久化上下文, 也就是写入. 默认就是AUTO.
  2. COMMIT, 将某一个查询设置为COMMIT的时候, 在执行这个查询之前, 就不会刷新上下文.
  3. ALWAYS, 这个一般不用, 即每次都会更新.
对于AUTO和COMMIT, JPA也有对应的标准, 就是setFlushMode(), 可以用于EntityManager和Query对象, 参数是JPA标准的AUTO和COMMIT.
@Test
public void testNamedQuery() {
    EntityManager em = getEM();
    em.getTransaction().begin();

    //设置query2查询的FlushMode为COMMIT
    TypedQuery<Sender> query = em.createNamedQuery("query2", Sender.class);

    query.setHint("org.hibernate.flushMode", FlushMode.COMMIT);


    System.out.println("开始第一次查询");
    List<Sender> senders = em.createQuery("SELECT s FROM Sender s ORDER BY s.id", Sender.class).getResultList();

    System.out.println("修改一个结果");
    senders.get(0).setName(String.valueOf(System.currentTimeMillis()));

    System.out.println("第二次查询");
    System.out.println(query.getResultList());


    em.getTransaction().commit();
    em.close();
}
这段代码设置成两种方式, 运行一下就可以看出来UPDATE语句的次序有变化.
LICENSED UNDER CC BY-NC-SA 4.0
Comment