映射看完了, 抓取策略也看完了, 剩下就是最核心的查询了. 查询永远是数据库操作的核心, 相比其他的UPDATE DELETE优先度高很多.
今天已经二月了, 开始继续看吧. 今年的任务就是再搞一遍Spring框架之后, 还是回头老老实实学数据结构和算法, 然后来刷点题目, 提升基本功了. 女儿也快到了可以学编程的年纪了, 当爹的我可不能落下了.
- 查询基础理论
- 创建查询 - Query 和 TypedQuery
- 创建查询 - CriteriaQuery
- 创建查询 - Hibernate的查询接口
- 准备查询
- 执行查询
- 命名查询
- 查询提示 - hints
查询基础理论
在JPA中使用查询有三个步骤:
- 创建查询
- 准备该查询, 包括传递参数, 设置提示和分页选项等
- 运行查询获取结果
针对这三个步骤, 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的查询接口是:
org.hibernate.Query
, 已过时, 被org.hibernate.query.Query
替代
org.hibernate.SQLQuery
, 已过时, 被org.hibernate.query.NativeQuery
替代
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()方法也已经过时, 这里就不再看了.
准备查询 - 绑定命名参数 和 绑定顺序参数
准备查询主要有如下工作:
- 绑定命名参数
- 绑定顺序参数
- 分页
- 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();
这里可以来看一下游标的三种模式了:
ScrollMode.SCROLL_INSENSITIVE
, 游标对于数据库的变化不敏感, 即查出来的结果集不会再变动(没关闭游标的情况下), 这样就没有脏读也没有不可重复读, 幻读的问题.
ScrollMode.SCROLL_SENSITIVE
, 没关闭游标的情况下, 如果有新提交数据, 结果集也会有变化. 不过由于Hibernate的一级缓存本身就提供了不可重复读的隔离, 即不会加载新数据, 所以这个情况仅仅会影响查询结果是标量集的情况.
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
, 有三种:
AUTO
, 在每次执行查询前, 都会刷新持久化上下文. 比如查出一个结果集, 修改结果集, 再进行查询. 第二次查询之前, 会先刷新持久化上下文, 也就是写入. 默认就是AUTO.
COMMIT
, 将某一个查询设置为COMMIT的时候, 在执行这个查询之前, 就不会刷新上下文.
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语句的次序有变化.