Hibernate6-入门手册-四-

189 阅读53分钟

Hibernate6 入门手册(四)

原文:Beginning Hibernate 6

协议:CC BY-NC-SA 4.0

七、JPA 集成和生命周期事件

Hibernate 提供了许多简单的“原生 Hibernate API”之外的功能在这一章中,我们将讨论如何使用标准的 JPA 配置资源、Hibernate 的对象验证工具、对象生命周期事件以及一些其他技巧。

Java 持久化 API

Java Persistence API,简称 JPA,是由 Java 社区过程批准的标准,来自许多项目和供应商的代表的输入——并且受到 Hibernate 的很大影响。它是作为新的企业 Java 规范的一部分创建的,主要是因为实体 Beans 企业持久化的先前标准——很难编写和使用,更难很好地使用。 1

Hibernate 参与了创建 JPA 的社区团队,公平地说,JPA 规范与 Hibernate 的 API 非常相似;Hibernate 本身集成了许多 JPA 实践,正如前面关于映射的章节所示。(大多数映射特性和注释都是 JPA 规范的一部分;现在,这些注释的原生 Hibernate 版本已经可用,但很少在实践中使用。 2

Hibernate 提供了 JPA 规范的实现。因此,您可以直接使用 JPA,带有 JPA 特定的配置文件,并获取一个EntityManager而不是一个Session

您可能想这样做有几个原因。首先,JPA 是一个标准,这意味着符合标准的代码通常是可移植的,允许不同实现之间的差异。例如,您可以将 Hibernate 用于开发,而对于生产,您可以部署到提供 EclipseLink 的应用服务器中。(反之亦然:您可以使用 EclipseLink 进行开发,并部署到使用 Hibernate 的架构中。)

另一个原因是 Java EE 规范本身。Java EE 容器需要提供 JPA,这意味着容器可以管理和配置资源;利用非 JPA 配置会给应用开发人员带来更大的负担。然而,值得指出的是,即使在默认为不同 JPA 实现的容器中,也可以使用 Hibernate 作为 JPA 实现,这为您提供了两全其美的好处:JPA 配置标准(在某些方面有其自身的好处)和 Hibernate 的出色性能和扩展特性集。 3

因此,让我们看看我们需要做些什么来支持 JPA 配置文件,而不是 Hibernate 配置过程。我们将通过一系列简单的步骤为我们提供一个工作工具包。它们是:

  1. 将 Hibernate 的 JPA 支持添加到util项目中,作为一个非转换依赖项。4

  2. 添加一个JPASessionUtil类,作为SessionUtil实用程序的近似模拟。就像SessionUtil提供了一个Session实例一样,JPASessionUtil将提供一个EntityManager实例,我们还将添加一个机制,通过这个机制它将提供一个 HibernateSession;这样,我们可以通过 Hibernate API 使用 JPA 配置。

  3. 编写 JPA 配置和测试来展示功能操作;这将让我们了解 JPA 和 Hibernate 之间的一些差异。

项目对象模型

再来看看util项目的pom.xml。我们在第三章中提到了它,但是跳过了它的细节,因为这本书的源代码有完整的形式(这里有一个秘密——我们知道我们会在这一章回顾它)。)

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>hibernate-6-parent</artifactId>
    <groupId>com.autumncode.books.hibernate</groupId>
    <version>5.0</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>
  <artifactId>util</artifactId>

  <dependencies>
    <dependency>
      <groupId>org.hibernate.orm</groupId>
      <artifactId>hibernate-hikaricp</artifactId>
      <version>${hibernate.core.version}</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Listing 7-1util/pom.xml

这大部分都很普通,但是请注意包含了lombok和 at test范围。这种依赖用于自动生成 Java 类的样板文件——我们将在本章的下一节深入讨论它。我们将它定义为test,因为我们希望能够编写使用它的代码(并测试它),但是我们不希望而不是强制任何使用它的项目将lombok作为显式依赖;我们基本上将其标记为非转换依赖。

毕竟,可传递的依赖关系是从一个项目传递到另一个项目的;如果我们说项目 A 依赖于依赖项 B,那么任何使用 A 的项目都必须依赖于 B。需要显式地包含非传递依赖项,因此如果 A 对工件 D 有非传递依赖项——比如lombok,如此处所示——使用 A 的项目必须显式地声明对 D 的依赖项

现在让我们先简单地讨论一下样板文件,因为现在是时候用 Lombok 去掉很多样板文件了。

介绍龙目岛

“样板文件”,在我们的上下文中,是一些被一遍又一遍重复使用而没有重大改变的东西。我们总是在简单的访问器和变异器中看到它;当我们有一个String name的时候,我们期望有两个方法伴随着它:

String getName() { return name; }
void setName(String name) { this.name=name; }

我们在equals()hashCode()身上看到了同样的事情,就此而言,toString()也是如此。当然,这些方法在不同的领域和不同的类中是不同的,但是它们本质上都是相同的,只是在细节上有所不同——通常 IDE 可以为我们生成它们。

这样做的问题是,它为这些方法创造了一种“常态”。只要他们总是做完全相同的事情,那可能没问题;在本书中,我们倾向于不在源代码中打印这些方法,因为它们非常重复,并且没有提供读者会发现有用的信息。

危险在于当实现与标准不同时;例如,如果我们的访问器(getName())要返回一个name字段的规范化版本,我们作为读者和程序员已经习惯了样板,一个与其他样板方法做的事情不完全相同的方法根本不会引人注目。

因此,尽管样板代码并不坏,但如果我们不必包含它,那会更好。

许多语言,包括更高版本的 Java(即 15 或更高版本),都提供了为类提供这种样板文件的语法。Java 14 引入了record,它是一个不可变的类,为您提供了访问器和其他标准方法。但是,记录不适合用 Hibernate 或 JPA 持久化,因为需要代理一个类来更新数据;记录不能被框架改变(因为它们是不可变的),因此不能被延迟初始化,这是一个对性能非常重要的特性。

Java 中的记录最适合数据传输,而不是持久化。

然而,Lombok ( https://projectlombok.org )允许我们简单而干净地注释一个对象,这样我们就可以显示与该实体相关的所有代码,甚至可以从源代码中删除所有的样板代码。

Lombok 提供了许多注释,可以生成我们刚刚提到的所有样板方法,以及更多:toString()equals()hashCode()、赋值函数、访问函数和无参数构造函数,等等。Lombok 是编译时依赖项;我们不需要库存在于任何依赖于生成的类的东西中。

我们已经将它包含在我们的pom.xml中,但是这里是具体的依赖关系;它作为一个非传递依赖项包含在这里。

我们希望 Lombok 是一个非传递依赖。它是一个注释处理器,只在编译时运行;在它生成的内容中没有下游依赖项,因此从部署的角度来看,可传递依赖项没有任何意义。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
    <scope>test</scope>
</dependency>

那么龙目岛为我们做了什么?它实际上根据我们想要它做的事情生成样板代码。例如,我们可以用@Getter来注释一个字段,它将基于该字段的名称生成一个适当的 JavaBean 兼容的访问器方法。甚至还有一种包罗万象的注释,@Data,它将为所有字段生成访问器和变异器,以及对equals()hashCode()toString()的良好实现,这样我们就可以拥有一个完整的和完整的实体,其清单如清单 7-2 所示。

package com.autumncode.util.model;

import lombok.Data;

import javax.persistence.*;

@Entity(name = "Thing")
@Data
public class Thing {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @Column
  String name;
}

Listing 7-2util/src/test/java/com/autumncode/util/model/Thing.java

在通过 Lombok 注释处理器进行编译和处理之后,这个类将拥有setId(Integer)getId()setName(String)getName()、一个与idname相比较的equals()实现,以及一个兼容的hashCode()实现,还有一个包含所有属性的toString()。该源代码就是我们所需要的全部——不再像我们在前面的清单中看到的那样“为了简洁而删除代码”。

我们在 Lombok 中也有选项来指定不同种类的构造函数;我们的Thing暂时保留了默认的构造函数。

在我们探索本章的其余部分时,我们将使用这个类。

JPASessionUtil 类

JPA 使用“持久化单元”的概念,这些单元被命名为配置。在给定的部署中,每个持久化配置都有一个唯一的名称。因为持久化单元是命名的,所以我们需要考虑我们的实用程序类有多个持久化单元的可能性,如清单 7-3 所示。

package com.autumncode.jpa.util;

import org.hibernate.Session;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.HashMap;
import java.util.Map;

public class JPASessionUtil {
  private static Map<String, EntityManagerFactory>
      persistenceUnits = new HashMap<>();

  @SuppressWarnings("WeakerAccess")
  public static synchronized EntityManager
  getEntityManager(String persistenceUnitName) {
    persistenceUnits
        .putIfAbsent(
            persistenceUnitName,
            Persistence
                .createEntityManagerFactory(
                    persistenceUnitName
                ));
    return persistenceUnits
        .get(persistenceUnitName)
        .createEntityManager();
  }

  public static Session getSession(String persistenceUnitName) {
    return getEntityManager(persistenceUnitName)
        .unwrap(Session.class);
  }
}

Listing 7-3util/src/main/java/com/autumncode/jpa/util/JPASessionUtil.java

原谅格式;这些都是很长的方法调用,而且它们都是链式的,所以尽管在进行链式调用时没有额外的复杂性,但从概念上讲,它们最终看起来比实际长得多。

在这个类中,我们建立了一种重用EntityManagerFactory实例的方法,通过名字来查找。如果给定的名称没有EntityManagerFactory,我们将创建并保存它。如果给定名称不存在持久化单元,将抛出一个javax.persistence.PersistenceException

如果您使用的是一个为您管理持久化单元的框架,比如 Jakarta EE 或 Spring,那么这段代码就没有用了。事实上,几乎任何提供 JPA 集成的框架都会使这个类变得完全没有必要,这也是它不是很长的部分原因。我们使用它主要是为了让后续章节中编写示例代码更加方便。

getSession()方法提供了对EntityManager底层实现的访问。对于 Hibernate,这将是org.hibernate.Session;如果实际的实现不是 Hibernate,那么就会抛出一个运行时异常。

所有这些都是有用的,但是让我们开始使用它。让我们写一些测试来展示如何使用这个类。

测试 JPASessionUtil

我们的第一个测试只是试图获取资源:一组正确配置的资源和另一组没有正确配置的资源。这将允许我们验证该实用程序是否返回了它所期望的结果,即使是在配置不当的情况下。清单 7-4 显示了我们第一套测试的代码;接下来,我们将使用这些测试将使用的 JPA 配置。

package com.autumncode.jpa.util;

import com.autumncode.util.model.Thing;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

import static org.testng.Assert.*;

public class JPASessionUtilTest {
  @Test
  public void getEntityManager() {
    EntityManager em = JPASessionUtil
        .getEntityManager("utiljpa");
    em.close();
  }

  @Test(
      expectedExceptions = {javax.persistence.PersistenceException.class}
  )
  public void nonexistentEntityManagerName() {
    JPASessionUtil.getEntityManager("nonexistent");
    fail("We shouldn't be able to acquire an EntityManager here");
  }

  @Test
  public void getSession() {

    Session session = JPASessionUtil.getSession("utiljpa");
    session.close();
  }

  @Test(
      expectedExceptions = {javax.persistence.PersistenceException.class}
  )
  public void nonexistentSessionName() {
    JPASessionUtil.getSession("nonexistent");
    fail("We shouldn't be able to acquire a Session here");
  }

  @Test
  public void testEntityManager() {
    EntityManager em = JPASessionUtil.getEntityManager("utiljpa");
    em.getTransaction().begin();
    Thing t = new Thing();
    t.setName("Thing 1");
    em.persist(t);
    em.getTransaction().commit();
    em.close();

    em = JPASessionUtil.getEntityManager("utiljpa");
    em.getTransaction().begin();
    TypedQuery<Thing> q = em.createQuery(
        "from Thing t where t.name=:name",
        Thing.class);
    q.setParameter("name", "Thing 1");
    Thing result = q.getSingleResult();
    assertNotNull(result);
    assertEquals(result, t);
    em.remove(result);
    em.getTransaction().commit();
    em.close();
  }

  @Test
  public void testSession() {

    Thing t = null;
    try (Session session = JPASessionUtil.getSession("utiljpa")) {
      Transaction tx = session.beginTransaction();
      t = new Thing();
      t.setName("Thing 2");
      session.persist(t);
      tx.commit();
    }

    try (Session session = JPASessionUtil.getSession("utiljpa")) {
      Transaction tx = session.beginTransaction();
      Query<Thing> q =
          session.createQuery(
              "from Thing t where t.name=:name",
              Thing.class);
      q.setParameter("name", "Thing 2");
      Thing result = q.uniqueResult();
      assertNotNull(result);
      assertEquals(result, t);
      session.delete(result);
      tx.commit();
    }
  }
}

Listing 7-4util/src/test/java/com/autumncode/jpa/util/JPASessionUtilTest.java

您会注意到不存在的测试做了一些奇怪的事情:它们声明预期的Exception类型。通常,异常意味着测试失败;在这种情况下,我们说如果抛出一个匹配的异常,测试没有失败。

然而,“没有失败”并不等同于“通过”对于这些测试,我们实际上希望失败,除非遇到异常;因此,我们试图获取资源并调用fail()——在fail()执行之前,一个异常将退出该方法,这意味着测试通过。

然而,除非我们包含一个 JPA 配置文件,否则这些测试都不会通过,这个文件需要在类路径的/META-INF/persistence.xml处,如清单 7-5 所示。

<persistence
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="
             http://java.sun.com/xml/ns/persistence
              http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">
  <persistence-unit name="utiljpa">
    <properties>
      <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:h2:./utiljpa"/>
      <property name="javax.persistence.jdbc.user" value="sa"/>
      <property name="javax.persistence.jdbc.password" value=""/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
      <property name="hibernate.show_sql" value="true"/>
    </properties>
  </persistence-unit>
</persistence>

Listing 7-5util/src/test/resources/META-INF/persistence.xml

创建了这个文件后,我们就有了一个有效的持久化单元,名为utiljpa;我们现在可以运行四个测试了。通过它们的传递,您可以看到(并证明)当被请求时,JPASessionUtil返回一个EntityManagerSession的实例,并在发出无效请求时抛出一个异常。

正如你在testEntityManager()方法中看到的,我们使用了一个Thing——我们的 Lombok 注释的实体类——就像它是一个常规的 POJO 一样,显式地调用setName()(并通过assertEquals()隐式地调用equals())。

您还会注意到testSession()方法。这和testEntityManager()是功能等同的测试。

在每一个中,我们有两个操作。在每一个中,我们获得一个提供持久化的类, 6 启动一个事务,持久化一个Thing实体,然后提交事务;然后我们重复这个过程,查询然后删除实体。这两种方法之间的唯一区别是使用的持久化 API 测试使用 JPA,testSession()使用 Hibernate API。

大多数区别都相当简单:例如,JPA 使用EntityManager.remove()而不是Session.delete()。查询类型是不同的(JPA 的类型化查询是一个javax.persistence.TypedQuery,而 Hibernate 的是一个org.hibernate.query.Query),尽管它们在功能上是相同的。可能最相关的变化是事务的使用,这完全是自愿的。例如,您可以在testSession()方法中使用清单 7-6 中所示的块,这使得它几乎与 JPA 版本完全相同。

try(Session session = JPASessionUtil.getSession("utiljpa")) {
  session.getTransaction().begin();
  Thing t = new Thing();
  t.setName("Thing 2");
  session.persist(t);
  session.getTransaction().commit();
}

Listing 7-6Mirroring the EntityManager API with Session

然而,重要的是要注意到SessionEntityManager是相似的,但不是相同的;虽然清单 7-6 如果你使用EntityManager而不是Session会工作,即使在测试代码的小块中Session使用org.hibernate.query.Query而不是javax.persistence.TypedQuery

那么应该用哪一个呢?嗯,这取决于你需要什么。如果您需要 JPA 兼容性,那么您必须限制自己使用EntityManager及其功能;否则,请使用您喜欢的方法。Hibernate API 提供了一些 JPA 无法提供的微调特性;如果你想使用它们,你会想使用Session,但是除此之外,这两个 API 在大多数意图和目的上是相同的。

生命周期事件

Java 持久化 API 向数据模型公开某些事件。这些事件允许开发人员实现架构本身不容易提供的附加功能。事件通过使用注释来指定,事件处理程序可以直接嵌入到实体中,也可以保存在单独的实体侦听器类中。

您可以以几种不同的方式使用生命周期:例如,您可以手动更新时间戳,或者您可以编写审计数据,初始化瞬态数据,或者在持久化数据之前验证数据。

存在与对象创建、读取、更新和删除相对应的生命周期事件。对于在持久化上下文中有意义的每种事件类型,在事件发生之前和之后都有回调挂钩。

事件处理程序是对应于七个生命周期阶段之一的简单方法。

表 7-1

实体生命周期阶段

|

生命周期注释

|

方法运行时

| | --- | --- | | @PrePersist | 在数据实际插入数据库表之前执行。当数据库中存在对象并且发生更新时,不使用它。 | | @PostPersist | 在数据写入数据库表后执行。 | | @PreUpdate | 更新托管对象时执行。当对象第一次保存到数据库中时,不使用此注释。 | | @PostUpdate | 在将托管对象的更新写入数据库后执行。 | | @PreRemove | 在从数据库中删除托管对象的数据之前执行。 | | @PostRemove | 从数据库中删除托管对象的数据后执行。 | | @PostLoad | 在从数据库加载托管对象的数据并初始化该对象后执行。 |

清单 7-7 提供了一个实体,描述性地命名为“LifecycleThing”,它为各种生命周期事件提供挂钩。与我们之前的类一样,它使用 Lombok 来隐藏样板文件,这样这就是实际的完整源代码清单。 7

package chapter07.lifecycle;

import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.*;
import java.util.BitSet;

@Entity
@Data
public class LifecycleThing {
  static Logger logger = LoggerFactory.getLogger(LifecycleThing.class);
  static BitSet lifecycleCalls = new BitSet();

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @Column
  String name;

  @PostLoad
  public void postLoad() {

    log("postLoad", 0);
  }

  @PrePersist
  public void prePersist() {
    log("prePersist", 1);
  }

  @PostPersist
  public void postPersist() {
    log("postPersist", 2);
  }

  @PreUpdate
  public void preUpdate() {
    log("preUpdate", 3);
  }

  @PostUpdate
  public void postUpdate() {
    log("postUpdate", 4);
  }

  @PreRemove
  public void preRemove() {
    log("preRemove", 5);
  }

  @PostRemove
  public void postRemove() {
    log("postRemove", 6);
  }

  private void log(String method, int index) {
    lifecycleCalls.set(index, true);
    logger.info("{}: {} {}", method,
        this.getClass().getSimpleName(), this);
  }
}

Listing 7-7src/main/java/chapter07/lifecycle/LifeCycleThing.java

这个类跟踪在一个BitSet中进行的生命周期调用。当生命周期事件发生时,它在BitSet中设置一个位;一个测试可以(并且将会)检查BitSet以确保没有缺口,这将会给我们一个更清晰的画面,我们是否已经成功地执行了每个回调。

当然,我们可以只用我们的眼睛来观察结果。这当然是可行的(可悲的是,这是大多数用户测试的基础),但是我们想要客观的、可重复的、更可验证的结果。

我们的生命周期测试如清单 7-8 所示。它需要做的只是创建、读取、更新和删除一个实体;这将触发我们的每个事件处理程序,我们可以看到序列(如果我们观察应用日志)并让测试验证没有跳过任何测试(因为它检查位集)。

package chapter07.lifecycle;

import com.autumncode.jpa.util.JPASessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.Reporter;
import org.testng.annotations.Test;

import static org.testng.Assert.*;

public class FirstLifecycleTest {
  @Test
  public void testLifecycle() {
    Integer id;
    LifecycleThing thing1, thing2, thing3;
    try (Session session = JPASessionUtil.getSession("chapter07")) {
      Transaction tx = session.beginTransaction();
      thing1 = new LifecycleThing();
      thing1.setName("Thing 1");

      session.save(thing1);
      id = thing1.getId();
      System.out.println(thing1);
      tx.commit();
    }

    try (Session session = JPASessionUtil.getSession("chapter07")) {
      Transaction tx = session.beginTransaction();
      thing2 = session

          .byId(LifecycleThing.class)
          .load(-1);
      assertNull(thing2);

      Reporter.log("attempted to load nonexistent reference");

      thing2 = session.byId(LifecycleThing.class)
          .getReference(id);
      assertNotNull(thing2);
      assertEquals(thing1, thing2);

      thing2.setName("Thing 2");

      tx.commit();
    }
    try (Session session = JPASessionUtil.getSession("chapter07")) {
      Transaction tx = session.beginTransaction();

      thing3 = session
          .byId(LifecycleThing.class)
          .getReference(id);
      assertNotNull(thing3);
      assertEquals(thing2, thing3);

      session.delete(thing3);

      tx.commit();
    }
    assertEquals(LifecycleThing.lifecycleCalls.nextClearBit(0), 7);
  }
}

Listing 7-8src/test/java/chapter07/lifecycle/FirstLifecycleTest.java

这个测试有三个部分,每个部分都使用自己的会话和事务。第一个创建一个LifecycleThing并持久化它。第二次尝试加载一个不存在的实体,然后加载一个现有的实体;然后,它更新现有的实体。第三部分加载相同的实体并删除它。这意味着我们表示了对象中的每个生命周期事件:创建、读取、更新和删除。

对于每个生命周期事件,都会生成一条日志消息。同时,修改内部BitSet来跟踪生命周期方法是否已经被调用;在测试结束时,检查BitSet以查看从 7 开始的每个位都已被设置。如果值是正确的,那么我们知道每个生命周期方法至少被调用过一次。

结果应该相当明显:在这种情况下,在持久化发生之前调用prePersist(),在持久化发生之后运行postPersist()。生命周期处理程序中的异常可能很棘手。如果在事件之前生命周期监听器中发生异常——也就是说,在@PrePersist@PreUpdate@PreRemove中——它将被传递给调用者进行处理。然而,该事务仍然有效。也就是说,如果@PostPersist@PostUpdate@PostRemove@PostLoad代码出现错误,您将使事务无效。

后加载操作中的异常处理起来会很有趣。(从对象的角度来看,这表明数据库中的数据是无效的;例如,考虑数据库中的字段是否具有来自枚举的值范围,并且这是在加载操作之后以编程方式检查的。)它可能必须在数据库本身中处理,建议您不惜一切代价避免这种可能性。

外部实体侦听器

LifecycleThing最大的弱点(除了它是一个唯一目的是说明持久化生命周期的类之外)是所有的事件监听器都嵌入在类本身中。相反,我们可以通过使用@EntityListeners注释,将一个外部类指定为具有相同注释的实体监听器。清单 7-9 显示了一个带有外部实体监听器的简单实体。

package chapter07.lifecycle;

import lombok.*;

import javax.persistence.*;

@Entity
@NoArgsConstructor
@Data
@EntityListeners({UserAccountListener.class})
public class UserAccount {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  String name;
  @Transient
  String password;
  Integer salt;
  Integer passwordHash;

  public boolean validPassword(String newPass) {
    return newPass.hashCode() * salt == getPasswordHash();
  }
}

Listing 7-9src/main/java/chapter07/lifecycle/UserAccount.java

清单 7-10 展示了一个简单的外部监听器可能是什么样子。

package chapter07.lifecycle;

import javax.persistence.PrePersist;

public class UserAccountListener {
  @PrePersist
  void setPasswordHash(Object o) {
    UserAccount ua = (UserAccount) o;
    if (ua.getSalt() == null || ua.getSalt() == 0) {
      ua.setSalt((int) (Math.random() * 65535));
    }
    ua.setPasswordHash(
        ua.getPassword().hashCode() * ua.getSalt()
    );
  }
}

Listing 7-10src/main/java/chapter07/lifecycle/UserAccountListener.java

UserAccount被持久化时,UserAccountListener将设置一个哈希密码,乘以一个随机盐;据推测,用户提供的密码可以通过应用相同的 salt 来测试。 8 (这是不安全的,无论如何。不要用这段代码作为安全性的例子。)

在这种情况下,侦听器只监控一种对象类型;它不进行错误检查。(如果传递给它的类型不正确,它将抛出一个错误。)

事件监听器可以方便地将您实际需要访问持久化生命周期的任何地方考虑在内,尤其是在考虑数据验证时。

数据有效性

从现在开始,我们将开始看到不是“当前版本”的库,这取决于它们是否已经更新到使用 Jakarta EE 打包。Hibernate 6 仍然在使用 JPA 的javax.persistence打包,而不是jakarta.persistence,并且像 Validator API 这样的东西的版本被选择为尽可能符合旧的javax前缀。Hibernate 一个jakarta.persistence迁移,但是它还不是“正常”的方法,在它之前,那些迁移应该被认为是“测试中”而不是“生产就绪”,即使它们可能很好。与此同时,同时使用jakartajavax会令人困惑,所以在迁移到jakarta完成之前,我们将继续使用javax前缀。

Hibernate 还提供了一个验证 API,目前是 Java 的 Bean 验证规范 3.0 版的参考实现。9Bean 验证规范允许您的数据模型强制执行自己的约束,而不是让编码人员在整个应用代码中添加自己的数据值检查。

基于模型的验证应该有明显的价值:这意味着无论在哪个阶段访问数据,您都能够信任模型的状态。

考虑在 web 服务中应用数据验证的情况;离开 web 服务访问数据可能不会应用验证,这意味着与从其他环境访问数据相比,您更信任通过 web 服务访问的数据。这是一件坏事。

注意,作为 JPA 规范本身的一部分,我们已经有了一些验证功能。例如,我们可以指定列的值是惟一的(通过@Id@Column(unique=true);我们还可以通过@Column(nullable=false)指定列不能为空。通过实体生命周期的魔力,我们还可以通过回调和外部监听器来实施数据验证, 10 值得注意的是,在某些情况下,这仍然是一种有价值的、可行的方法。

因此,让我们来看看我们可以做些什么来尝试 Hibernate 的一些更强大的验证功能。

第一步是将 Hibernate 验证器添加到我们的项目中。如果您在 Java SE 项目中使用 Validator(一个独立的应用,比如我们的测试),那么您需要添加四个依赖项;如果像 WildFly 一样将应用部署到 Java EE 应用服务器中,只需要添加验证器依赖项本身。

清单 7-11 显示了本章的完整pom.xml。请注意,它使用占位符来表示其依赖项的版本;这本书的源代码被组织成一个单独的项目,这些版本被指定为顶层项目中的属性。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-6-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>5.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter07</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate.validator.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator-cdi</artifactId>
            <version>${hibernate.validator.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>${javax.el-api.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>${javax.el-api.version}</version>
        </dependency>
    </dependencies>
</project>

Listing 7-11chapter07/pom.xml

现在让我们看一个使用验证来确保数据正确性的类和测试。首先是ValidatedPerson类,如清单 7-12 所示。

package chapter07.validated;

import lombok.*;

import javax.persistence.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

@Entity
@Data
@Builder
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@NoArgsConstructor
public class ValidatedPerson {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  Long id;
  @Column
  @NotNull
  @Size(min = 2, max = 60)
  String fname;
  @Column
  @NotNull
  @Size(min = 2, max = 60)
  String lname;
  @Column
  @Min(value = 13)
  Integer age;
}

Listing 7-12src/main/java/chapter07/validated/ValidatedPerson.java

我们实际上已经通过 Lombok 给这个实体添加了一些东西。我们首先要研究的是@AllArgsConstructor注释,它创建了一个包可见的构造函数,所有属性都作为参数;就好像我们创造了ValidatedPerson(Long id, String fname, String lname, Integer age)。我们将它设置为 package-visible,因为我们不希望任何其他类使用它,主要是因为我们使用了另一个 Lombok 注释,@Builder

@Builder注释创建了一个内部类,可以通过builder()方法访问。 11 这个内部类使用了一个流畅的 API 12 提供了一种便捷的构造类的方式;有了生成器,我们可以使用下面的代码来构造一个ValidatedPerson:

ValidatedPerson person=ValidatedPerson.builder()
    .age(15)
    .fname("Johnny")
    .lname("McYoungster")
    .build();

现在让我们看看我们正在使用的验证注释,以及为什么。值得注意的是,我们并没有使用 Validator 提供给我们的所有注释——目前有超过 25 个注释被记录在案,这还不包括定制验证器的可能性。这些只是一些常用的验证注释。

第一个突出的是@NotNull,用在fname属性上。这类似于我们之前提到的@Column(nullable=false)注释,但是应用于持久化生命周期的不同点;如果使用了@NotNull,列仍然会以同样的方式设置(不允许空值),但是验证发生在持久化之前。如果我们使用@Column(nullable=false),验证发生在数据库中,并给我们一个数据库约束违反,而不是验证失败——这是一个非常微小的语义差异,但仍然是一个差异。

使用@Column(length=60)可以部分模拟@Size,但是@Column没有办法强制最小大小约束,并且验证阶段发生在持久化阶段之前。

@Min(value=13)指定整数值有一个最小值,正如人们可能预料的那样;最大值有相应的@Max注释。

其中一件有趣的事情是,它们实际上可以改变数据库定义。例如, 13 @Min@Max在数据库支持的情况下添加表约束,@NotNull在代码和数据库级别强制执行约束。@Size如果给定了最大大小,将为数据库列指定一个最大大小;数据库通常不会强制要求最小大小。

让我们在测试中看看这是什么样的。我们要做的是将一系列对象写入 Hibernate Session中,其中大部分将以某种方式通过验证。实际的持久化机制听起来像是我们可以为其编写一个方法的东西,所以不再多说, 14 让我们看看清单 7-13 中的整套测试,这样我们就可以看到验证是如何应用的。

package chapter07.validator;

import chapter07.unvalidated.UnvalidatedSimplePerson;
import chapter07.validated.ValidatedPerson;
import com.autumncode.hibernate.util.SessionUtil;

import javax.validation.ConstraintViolationException;

import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.Test;

import static org.testng.Assert.fail;

public class ValidatorTest {
  private ValidatedPerson persist(ValidatedPerson person) {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(person);
      tx.commit();
    }

    return person;
  }

  @Test
  public void createUnvalidatedUnderagePerson() {
    Long id = null;
    try (Session session = SessionUtil.getSession()) {
      Transaction transaction = session.beginTransaction();

      UnvalidatedSimplePerson person = new UnvalidatedSimplePerson();
      person.setAge(12); // underage for system
      person.setFname("Johnny");
      person.setLname("McYoungster");

      // this succeeds because the UnvalidatedSimplePerson
      // has no validation in place.
      session.persist(person);
      id = person.getId();
      transaction.commit();
    }
  }

  @Test
  public void createValidPerson() {
    persist(ValidatedPerson.builder()
        .age(15)
        .fname("Johnny")
        .lname("McYoungster").build());
  }

  @Test(expectedExceptions = ConstraintViolationException.class)
  public void createValidatedUnderagePerson() {

    persist(ValidatedPerson.builder()
        .age(12)
        .fname("Johnny")
        .lname("McYoungster").build());
    fail("Should have failed validation");
  }

  @Test(expectedExceptions = ConstraintViolationException.class)
  public void createValidatedPoorFNamePerson2() {
    persist(ValidatedPerson.builder()
        .age(14)
        .fname("J")
        .lname("McYoungster2").build());
    fail("Should have failed validation");
  }

  @Test(expectedExceptions = ConstraintViolationException.class)
  public void createValidatedNoFNamePerson() {
    persist(ValidatedPerson.builder()
        .age(14)
        .lname("McYoungster2").build());
    fail("Should have failed validation");
  }

}

Listing 7-13src/test/java/chapter07/validator/ValidatorTest.java

清单中的第一个方法——persist()—练习持久化循环,以节省代码。我们的测试方法将创建一个对象,并将其传递给 this 来执行验证生命周期。

我们的其他四个方法创建匹配各种单一标准的实体:一个有效的实体,一个fname太短的实体,一个lname太短的实体,一个没有fname的实体,以及一个underage的实体。在我们预期验证失败的测试中,我们将方法标记为接受异常——如果persist()方法成功执行,则方法失败。这对我们有用,因为我们预计persist()方法在这些情况下会失败。

值得注意的一件事是所有这些代码是如何重复的:我们有很多改变字段值的测试,因此我们的测试看起来都是一样的。我们可以做得比这更好——通常使用的测试框架实际上提供了这一点。我们可以参数化我们的测试,在这里我们声明一个生成输入的数据提供者方法,测试框架将调用我们所有数据集的参数化测试方法,将它们视为单独的测试。

清单 7-14 显示了ValidatorTest的参数化版本。

package chapter07.validator;

import chapter07.unvalidated.UnvalidatedSimplePerson;
import chapter07.validated.ValidatedPerson;
import com.autumncode.hibernate.util.SessionUtil;
import lombok.val;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.IExpectedExceptionsHolder;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import javax.validation.ConstraintViolationException;

import static org.testng.Assert.fail;

public class ParameterizedTest {
  private ValidatedPerson persist(ValidatedPerson person) {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(person);
      tx.commit();
    }
    return person;
  }

  @DataProvider
  Object[][] provider() {
    return new Object[][]{
        {"Johnny", "McYoungster", 15, false},
        {"Johnny", "McYoungster", 12, true},
        {"J", "McYoungster", 14, true},
        {"Johnny", "M", 14, true},
        {"Johnny", null, 14, true},
    };
  }

  @Test(dataProvider = "provider")
  void testValidations(String fname, String lname, Integer age, boolean expectException) {
    try {
      val builder=ValidatedPerson

          .builder()
          .age(age)
          .fname(fname);
      if(lname!=null) {
        builder.lname(lname);
      }
      persist(builder.build());
      if (expectException) {
        fail("should have caught an exception");
      }
    } catch (Exception ex) {
      if (!expectException) {
        fail("expected an exception");
      }
    }
  }
}

Listing 7-14src/test/java/chapter07/validator/ParameterizedTest.java

我们在这里所做的是声明一个方法,该方法返回一个对象数组的数组——provider()——并给它四个值:名、姓、年龄和一个布尔值,该值指示数据集是否“有效”。这些值将在我们的测试方法testValidations()中进行位置设置。

测试方法本身比ValidatorTest中的版本稍微复杂一些,因为我们希望能够测试缺失的值。我们实际上并没有像这里的一样完整——我们只有一个缺失的lname的检查——但是这个概念适用于每个参数。我们还使用了 Java 11 的val关键字,因为我们可以让 Java 推断出builder的类型——它实际上是ValidatedPerson.ValidatedPersonBuilder

try/catch结构的每个分支中,我们使用expectException值来确定结果是否是我们想要的;如果我们到达了try的末尾,没有出现我们期望的异常,那么我们就没有通过测试,并且我们反转了catch子句的机制。如果您想要验证这是否如预期的那样工作,更改provider()方法中的值,这也集中了所有测试数据的去向。 15

不过,您可能会注意到,我们的验证只包含单个属性。我们可以使用实体生命周期来创建我们自己的定制验证,但是 Validator 允许我们创建我们自己的验证注释——包括单字段验证(正如我们已经看到的)和类级验证。

让我们创建一个坐标实体——为了举例,让我们使用一个验证来确保一个有效的坐标不允许出现在笛卡尔象限系统的象限 III 中。(象限 III 中的坐标具有负的 x 和 y 属性。)单字段验证在这里不起作用,因为–5 作为 x 坐标是有效的,只要 y 坐标也不是负的。

我们实际上有很多选项可以选择来构建验证。最灵活的选项是一个查找依赖字段的注释——因此,对 X 的验证将包含对 Y 的引用以及附带的可接受标准,反之亦然。也就是说,让我们选择一个更简单的选项,一个特定于我们的坐标类的选项。 16

首先,我们来看看Coordinate类。然后我们将创建我们期望通过的测试;最后,我们将看看应用验证的注释。与SimpleValidatedPerson实体非常相似,我们将大量使用 Lombok 来消除样板代码。

package chapter07.validated;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@NoQuadrantIII
public class Coordinate {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @NotNull
  Integer x;
  @NotNull
  Integer y;
}

Listing 7-15src/main/java/chapter07/validated/Coordinate.java

如果没有完全定义我们的注释(@NoQuadrantIII注释),这个类将无法编译;很快就会出现。

让我们看一下我们的测试代码,它创建了九个坐标并全部持久化;代表原点以及象限 I、II 和 IV 的Coordinate对象都应该成功保存, 17 和象限 III 的坐标应该失败。我们将再次使用数据提供者机制来消除大量的重复代码,但不是针对失败情况,这在范围上是有限的。这一次,我们将明确测试故障条件。

package chapter07.validator;

import chapter07.validated.Coordinate;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import javax.validation.ConstraintViolationException;

public class CoordinateTest {
  private void persist(Coordinate entity) {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.persist(entity);
      tx.commit();
    }
  }

  @DataProvider(name = "validCoordinates")
  private Object[][] validCoordinates() {
    return new Object[][]{
        {1, 1},
        {-1, 1},
        {1, -1},
        {1, 0},
        {-1, 0},
        {0, -1},
        {0, 1},
        {0, 0},
      // trailing comma is valid: see JLS 10.6 https://bit.ly/3C3QN0J
    };
  }

  @Test(dataProvider = "validCoordinates")
  public void testValidCoordinate(Integer x, Integer y) {
    Coordinate c = Coordinate.builder().x(x).y(y).build();
    persist(c);
    // has passed validation, if we reach this point.
  }

  @Test(expectedExceptions = ConstraintViolationException.class)
  public void testInvalidCoordinate() {
    testValidCoordinate(-1, -1);
  }
}

Listing 7-16src/test/java/chapter07/validator/CoordinateTest.java

创建验证约束涉及两个类:一个是注释本身,另一个是注释的实现。

package chapter07.validated;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class QuadrantIIIValidator
    implements ConstraintValidator<NoQuadrantIII, Coordinate> {
  @Override
  public void initialize(NoQuadrantIII constraintAnnotation) {
  }

  @Override
  public boolean isValid(
      Coordinate value,
      ConstraintValidatorContext context
  ) {
    return !(value.getX() < 0 && value.getY() < 0);
  }
}

Listing 7-18src/main/java/chapter07/validated/QuadrantIIIValidator.java

package chapter07.validated;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {QuadrantIIIValidator.class})
@Documented
public @interface NoQuadrantIII {
  String message() default "Failed quadrant III test";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
}

Listing 7-17src/main/java/chapter07/validated/NoQuadrantIII.java

在这种情况下,isValid()方法——它得到一个ConstraintValidatorContext和一个Coordinate进行验证——我们可以简单地使用Coordinate并检查它的属性,看看它是否通过验证。可能存在更复杂的情况;例如,注释可以包括要使用的值的范围。

摘要

本章介绍了标准 Java 持久化 API 配置文件的使用,以及如何访问持久化生命周期和持久化之前的验证。它还讨论了使用 Lombok 来帮助避免样板代码,并展示了如何在 TestNG 中使用数据提供者来消除额外的测试代码。

在下一章中,我们将看看客户端应用如何通过使用Session对象与实体的数据库表示进行通信。

Footnotes 1

2004 年服务器端研讨会上对开发人员的一项非正式调查表明,近 95%的实体 beans 被低效或不当地使用。虽然是非正式的,因此是轶事,但这仍然是一个了不起的结果。

  2

像“X 在实践中很少使用”这样的说法几乎都是轶事。这一个肯定是;您可能会发现一些项目狂热地依赖于特定于 Hibernate 的注释。这个轶事仍然存在。

  3

在 Hibernate 不是默认提供者的环境中使用 Hibernate 的机制相当简单:在 persistence.xml 中添加<provider>org.hibernate.ejb.HibernatePersistence</provider>。然而,您仍然需要了解如何将 Hibernate 安装到您的应用服务器中。

  4

我们想要一个非传递依赖,因为我们不想强迫所有使用 util 项目的模块都包含 JPA 支持。

  5

包括柏拉图式的“存在”我们每个人都可以自己决定这是否是对共和国的赞美。

  6

您的作者将提供持久化服务的类称为“持久化演员”,但这听起来实在太乏味了。

  7

这也有希望是最后一次指出 Lombok 的用法。

  8

有关加密 salt 的更多信息,请参见 https://en.wikipedia.org/wiki/Salt_(cryptography)

  9

https://jakarta.ee/specifications/bean-validation/3.0/

  10

回调是通过生命周期方法应用的验证;例如,您可以在用@PrePersist 注释的方法中测试一个值。外部实体侦听器会做同样的事情。

  11

和大多数事情一样,这是有限制的。Lombok 无法生成知道类层次结构的生成器;这是由 Lombok 的工作方式造成的,很难绕过。

  12

参见 http://en.wikipedia.org/wiki/Fluent_interface 了解更多关于什么是流畅的 API 以及它可能是什么样子的信息。

  13

验证器文档将对数据库的影响级别称为“Hibernate 元数据影响”,这样数据库不知道的验证就不会对元数据产生影响,但是像@NotNull这样的验证被描述为意味着“列不可为空”

  14

有没有人喜欢热闹?或者甚至知道,准确地说,很多麻烦会是什么样子?

  15

JUnit 5 和 TestNG 内置了数据提供者机制;JUnit 的早期版本也有这个特性,但是是通过附加库实现的。

  16

如果你对定制约束的更多细节感兴趣——如果 Validator 让你感兴趣,你可能会感兴趣——参见 https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/

  17

有趣的数学事实:我们也在测试位于象限之间的坐标。例如,点(1,0)位于象限 I 和 II 之间,但是我们已经决定我们的坐标可以在任何地方使用除了象限 III,所以没问题。

 

八、使用会话

您可能已经注意到,Session是访问 Hibernate 功能的中心点。我们现在来看看它体现了什么,以及它暗示了你应该如何使用它。

会话

从前面章节的例子中,你会注意到一小部分类主导了我们与 Hibernate 的交互。其中,Session,实际上是一个接口,是关键。

Session对象用于创建新的数据库实体,从数据库中读入对象,更新数据库中的对象,以及从数据库中删除对象。 1 它允许您管理数据库访问的事务边界,并(在必要时)获得一个传统的 JDBC 连接对象,以便您可以对数据库做一些 Hibernate 开发人员在他们现有的设计中没有考虑到的事情。

如果您熟悉 JDBC 方法,这有助于将Session对象想象成 JDBC 连接,将提供会话对象的SessionFactory想象成提供Connection对象的连接池。图 8-1 展示了这些角色的相似之处。

img/321250_5_En_8_Fig1_HTML.png

图 8-1

会话和 JDBC 连接之间的相似性

SessionFactory物为贵物;不必要的重复会很快导致问题,创建它们是一个相对耗时的过程。理想情况下,您的应用将要访问的每个数据库都应该有一个单独的SessionFactory

对象也是线程安全的,所以没有必要为每个线程获取一个。然而,您将创建大量的Session对象——至少每个使用 Hibernate 的线程一个。Hibernate 中的Sessions而不是线程安全的,所以线程间共享Session对象可能会导致数据丢失或死锁。事实上,即使在一个特定线程的生命周期中,您也会经常想要创建多个Session实例(参见“线程”一节中的并发问题)。

Hibernate 会话和 JDBC 连接之间的类比仅限于此。一个重要的区别是,如果 Hibernate Session对象抛出任何类型的异常,您必须丢弃它并获得一个新的Session。这可以防止会话缓存中的数据与数据库不一致。

我们已经在第四章中介绍了核心方法,所以我们不会讨论所有通过Session接口可用的方法。要全面了解可用的 API,您应该阅读 Hibernate 网站或 Hibernate 6 下载中的 API 文档。表 8-1 到 8-4 给出了您可用的各种方法的概述;尽管篇幅很长,但这并不是一个详尽的列表。

表 8-4

Session与 JDBC 连接相关的方法

|

方法

|

描述

| | --- | --- | | connection() | 检索对基础数据库连接的引用。 | | disconnect() | 断开基础数据库连接。 | | reconnect() | 重新连接基础数据库连接。 | | isConnected() | 确定基础数据库连接是否已连接。 |

表 8-3

Session资源管理方法

|

方法

|

描述

| | --- | --- | | contains() | 确定特定对象是否与数据库相关联。 | | clear() | 清除所有已加载实例的会话,并取消任何尚未完成的保存、更新或删除操作。保留所有正在使用的迭代器。 | | evict() | 取消对象与会话的关联,以便不会保留对其进行的后续更改。 | | flush() | 将所有挂起的更改刷新到数据库中-将执行所有保存、更新和删除操作。本质上,这将使会话与数据库同步。然而,这仍然发生在事务的上下文中,因此它的有用性可能会受到所使用的事务种类的限制。 | | isOpen() | 确定会话是否已关闭。 | | isDirty() | 确定会话是否与数据库同步;如果会话没有将内存中的更改写入数据库表,则为true。 | | getCacheMode() | 确定当前使用的缓存模式。 | | setCacheMode() | 更改当前使用的缓存模式。 | | getCurrentLockMode() | 确定特定对象当前采用的锁定模式。(可使用lock()方法进行设置,例如,在许多其他选项中。) | | setFlushMode() | 确定当前使用的冲洗方法。选项包括每次操作后刷新、需要时刷新、从不刷新或仅在提交时刷新。 | | setReadOnly() | 将持久对象标记为只读(或可写)。将一个对象标记为只读会带来一些性能上的好处,但是在将其标记为可写之前,对其状态的更改将被忽略。 | | close() | 关闭会话,并因此关闭基础数据库连接;释放其他资源(如缓存)。调用close()后,不得对Session对象执行操作。 | | getSessionFactory() | 检索对创建当前Session实例的SessionFactory对象的引用。 |

表 8-2

Session事务和锁定的方法

|

方法

|

描述

| | --- | --- | | beginTransaction() | 开始事务。 | | getTransaction() | 检索当前事务对象。当没有事务正在进行时,这不会返回null。而是返回对象的active属性为false。 | | lock() | 获取一个对象的数据库锁(或者可以像merge()一样使用,如果给定了LockMode.NONE)。实际上,该方法检查数据库中的对象与内存中的对象相比的状态。 |

表 8-1

Session创建、读取、更新、删除的方法

|

方法

|

描述

| | --- | --- | | save() | 将对象保存到数据库。对于已经保存到数据库中的对象,不应调用此方法。 | | saveOrUpdate() | 将对象保存到数据库,如果对象已经存在,则更新数据库。这种方法比save()方法效率稍低,因为它可能需要执行一个SELECT语句来检查对象是否已经存在,但是如果对象已经被保存,它不会失败。 | | merge() | 将非持久化对象的字段合并到适当的持久化对象中(由 ID 决定)。如果数据库中不存在这样的对象,则创建并保存一个。 | | persist() | 将对象与会话重新关联,以便持久保存对对象所做的更改。 | | get() | 通过对象的标识符从数据库中检索特定对象。 | | getEntityName() | 检索实体名称(这通常与 POJO 的完全限定类名相同)。 | | getIdentifier() | 确定与会话关联的特定对象的标识符(表示主键的对象)。 | | load() | 通过对象的标识符从数据库加载一个对象(如果您不确定该对象是否在数据库中,并且您不想捕获一个异常,那么您应该使用get()方法)。 | | refresh() | 刷新数据库中关联对象的状态。 | | update() | 用对对象的更改更新数据库。 | | delete() | 从数据库中删除对象。 | | createFilter() | 创建过滤器(选择标准)以缩小数据库操作的范围。 | | enableFilter() | 在createFilter()生成的查询中启用命名过滤器。 | | disableFilter() | 禁用命名过滤器。 | | getEnabledFilter() | 检索当前启用的筛选器对象。 | | createQuery() | 创建要应用于数据库的 Hibernate 查询。 | | getNamedQuery() | 从映射文件中检索查询。 | | cancelQuery() | 取消另一个线程中当前正在进行的任何查询的执行。这不一定规定释放什么资源或何时释放;例如,尽管取消了查询,数据库仍可能尝试完成查询。 | | createCriteria() | 创建用于缩小搜索结果范围的 criteria 对象。 |

事务和锁定

事务和锁定密切相关:为执行事务而选择的锁定技术可以决定事务的性能和成功的可能性。所选的事务类型在某种程度上决定了它必须使用的锁定类型。

如果事务不符合您的需求,您没有义务使用它们,但是很少有好的理由来避免它们。如果您决定避免它们,您将需要在适当的时候调用会话中的flush()方法,以确保您的更改被持久化到数据库中。

不要回避事务。获取一个事务只需要很少的代码,我们已经在前面章节的SessionUtil中看到了使用 lambdas 管理带有活动SessionTransaction的操作的例子——知道事情何时以及如何发生的好处不能被夸大。值得重复的是:只使用事务。

处理

事务是一个工作单元,保证其行为就像您独占使用数据库一样。一般来说,如果你把工作包装在一个事务中,其他系统用户的行为不会影响你的数据。 2 一个事务可以被启动,提交将数据写入数据库,或者回滚以删除从头开始的所有更改(通常是错误的结果)。为了正确地完成一个操作,您从数据库获得一个Transaction对象(开始事务)并操纵会话,如下面的代码所示:

try(Session session = factory.openSession()) {
  session.beginTransaction();
  // Normal session usage here?
  session.getTransaction().commit();
} catch (HibernateException e) {
  Transaction tx = session.getTransaction();
  if (tx.isActive()) tx.rollback();
}

在现实世界中,并不希望所有的事务都是完全 ACID 的(参见下一节!)因为这会导致性能问题。

不同的数据库供应商支持并允许您或多或少地违反 ACID 规则,但是对隔离规则的控制程度实际上是由 SQL-92 标准规定的。有一些重要的原因让你想打破这个规则,所以 JDBC 和 Hibernate 都明确地考虑到了这一点。

酸性测试

ACID 是一个经常与数据库联系在一起的缩写词,代表一个事务所代表的四个属性。分别是原子性一致性隔离性耐久性:

  • 原子性:一个事务应该要么全部要么什么都没有。如果未能完成,数据库将保持原样,就像从未执行过任何操作一样——这被称为rollback。原子性意味着你不能只获得事务提交数据的一部分;事务中的更改作为一个单元应用。

  • 一致性:事务应该不会违反为数据库定义的任何规则。例如,必须遵守外键约束。如果由于某种原因这是不可能的(例如,您试图持久化与模式不一致的数据),事务将被回滚。

  • 隔离:在事务成功完成之前,该事务的影响对所有其他事务都是完全不可见的。这保证了事务将总是看到处于合理状态的数据。例如,考虑对用户地址的更新是否应该只包含正确的地址(即,它永远不会有一个位置的房屋名称,而是另一个位置的邮政编码);如果没有隔离,一个事务可以很容易地看到另一个事务何时更新了第一部分但尚未完成。

  • 持久化:数据应该保持完整。如果系统由于任何原因出现故障,应该总是可以检索到故障发生前的数据库。

表 8-5 中列出了 JDBC(和休眠)允许的隔离级别。

表 8-5

JDBC 隔离级别

|

水平

|

名字

|

事务行为

| | --- | --- | --- | | Zero | 没有人 | 任何事情都是允许的;数据库或驱动程序不支持事务。 | | one | 未提交读取 | 允许脏的、不可重复的和幻像读取。 | | Two | 已提交读取 | 允许不可重复的读取和幻像读取。 | | four | 可重复读 | 允许幻像读取。 | | eight | 可序列化 | 这条规则必须绝对遵守。 |

脏读可以看到未提交事务的进行中的改变。与 ACID 列表中讨论的隔离示例一样,它可能会看到地址的错误邮政编码。

一个不可重复的 read 可以随着时间的推移看到相同查询的不同数据。例如,它可能在事务开始时确定特定用户的邮政编码,并在事务结束时再次确定邮政编码,然后两次都得到不同的答案,而不进行任何更新。

一个幻影读取看到相同查询的不同行数。例如,它可能在查询开始时看到数据库中有 100 个用户,在查询结束时看到 105 个用户,而没有进行任何更新。

Hibernate 将隔离视为一个全局设置:您以通常的方式应用配置选项hibernate.connection.isolation,将其设置为表 8-5 中允许的值之一。

数据库可以以多种方式符合这些不同的隔离级别,并且您将需要锁的工作知识,以便在所有情况下从您的应用中获得期望的行为和性能。

为了防止同时访问数据,数据库本身将获取该数据的锁。这可以仅在对数据进行瞬时操作时获取,也可以保留到事务结束。前者叫做乐观锁定,后者叫做悲观锁定

Read Uncommitted 隔离级别总是获取乐观锁,而 Serializable 隔离级别将只获取悲观锁。一些数据库提供了一个特性,允许您将FOR UPDATE查询附加到一个选择操作,这需要数据库获得一个悲观锁,即使在较低的隔离级别。

Hibernate 在这个特性可用的时候提供了一些支持,并通过添加描述从 Hibernate 自己的缓存中获得的额外隔离程度的工具来进一步发展这个特性。

LockMode枚举??控制这种细粒度的隔离(见表 8-6 )。它只适用于get()方法,所以它是有限的;然而,如果可能的话,最好是直接控制前面提到的隔离。

表 8-6

可以请求的锁定模式

|

方式

|

描述

| | --- | --- | | NONE | 仅当缓存中的对象不可用时,才从数据库中读取。 | | READ | 从数据库中读取,而不考虑缓存中的内容。 | | UPGRADE | 为要访问的数据获取特定于方言的升级锁(如果数据库中有这种锁的话)。 | | UPGRADE_NOWAIT | 行为类似于UPGRADE,但是当数据库和方言提供支持时,该方法将立即失败并出现异常。如果没有该选项,或者在不支持该选项的数据库上,查询必须等待锁被授予(或者等待超时)。 |

当 Hibernate 写入当前事务中的某一行时,它会自动获得另一个锁模式WRITE。这个模式不能被显式设置,但是调用getLockMode()可能会返回它。

讨论了锁定的一般情况后,我们需要触及锁可能导致的一些问题。

僵局

当两个资源争用依赖关系而没有解决方案时,就会发生死锁。例如,假设您有两个需要资源“A”和“B”的进程——只不过第一个进程先获取资源 A,然后访问 B,第二个进程先获取资源 B,然后加载 A。如果第一个进程获取 A,然后等待访问 B,但第二个进程在进程 A 获取它之前加载 B,则它们在尝试获取第二个资源时会死锁。

它看起来像这样:

|

流程一

|

流程二

| | --- | --- | | 锁定资源 A |   | |   | 锁定资源 B | |   | 等待,直到有可用的 | | 等到 B 可用 |   |

Hibernate 可以检测到这种循环,如果发现,它将抛出一个错误(一个OptimisticLockException,因为我们依赖乐观锁)。让我们创建一个,这样我们就可以看到发生了什么。我们的例子将把两个Runnable实例提交到一个ServiceExecutor中,每个实例将获得(并修改,因此锁定)两个资源,除了顺序不同,因此造成了我们的死锁情况。之后,它将通过确定数据是否回到其原始(未修改)状态来验证两个事务都失败了。

首先,我们当然需要我们的项目模型。

这个项目中有一些元素适用于缓存部分,我们将在本章后面讨论。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-6-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>5.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter08</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.ignite</groupId>
            <artifactId>ignite-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate.orm</groupId>
            <artifactId>hibernate-jcache</artifactId>
        </dependency>
    </dependencies>
</project>

Listing 8-1chapter08/pom.xml

接下来,我们需要一个合作实体。

package chapter08.model;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Data
public class Publisher {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Long id;
  String name;
}

Listing 8-2chapter08/src/main/java/chapter08/model/Publisher.java

这是 Hibernate 配置文件。注意,它和pom.xml一样,有一些与缓存相关的东西;这些将在本章后面使用。

<!DOCTYPE hibernate-configuration PUBLIC

    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!--  Database connection settings  -->
    <property name="connection.driver_class">org.h2.Driver</property>
    <property name="connection.url">jdbc:h2:./db8</property>
    <property name="connection.username">sa</property>
    <property name="connection.password"/>
    <property name="dialect">org.hibernate.dialect.H2Dialect</property>

    <property name="hibernate.cache.region.factory_class">
      jcache
    </property>
    <property name="hibernate.javax.cache.missing_cache_strategy">
      create
    </property>

    <!--  Echo all executed SQL to stdout  -->
    <property name="show_sql">true</property>
    <property name="use_sql_comments">true</property>

    <!--  Drop and re-create the database schema on startup  -->
    <property name="hbm2ddl.auto">create-drop</property>

    <mapping class="chapter08.model.Publisher"/>
  </session-factory>
</hibernate-configuration>

Listing 8-3chapter08/src/test/resources/hibernate.cfg.xml

最后我们来看死锁例子本身。它看起来很长,但是大部分的复杂性在于试图确保更新在正确的时间以正确的顺序发生。让我们看一下代码,然后解包。

package chapter08;

import chapter08.model.Publisher;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.PessimisticLockException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory

;
import org.testng.annotations.Test;

import javax.persistence.OptimisticLockException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.testng.Assert.assertEquals;

public class DeadlockExample {
  Logger logger= LoggerFactory.getLogger(this.getClass());

  private Long createPublisher(Session session, String name) {
    Publisher publisher = new Publisher();
    publisher.setName(name);
    session.save(publisher);
    return publisher.getId();
  }

  private void updatePublishers(String prefix, Long... ids) {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      for (Long id : ids) {
        Thread.sleep(300);
        Publisher publisher = session
            .byId(Publisher.class)
            .load(id);
        publisher.setName(prefix + " " + publisher.getName());
      }
      tx.commit();
    } catch (OptimisticLockException e) {
      logger.error("lock exception with prefix "+ prefix);
    } catch(InterruptedException ignored) {
    }
  }

  @Test
  public void showDeadlock() throws InterruptedException {

    Long publisherAId;
    Long publisherBId;

    //clear out old data and populate tables
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session
          .createQuery("delete from Publisher")
          .executeUpdate();

      publisherAId = createPublisher(session, "A");
      publisherBId = createPublisher(session, "B");
      tx.commit();
    }

    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.submit(
        () -> updatePublishers("session1", publisherAId, publisherBId));
    executor.submit(
        () -> updatePublishers("session2", publisherBId, publisherAId));
    executor.shutdown();

    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
      executor.shutdownNow();
      if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        System.out.println("Executor did not terminate");
      }
    }
    try (Session session = SessionUtil.getSession()) {
      Query<Publisher> query = session.createQuery(
          "from Publisher p order by p.name",
          Publisher.class
      );
      String result = query
          .list()
          .stream()
          .map(Publisher::getName)
          .collect(Collectors.joining(","));
      assertEquals(result, "A,B");
    }
  }
}

Listing 8-4chapter08/src/test/java/chapter08/DeadlockExample.java

我们看到的第一个方法是createPublisher(),它接受一个活动的Session和一个“发布者名称”——并返回它刚刚为我们保存的Publisher的一个id。当我们试图更新Publisher时,我们将使用该标识符来锁定它。

第二种方法是updatePublishers(),它需要一个“前缀”和一个键列表来更新。这个方法的要点在这个测试的上下文之外是无意义的;只需在每个标识符被传入的Publisher的名字前加上prefix,按顺序加上的*,每次更新之间有相当长的延迟。我们将从两个不同的线程中执行这个方法,每个线程都有不同的标识符顺序,这就造成了我们的死锁情况。*

它甚至告诉我们它何时得到锁异常——“以防万一”

为什么延迟这么久?主要是因为你不能保证遗嘱执行人什么时候会真正开始执行。在大多数机器上,300 毫秒的延迟是不必要的;但是,请注意,您的 CPU 可能不同,您可能需要调整延迟。

最后,我们来看看实际的测试本身。它有三个阶段:第一阶段建立我们的数据(使用createPublisher()方法);第二个创建一个ExecutorService并提交两个任务,由updatePublishers()执行,颠倒标识符的顺序:第一个执行程序更新第一个,然后更新第二个Publisher,另一个执行程序更新第二个,然后更新第一个*Publisher,这会创建冲突的更新。*

*showDeadlock()的最后一个阶段检索一组Publisher实体,按照name对它们进行排序,这样结果是可预测的,并验证名称分别是“A”和“B”,这是我们第一次创建它们时设置的名称。

请记住,我们希望它们保持不变,因为我们故意制造了一个死锁情况,这样两组更新都会失败。

在输出中,连同任何其他日志记录信息,我们应该看到以下消息,尽管它们的顺序可能因您而异(我还从消息中截取了时间戳,因为它们是不相关的):

[pool-1-thread-2] ERROR chapter08.DeadlockExample - lock exception with prefix session2
[pool-1-thread-1] ERROR chapter08.DeadlockExample - lock exception with prefix session1

贮藏

访问数据库是一个昂贵的 4 操作,即使是简单的查询。请求必须被发送(通常通过网络)到服务器。数据库服务器可能必须将 SQL 编译成查询计划。查询计划必须运行,并且很大程度上受到磁盘性能的限制。产生的数据必须被传送回(同样,通常通过网络T5到客户端,然后应用才能开始处理结果。

如果查询运行多次,大多数好的数据库都会缓存查询结果,从而消除磁盘 I/O 和查询编译时间。但是,如果有大量的客户端发出完全不同的请求,那么这种方法的价值将是有限的。即使高速缓存通常保存结果,通过网络传输信息所花费的时间通常也是延迟的主要部分。

一些应用将能够利用进程内数据库,但这是例外而不是规则——这种数据库有其自身的局限性。

自然而明显的答案是在数据库连接的客户端有一个缓存。这不是 JDBC 直接提供或支持的特性,但是 Hibernate 提供了一个缓存(一级,或 L1 缓存),所有请求都必须通过它。二级缓存(L2)是可选和可配置的。

L1 缓存确保在一个会话中,对数据库中给定对象的请求总是返回相同的对象实例,从而防止数据冲突,并防止 Hibernate 多次尝试加载一个对象。

L1 缓存中的项目可以通过在会话中为您希望丢弃的对象调用evict()方法来单独丢弃。要丢弃 L1 缓存中的所有项目,调用clear()方法。

通过这种方式,Hibernate 比传统的 JDBC 方法有一个主要的优势:不需要开发人员做额外的工作,Hibernate 应用就可以获得客户端数据库缓存的好处。

图 8-2 显示了会话可用的两个缓存:强制的 L1 缓存,所有请求都必须通过它,以及可选的 L2 缓存。在尝试在 L2 缓存中定位对象之前,将始终查询 L1 缓存。您会注意到 L2 缓存在 Hibernate 的外部;尽管它是以对 Hibernate 用户透明的方式通过会话访问的,但它是各种缓存的可插拔接口,这些缓存与 Hibernate 应用维护在同一个 JVM 上,或者维护在外部 JVM 上。这允许在同一台机器上的应用之间或者甚至在多台机器上的多个应用之间共享缓存。

img/321250_5_En_8_Fig2_HTML.png

图 8-2

Session和缓存的关系

原则上,任何第三方缓存都可以和 Hibernate 一起使用。提供了一个org.hibernate.Cache接口,必须实现该接口才能为 Hibernate 提供缓存实现的句柄。然后,通过将实现类名作为hibernate.cache.provider_class属性的值来指定缓存提供者。

对于 Hibernate 6,首选的缓存机制是使用 JCache 兼容的提供者。JCache 是管理缓存应该提供的最少功能的规范(就像 JPA 是管理持久化框架应该提供的最少功能的规范一样)。有很多兼容 JCache 的库(?? );已知提供商列表见 https://jcp.org/aboutJava/communityprocess/implementations/jsr107/index.html

通过选择一个CacheMode枚举(参见表 8-7 )并使用Session.setCacheMode()方法应用它,可以在每个会话的基础上配置对 L2 缓存的访问类型。

表 8-7

缓存模式选项

|

方式

|

描述

| | --- | --- | | NORMAL | 根据需要从缓存中读取数据,并将其写入缓存。 | | GET | 数据永远不会添加到缓存中(尽管缓存条目在被会话更新时会失效)。 | | PUT | 永远不会从缓存中读取数据,但是当会话从数据库中读取缓存条目时,缓存条目会被更新。 | | REFRESH | 这与 PUT 相同,但是如果已经设置了 use_minimal_puts Hibernate 配置选项,它将被忽略。 | | IGNORE | 数据永远不会从缓存中读取或写入缓存(除非缓存条目在被会话更新时仍然无效,以防另一个Session以某种方式缓存了它们)。 |

CacheMode设置不影响 L1 缓存的访问方式。

使用 L2 缓存的决定并不明确。虽然它有可能大大减少对数据库的访问,但好处取决于缓存的类型和访问方式。

分布式缓存会导致额外的网络流量。某些类型的数据库访问可能会导致缓存内容在使用前被刷新;在这种情况下,它会给事务增加不必要的开销。

L2 缓存无法解释底层数据中的更改,这些更改是不支持缓存的外部程序的操作的结果。这可能会导致陈旧数据的问题,而这不是 L1 缓存的问题。

实际上,与大多数优化问题一样,最好在真实的负载条件下进行性能测试。这将让您确定是否需要缓存,并帮助您选择哪一个将提供最大的改进。

实际上,配置缓存使用非常简单。在本例中,为了设置好一切,我们需要执行以下操作:

  1. 选择一个缓存提供者并将依赖项添加到 Maven。

  2. 将 Hibernate 配置为将缓存提供者用于二级缓存。

  3. 改变我们的实体,将它们标记为可缓存的。

我们将选择 Apache Ignite 作为缓存提供者,因为在 Java SE 环境中设置它很简单。Maven 的依赖块如下所示: 6

<dependency>
  <groupId>org.apache.ignite</groupId>
  <artifactId>ignite-core</artifactId>
  <version>2.10.0</version>
</dependency>

我们可以通过在配置中添加一些属性来告诉 Hibernate 使用我们的二级缓存,正如我们在本章前面已经看到的:

<property
  name="hibernate.cache.region.factory_class">
  jcache
</property>
<property
  name="hibernate.javax.cache.missing_cache_strategy">
  create
</property>

我们需要做的最后一件事是将实体标记为可缓存的。在清单 8-5 中,我们将创建一个简单的Supplier实体(我们将在接下来的章节中再次讨论)并将其标记为二级缓存的候选对象。

package chapter08.model;

import lombok.Data;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Data
public class Supplier implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @Column(unique = true)
  String name;

  public Supplier(String name) {
    this.name = name;
  }

  public Supplier() {
  }
}

Listing 8-5chapter08/src/main/java/chapter08/model/Supplier.java

如果我们在一个会话中加载一个特定的Supplier,然后在另一个会话中立即加载相同的Supplier,数据库将不(必然)被查询,因为每次都是从二级缓存中而不是从数据库中提取。使用不同的会话是必要的,因为Supplier实例将被缓存在每个Session的一级缓存中;会话共享二级缓存,而不是一级缓存。

让我们通过另一个测试来展示这一点。

package chapter08;

import chapter08.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.List;

public class QueryTest {
  List<Integer> keys = new ArrayList<>();

  @BeforeMethod
  public void populateData() {
    clearSuppliers();
    Session session = SessionUtil.getSession();
    Transaction tx = session.beginTransaction();
    for (int i = 0; i < 10; i++) {
      Supplier supplier = new Supplier("Supplier " + (i + 1));
      session.save(supplier);
      keys.add(supplier.getId());
    }
    tx.commit();
    session.close();
  }

  @AfterMethod
  public void clearSuppliers() {
    Session session = SessionUtil.getSession();
    Transaction tx = session.beginTransaction();

    session.createQuery("delete from Supplier")
        .executeUpdate();
    tx.commit();
    session.close();
  }

  @Test
  public void testSuppliers() {
    for(int i=0;i<100; i++) {
      // create a new Session every loop...
      try(Session session=SessionUtil.getSession()) {
        Transaction tx = session.beginTransaction();
        Integer key=keys.get((int)(Math.random()*keys.size()));
        Supplier supplier = session.get(Supplier.class,key);
        System.out.println(supplier.getName());
        tx.commit();
      }
    }
  }
}

Listing 8-6chapter08/src/test/java/chapter08/QueryTest.java

这里,我们通过在数据库中创建一组Supplier实例来开始我们的测试。(当然,我们有很多方法可以做到这一点,但这非常简单。)

实际的测试本身—testSuppliers—只是使用一个随机键对我们的一组Supplier实例进行大量加载。然后它打印出每次装载的供应商名称,因此它将生成一个相当长的输出。如果您运行这个测试,输出看起来像这样,因为我们在hibernate.cfg.xml的配置中打开了“显示 SQL”:

Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 9
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 2
Supplier 2
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 7
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 5
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 6
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 10
Supplier 7
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 1
Hibernate: select s1_0.id, s1_0.name from Supplier as s1_0 where s1_0.id = ?
Supplier 8
Supplier 10
Supplier 9

您会注意到数据库实际上并不经常被查询;我们将看到发出十条 SQL 语句(过一会儿,因为这不是输出的完整运行),带有一批重复的供应商名称;这是因为会话正在从 Ignite 缓存中加载供应商。

这个有用吗?很难说。在这种情况下,它对于演示缓存的功能当然是有用的,但是这真的是性能改进吗?不够重要;对于我们的测试来说,生成控制台输出是整个测试中最昂贵的部分,缓存为我们节省的很少。对于一个真正的应用来说,建议是总是在真实的读写条件下进行彻底的测试,并测量不同配置的结果。

二级缓存可以显著提高性能,但前提是条件合适并且应用得当。

线

考虑了 Hibernate 应用可用的缓存后,现在您可能会担心如果两个执行线程争用 Hibernate 会话缓存中的同一个对象,会有传统 Java 死锁的风险。

原则上,这是可能的,与数据库死锁不同,Java 线程死锁不会超时并显示错误消息。幸运的是,有一个非常简单的解决方案:

Patient: Doctor, it hurts when I do this.
Doctor: Don’t do that, then.

不要在线程间共享Session对象。这将消除会话缓存中包含的对象死锁的风险。

确保不在当前线程外使用同一个Session对象的最简单方法是使用当前方法的本地实例,或者创建一个会话并将其传递给多个“工作方法”,当操作完成时关闭会话:

try(Session session=SessionUtil.getSession()) {
  Transaction tx=session.beginTransaction();
  operationOne(session);
  operationTwo(session);
  operationThree(session);
  tx.commit();
}

如果您必须在更长的时间内维护一个实例,那么就在一个ThreadLocal对象中维护这个实例。然而,在大多数情况下,Session对象的轻量级特性使得构造、使用和销毁一个实例比存储一个会话更实际。

摘要

在这一章中,我们已经讨论了Session对象的性质以及如何使用它们来获取和管理事务。我们已经了解了应用可用的两个级别的缓存,以及并发线程应该如何管理会话。

在下一章,我们将讨论从数据库中检索对象的各种方法。我们还将向您展示如何使用 HQL 对数据库执行更复杂的查询。

Footnotes 1

换句话说,Session几乎用于所有事情,这使得它更像关键。去想想。

  2

严格地说,事务给了你能力来隔离数据库上其他同时发生的操作。

  3

https://docs.jboss.org/hibernate/orm/6.0/javadocs/org/hibernate/LockMode.html

  4

“贵”是一个相对的术语,它非常依赖于你的比较点。正如这里所使用的,与访问内存中的数据相比,访问数据库是“昂贵的”——内存中的操作,即使是在大型数据集上,也可能需要几微秒,而数据库操作必须与网络一起工作,这增加了毫秒。这听起来可能不多,但正如 R. Admiral 格蕾丝·赫柏曾经指出的,几毫秒加起来…但将数据库上的 4 毫秒操作与加载物理磁带可能需要的 3 分钟相比较,数据库看起来棒极了

  5

请注意,由于我们使用的是嵌入式数据库,我们的大多数示例根本不会跨越物理网络接口,因为没有什么比讽刺更好的了。

  6

实际上,本书源代码中使用的依赖块没有<version>标签,因为版本是由顶层项目模型管理的。

 

*

九、搜索和查询

在上一章中,我们讨论了如何使用 Hibernate 会话与数据库进行交互。一些会话的方法在其参数列表中接受查询字符串或返回Query对象。这些方法用于从数据库请求任意信息。为了充分展示它们是如何被使用的,我们必须引入 Hibernate 查询语言(HQL ),用于表述这些请求。除了提取信息(用SELECT),HQL 还可以用来修改数据库中的信息(用INSERTUPDATEDELETE)。我们将在本章中介绍所有这些基本功能。

HQL 是一种面向对象的查询语言,类似于 SQL,但 HQL 不是对表和列进行操作,而是处理持久对象及其属性。它是 JPQL 的超集,JPQL 是 Java 持久化查询语言;JPQL 查询是有效的 HQL 查询,但不是所有的 HQL 查询都是有效的 JPQL 查询。

HQL 是一种有自己句法和语法的语言。HQL 查询的表达方式很像 SQL 本身,像from Product p一样是一个字符串。最终,Hibernate 会将您的 HQL 查询翻译成传统的 SQL 查询;Hibernate 还提供了一个 API,允许您直接发出 SQL 查询。

Hibernate 查询语言(HQL)

虽然大多数 ORM 工具和对象数据库都提供了对象查询语言,但 Hibernate 的 HQL 以其完整和易用而脱颖而出。虽然您可以在 Hibernate 中直接使用 SQL 语句(这将在本章的“使用原生 SQL”一节中详细介绍),但我们建议您尽可能使用 HQL(或标准),以避免数据库可移植性的麻烦,并利用 Hibernate 的 SQL 生成和缓存策略。除了相对于传统 SQL 的技术优势,HQL 是一种比 SQL 更紧凑的查询语言,因为它可以利用 Hibernate 映射中定义的关系信息。

我们意识到,并不是每个开发人员都相信 Hibernate 生成的 SQL 是经过完美优化的。如果您在查询中遇到性能瓶颈,我们建议您在关键组件的性能测试期间对数据库使用 SQL 跟踪。如果您发现某个领域需要优化,首先尝试使用 HQL 进行优化,然后再使用原生 SQL。Hibernate 通过 JMX MBean 提供统计信息,您可以用它来分析 Hibernate 的性能。Hibernate 的统计数据还可以让您深入了解缓存的性能。

如果您想通过基于 GUI 的工具执行 HQL 语句,Hibernate 团队在 Hibernate Tools 子项目中为 Eclipse 提供了一个 Hibernate 控制台。这个控制台是 Eclipse 最新版本的插件;更多信息见 https://tools.jboss.org/ 。其他 ide 也有类似的功能。

语法基础

HQL 受到了 SQL 的启发,也是 Java 持久化查询语言(JPQL)的主要灵感来源。JPQL 规范包含在 Java 社区过程网站( www.jcp.org/en/jsr/detail?id=338 )提供的 JPA 标准中。HQL 的句法被定义为一种反语法;语法文件包含在 Hibernate 核心下载的语法目录中。(ANTLR 是一个构建语言解析器的工具。)

由于 ANTLR 语法文件有些晦涩,而且根据 ANTLR 语法的规则,并不是每一个允许的语句都可以在 Hibernate 中使用,所以我们在本节中概述了四个基本 HQL 操作的语法。注意,下面对语法的描述并不全面;有一些不推荐使用的或者更晦涩的用法(特别是对于SELECT语句)没有在这里讨论。

更新

UPDATE改变数据库中现有对象的详细信息。这是一个从内存数据库的操作——更新不会影响你已经加载的任何东西。(当然,您可以更新已加载的对象,当提交事务时,任何更改都将被写入数据库;然而,这并不需要 HQL UPDATE。)下面是UPDATE语句的语法:

UPDATE [VERSIONED]
   [FROM] path [[AS] alias] [, ...]
   SET property = value [, ...]
   [WHERE logicalExpression]

“路径”是指一个或多个实体的完全限定名。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中的属性名称不明确时使用。

VERSIONED表示更新将更新时间戳,如果有的话,时间戳是被更新实体的一部分。

属性名称是“从”路径中列出的实体的属性名称。

逻辑表达式的语法将在后面的“对 HQL 使用限制”一节中讨论。

实际更新的示例可能如下所示:

Query query=session.createQuery(
    "update Person p set p.creditscore=:creditscore where p.name=:name"
    );
query.setInteger("creditscore", 612);
query.setString("name", "John Q. Public");
int modifications=query.executeUpdate();

Query正常输入;实际类型是Query<R>,我们在这里没有显示R!没关系,因为我们发布的是UPDATE,而不是选择;executeUpdate()调用返回修改记录的计数,我们丢弃了可能从list()操作返回的类型。

删除

DELETE从数据库中删除现有对象的详细信息。与更新一样,你已经加载的对象不受 HQL DELETE的影响。这也意味着,对于使用 HQL 执行的删除,将不遵循 Hibernate 的级联规则。但是,如果您在数据库级别指定了级联删除(直接或通过 Hibernate),数据库仍然会删除子行。这种删除方法通常称为“批量删除”,因为这是从数据库中删除大量实体的最有效方法。下面是DELETE语句的语法:

DELETE
   [FROM] path [[AS] alias]
   [WHERE logicalExpression]

UPDATE一样,path是实体的完全限定名。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中的属性名称不明确时使用。

实际上,删除可能如下所示:

Query query=session.createQuery(
    "delete from Person p where p.accountstatus=:status
    ");
query.setString("status", "toBePurged");
int rowsDeleted=query.executeUpdate();

UPDATE的例子一样,我们放弃了可能已经用Query指定的类型,因为我们只对删除的实体数量感兴趣;我们没有使用类型化查询。

插入

HQL INSERT不能用于直接插入任意实体——它只能用于插入从SELECT查询中获得的信息构建的实体(不像普通 SQL,在普通 SQL 中,INSERT命令可以用于将任意数据插入表中,以及插入从其他表中选择的值)。基本上,HQL INSERT使用来自数据库的数据来构建实体,而不是使用提供给它的数据。下面是INSERT语句的语法:

INSERT
   INTO path ( property [, ...])
   select

实体的名称由path表示。属性名是在合并的SELECT查询的FROM路径中列出的实体的属性名。

选择查询是一个 HQL SELECT查询(如下一节所述)。

由于该 HQL 语句只能使用 HQL select 提供的数据,因此其应用可能会受到限制。假设我们想把要删除的记录从一个USERS表复制到一个PURGED_USERS表,用于存档目的。 1 我们可以手工将USERS表中的记录复制到PURGED_USERS表中,这样就满足了INSERT对 HQL 的要求,比如:

Query query=session.createQuery(
    "insert into purged_users(id, name, status) "+
    "select id, name, status from users where status=:status"
    );
query.setString("status", "toBePurged");
int rowsCopied=query.executeUpdate();

与 HQL 的UPDATEDELETE一样,我们忽略了Query的类型,因为我们不要求返回类型化的信息,只要求返回修改的行数。

挑选

HQL SELECT用于查询数据库中的类及其属性。如前所述,这是对 HQL SELECT查询全部表达能力的简短总结。下面是SELECT语句的语法:

[SELECT [DISTINCT] property [, ...]]
   FROM path [[AS] alias] [, ...] [FETCH ALL PROPERTIES]
   WHERE logicalExpression
   GROUP BY property [, ...]
   HAVING logicalExpression
   ORDER BY property [ASC | DESC] [, ...]

实体的全限定名是path。别名可用于缩写对特定实体或其属性的引用,并且必须在查询中使用的属性名称不明确时使用。

属性名是在FROM路径中列出的实体的属性名。

如果使用了FETCH ALL PROPERTIES,那么懒惰加载语义将被忽略,所有被检索对象的立即属性将被主动加载(这不适用于递归;以这种方式加载的实体可能会也可能不会检索到它们的嵌套数据)。

当列出的属性仅由FROM子句中的别名组成时,在 HQL 中可以省略SELECT子句。如果你在 JPQL 中使用 JPA,HQL 和 JPQL 的一个区别是 JPQL 中需要SELECT子句。

因此,在 HQL,使用“FROM USERS U”作为查询是可以接受的,而在 JPQL 中对应的是SELECT U FROM USERS U

命名查询

Hibernate(和 JPA)提供了命名查询。命名查询是通过实体上的类级注释创建的;通常,查询应用于它们出现在源文件中的实体,但是没有绝对的要求这是真的。

命名查询是用@NamedQueries注释创建的,它包含一组@NamedQuery集合;每个都有查询内容(查询本身)和名称。

首先,让我们看一下项目模型,在这个框架下我们可以构建我们的项目。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>hibernate-6-parent</artifactId>
        <groupId>com.autumncode.books.hibernate</groupId>
        <version>5.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <artifactId>chapter09</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.autumncode.books.hibernate</groupId>
            <artifactId>util</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>${hibernate.validator.version}</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator-cdi</artifactId>
            <version>${hibernate.validator.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.el</groupId>
            <artifactId>javax.el-api</artifactId>
            <version>${javax.el-api.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId>
            <version>${javax.el-api.version}</version>
        </dependency>
    </dependencies>
</project>

Listing 9-1chapter09/pom.xml

接下来,让我们创建一个可以用作示例的对象模型。我们的对象模型将包含产品和供应商;它还将包含一个专门的产品(“软件”),为Product添加一个属性。我们在这里使用的层次结构的一个影响是 Lombok 不再像以前一样可用, 2 所以我们将从源代码中删除一些样板文件——即构造函数、赋值函数、访问函数、equals()hashCode()toString()。当然,本书的源代码下载将会包含所有这些方法。

package chapter09.model;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Product implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  Supplier supplier;
  @Column
  @NotNull
  String name;
  @Column
  @NotNull
  String description;
  @Column
  @NotNull
  Double price;

  public Product() {
  }

  public Product(Supplier supplier,
                 String name,
                 String description,
                 Double price) {
    this.supplier = supplier;
    this.name = name;
    this.description = description;
    this.price = price;
  }
}

Listing 9-2chapter09/src/main/java/chapter09/model/Product.java

package chapter09.model;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Supplier implements Serializable {
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;
  @Column(unique = true)
  @NotNull
  String name;
  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true,
      mappedBy = "supplier", targetEntity = Product.class)
  List<Product> products = new ArrayList<>();

  public Supplier(String name) {
    this.name = name;
  }

  public Supplier() {
  }
}

Listing 9-3chapter09/src/main/java/chapter09/model/Supplier.java

package chapter09.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Objects;

@Entity
public class Software extends Product implements Serializable {
  @Column
  @NotNull
  String version;

  public Software() {
  }

  public Software(Supplier supplier,
                  String name,
                  String description,
                  Double price,
                  String version) {
    super(supplier, name, description, price);
    this.version = version;
  }
}

Listing 9-4chapter09/src/main/java/chapter09/model/Software.java

当然,我们还需要 Hibernate 配置。

<?xml version="1.0"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!--  Database connection settings  -->
    <property name="connection.driver_class">org.h2.Driver</property>
    <property name="connection.url">jdbc:h2:./db9</property>
    <property name="connection.username">sa</property>
    <property name="connection.password"/>
    <property name="dialect">org.hibernate.dialect.H2Dialect</property>
    <!-- set up c3p0 for use -->
    <property name="c3p0.max_size">10</property>
    <!--  Echo all executed SQL to stdout  -->
    <property name="show_sql">true</property>
    <property name="use_sql_comments">true</property>

    <!--  Drop and re-create the database schema on startup  -->
    <property name="hbm2ddl.auto">create-drop</property>

    <mapping class="chapter09.model.Software"/>
    <mapping class="chapter09.model.Product"/>
    <mapping class="chapter09.model.Supplier"/>
  </session-factory>
</hibernate-configuration>

Listing 9-5chapter09/src/test/resources/hibernate.cfg.xml

添加一个命名查询就像向其中一个实体添加注释一样简单。例如,如果我们想要添加一个命名查询来检索所有的Supplier实体,我们可以通过向任何实体添加一个@NamedQuery注释来实现,尽管将查询放在Supplier的源代码中最有意义:

@NamedQuery(name = "supplier.findAll", query = "from Supplier s")

当然,对于这样一个简单的查询,根本不需要命名查询*——您只需要使用查询文本。但是,为了保持一致性或者便于将来的维护,您可以使用命名查询。想象一下给Supplier添加一个字段,指示Supplier是否活动;然后,您可以轻松地更新查询以包含where active=true,而不必在代码中搜索查询Supplier集合的每个地方。*

*通过添加一个@NamedQueries 注释,然后将命名查询作为该注释的一部分嵌入到一个数组中,可以将查询分组在一起。这将类似于清单 9-6 中所示的内容。

@NamedQueries({
    @NamedQuery(name = "supplier.findAll",
        query = "from Supplier s"),
    @NamedQuery(name = "supplier.findByName",
        query = "from Supplier s where s.name=:name"),
    @NamedQuery(name = "supplier.averagePrice",
        query = "select p.supplier.id, avg(p.price) " +
            "from Product p " +
            "GROUP BY p.supplier.id"),
})
@NamedNativeQueries({
    @NamedNativeQuery(name = "supplier.findAverage",
        query = "SELECT p.supplier_id, avg(p.price) "
            + "FROM Product p GROUP BY p.supplier_id"
    )
})

Listing 9-6chapter09/src/main/java/chapter09/model/Supplier.java

敏锐的读者会看到@NamedNativeQueries在使用中。这将在后面解释,但是本地查询是通过 Hibernate 发出的 SQL 查询。如果你非常了解 SQL,并且你的目标是一个特定的数据库,那么本地查询是非常有用的;这里,我们实际上是使用简单的 SQL 从Product表构建一个投影。

使用命名查询非常简单。让我们创建一个为每个测试填充和清除数据的TestBase类,然后我们将创建一个使用我们的supplier.findAll查询的测试。??

使用TestBase非常简单,尽管它可能不是写过的最好的类;在运行测试之前,它填充一个数据集并初始化一个Session;测试结束后,关闭Session并进行清理。(它使用Session的方式主要是帮助缩短使用 TestBase 的类。)

package chapter09;

import chapter09.model.Product;
import chapter09.model.Software;
import chapter09.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;

public class TestBase {
  Session session;
  Transaction tx;

  @BeforeMethod
  public void populateData() {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();

      Supplier supplier = new Supplier("Hardware, Inc.");
      supplier.getProducts().add(
          new Product(supplier, "Optical Wheel Mouse", "Mouse", 5.00));
      supplier.getProducts().add(
          new Product(supplier, "Trackball Mouse", "Mouse", 22.00));
      session.save(supplier);

      supplier = new Supplier("Supplier 2");
      supplier.getProducts().add(

          new Software(supplier, "SuperDetect", "Antivirus", 14.95, "1.0"));
      supplier.getProducts().add(
          new Software(supplier, "Wildcat", "Browser", 19.95, "2.2"));
      supplier.getProducts().add(
          new Product(supplier, "AxeGrinder", "Gaming Mouse", 42.00));

      session.save(supplier);
      tx.commit();
    }

    this.session = SessionUtil.getSession();
    this.tx = this.session.beginTransaction();
  }

  @AfterMethod
  public void closeSession() {
    session.createQuery("delete from Product").executeUpdate();
    session.createQuery("delete from Supplier").executeUpdate();
    if (tx.isActive()) {
      tx.commit();
    }
    if (session.isOpen()) {
      session.close();
    }
  }

}

Listing 9-7chapter09/src/test/java/chapter09/TestBase.java

这里有一个测试,使用了我们在Supplier中的一个命名查询。因为它使用的是已知的数据集,所以它可以查询返回列表的大小,并在此基础上验证查询是否成功。

package chapter09;

import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestNamedQuery extends TestBase{
  @Test
  public void testNamedQuery() {
    Query<Supplier> query = session.getNamedQuery("supplier.findAll");
    List<Supplier> suppliers = query.list();
    assertEquals(suppliers.size(), 2);
  }
}

Listing 9-8chapter09/src/test/java/chapter09/TestNamedQuery.java

当然,我们可以随时创建查询,就像我们在本书中多次展示的那样。

package chapter09;

import chapter09.model.Product;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestSimpleQuery extends TestBase{
  @Test
  public void testSimpleQuery() {
    Query<Product> query = session.createQuery(
        "from Product",
        Product.class);

    query.setComment("This is only a query for product");
    List<Product> products = query.list();
    assertEquals(products.size(), 5);
  }
}

Listing 9-9chapter09/src/test/java/chapter09/TestSimpleQuery.java

createQuery()方法接受一个有效的 HQL 语句(如果需要,还有一个 Java 类型引用,比如Supplier.class)并返回一个org.hibernate.query.Query对象。Query接口提供了将查询结果作为 Java ListIterator或唯一结果返回的方法。如果你提供一个类型引用,许多操作将使用该类型作为返回值,所以如果你正在寻找一个Supplier实体的列表,你可以使用createQuery("from Supplier s", Supplier.class),并且list()将返回一个List<Supplier>而不是一个List<Object>。其他功能包括命名参数、结果滚动、JDBC 提取大小和 JDBC 超时。您还可以向 Hibernate 创建的 SQL 添加注释,这对于跟踪哪些 HQL 语句对应于哪些 SQL 语句非常有用,如下一节所示。

像所有 SQL 语法一样,您可以用小写或大写(或混合大小写)来编写。但是,您在 HQL 查询中引用的任何 Java 类或属性都必须以正确的大小写指定。例如,当您查询名为 Product 的 Java 类的实例时,HQL 查询"from Product"相当于"FROM Product"。然而,HQL 查询“来自产品”与 HQL 查询"from Product"不同。因为 Java 类名区分大小写,所以 Hibernate 也区分类名的大小写。

记录和注释底层 SQL

Hibernate 可以将 HQL 查询背后的底层 SQL 输出到应用的日志文件中。如果 HQL 查询没有给出您期望的结果,或者如果查询花费的时间比您期望的长,这将特别有用。您可以稍后在数据库的查询分析器中直接运行 Hibernate 针对您的数据库生成的 SQL 来确定问题的原因。这不是一个必须经常使用的特性,但是如果您必须向数据库管理员寻求帮助来调优 Hibernate 应用时,这是非常有用的。

记录 SQL

查看 Hibernate HQL 查询的 SQL 的最简单方法是使用 show_sql 属性在日志中启用 SQL 输出。在您的hibernate.cfg.xml配置文件中设置这个属性为 true, 4 和 Hibernate 将把 SQL 输出到日志中。您不需要启用任何其他日志记录设置,尽管将 Hibernate 的日志记录设置为 debug 也会输出生成的 SQL 语句,以及许多其他文字。

在 Hibernate 中启用 SQL 输出后,您应该重新运行前面的例子(清单 9-9 中的TestSimpleQuery测试)。以下是为产品中的 HQL 语句生成的 SQL 语句:

Hibernate: /* This is only a query for product */ select p1_0.id, p1_1.id, case when p1_1.id is not null then 1 when p1_0.id is not null then 0 end, p1_0.description, p1_0.name, p1_0.price, p1_0.supplier_id, p1_1.version from Product as p1_0 left outer join Software as p1_1 on p1_0.id = p1_1.id

顺便说一下,记住Software类继承自Product,这使得 Hibernate 为这个简单查询生成的 SQL 变得复杂。当我们从简单的Supplier类中选择所有对象时,为 HQL 查询"from Supplier"生成的 SQL 要简单得多:

Hibernate: /* dynamic native SQL query */ select s1_0.id, s1_0.name from Supplier as s1_0

如果您将 Hibernate 类的日志级别提高到 debug 5 ,您将在日志文件中看到 SQL 语句,以及大量关于 Hibernate 如何解析您的 HQL 查询并将其翻译成 SQL 的信息。

注释生成的 SQL

将您的 HQL 语句跟踪到生成的 SQL 可能很困难,因此 Hibernate 在 Query 对象上提供了一个注释工具,允许您将注释应用于特定的查询。Query<R>接口有一个setComment()方法,它接受一个字符串对象作为参数,如下所示:

public Query<R> setComment(String comment)

如果没有一些额外的配置,Hibernate 不会给你的 SQL 语句添加注释,即使你使用了setComment()方法。您还需要在 Hibernate 配置中将 Hibernate 属性use_sql_comments设置为true,如本章前面的清单所示。如果设置了该属性,但没有以编程方式对查询设置注释,Hibernate 将在注释中包含用于生成 SQL 调用的 HQL。我们发现这对于调试 HQL 非常有用。

如果启用了 SQL 日志记录,请使用注释来标识应用日志中的 SQL 输出。例如,如果我们给这个例子添加一个注释,Java 代码看起来会像这样:

String hql = "from Supplier";
Query<Supplier> query = session.createQuery(hql, Supplier.class);
query.setComment("My HQL: " + hql);
List<Supplier> results = query.list();

应用日志中的输出将在 SQL:

Hibernate: /*My HQL: from Supplier*/ select supplier0_.id as id, supplier0_.name as name2_ from Supplier supplier0_

这对于识别日志中的 SQL 非常有用,尤其是当您扫描大量 SQL 时,生成的 SQL 有点难以理解。(运行本章测试中的示例代码是一个很好的例子;这相当于数百行的输出。)

from子句和别名

我们已经在前面的“选择”一节中讨论了 HQL 的 from 子句的基本内容需要注意的最重要的特性是别名。Hibernate 允许您使用 as 子句为查询中的类分配别名。使用别名引用查询中的类。例如,我们之前的简单示例如下:

from Product as p

或以下内容:

from Product as product

您将在应用中看到这两种别名命名约定,尽管它通常用于缩短长查询(因此您将比其他此类形式更经常看到"from Product as p")。as关键字是可选的——您也可以直接在类名后指定别名,如下所示:

from Product product

如果您需要在 HQL 完全限定类名,只需指定包和类名。Hibernate 会在后台处理大部分的事情,所以只有当你的应用中有重名的类时,你才需要这么做。如果必须在 Hibernate 中这样做,请使用如下语法:

from chapter09.model.Product

对于直接处理对象来说,from子句是非常基本和有用的。但是,如果您想处理对象的属性而不将整个对象加载到内存中,您必须使用select子句。

select子句和投影

from子句相比,select子句提供了对结果集的更多控制。如果您想获得结果集中对象属性的子集——而不是完整的对象本身——使用select子句。例如,我们可以对数据库中的产品运行投影 6 查询,只返回产品名称,而不是将整个对象加载到内存中,如清单 9-10 中所示的类。

package chapter09;

import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestSimpleProjection extends TestBase {
  @Test
  public void testSimpleProjection() {
    Query<String> query = session.createQuery(
        "select p.name from Product p",
        String.class);
    List<String> suppliers = query.list();
    for (String s : suppliers) {
      System.out.println(s);
    }
    assertEquals(suppliers.size(), 5);
  }
}

Listing 9-10chapter09/src/test/java/chapter09/TestSimpleProjection.java

这个查询的结果集将包含一个 Java 字符串对象的List。此外,我们可以检索数据库中每个产品的价格和名称,如清单 9-11 所示。

package chapter09;

import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestBiggerProjection extends TestBase {
  @Test
  public void testBiggerProjection() {
    Query<Object[]> query = session.createQuery(
        "select p.name, p.price from Product p");
    List<Object[]> products = query.list();
    for (Object[] data : products) {
      System.out.println(Arrays.toString(data));
    }
    assertEquals(products.size(), 5);
  }
}

Listing 9-11chapter09/src/test/java/chapter09/TestBiggerProjection.java

当我们在下一章讨论“数据传输对象”或“dto”时,我们将以稍微不同的方式重新审视这个想法。Object[]的一个结果不是天生有用的。

这个结果集包含一个对象数组的List(因此,List<Object[]>)–每个数组代表一组属性(在本例中,是一个名称和价格对)。

如果您只对少数属性感兴趣,这种方法可以减少数据库服务器的网络流量,并节省应用机器上的内存。

对 HQL 使用限制

与 SQL 一样,使用 where 子句选择与查询表达式匹配的结果。HQL 提供了许多不同的表达式,可以用来构建查询。在 HQL 语语法中,有许多可能的表达方式, 7 包括这些:

  • 逻辑运算符:ORANDNOT

  • 相等运算符:=(表示“相等”)、<>!=^=(表示“不相等”)

  • 比较运算符:<>>=likenot likebetweennot between

  • 数学运算符:+-*/

  • 串联运算符:||

  • 案例:Case when <logical expression> then <unary expression> else <unary expression> end

  • 收藏表情:someexistsallany

此外,还可以在 where 子句中使用以下表达式:

  • HQL 命名的参数,如:date:quantity

  • JDBC 查询参数:?(在 HQL 很少使用,应该尽量避免使用命名参数)

  • 日期和时间 SQL-92 函数运算符:current_time()current_date()current_timestamp()

  • SQL 函数(数据库支持):length()upper()lower()ltrim()rtrim()等。

使用命名参数

Hibernate 在其 HQL 查询中支持命名参数。这使得编写接受用户输入的查询变得容易——并且您不必防御 SQL 注入攻击。

SQL 注入是一种针对应用的攻击,这些应用通过字符串连接直接从用户输入创建 SQL。例如,如果我们通过 web 应用表单接受用户的姓名,那么像这样构造 SQL(或 HQL)查询将是一种非常糟糕的形式:

String sql = "select p from products where name = '" + name + "'";

恶意用户可以向应用传递一个包含终止引号和分号的名称,后跟另一个 SQL 命令(如 delete from products ),这样用户就可以为所欲为。他们只需要用另一个与 SQL 语句的结束引号相匹配的命令来结束。 8 这是一种非常常见的攻击,尤其是如果恶意用户能够猜出你的数据库结构的细节。

对于每个查询,您可以自己避开用户的输入,但是如果您让 Hibernate 用命名参数管理您的所有输入,那么安全风险会小得多。Hibernate 的命名参数类似于 JDBC 查询参数(?)你可能已经很熟悉了,但是 Hibernate 的参数就没那么混乱了。如果您有一个在多个地方使用相同参数的查询,那么使用 Hibernate 的命名参数也更简单。

使用 JDBC 查询参数时,任何时候添加、更改或删除 SQL 语句的一部分,都需要更新设置其参数的 Java 代码,因为参数是根据它们在语句中出现的顺序进行索引的。Hibernate 允许您为 HQL 查询中的参数提供名称,因此您不必担心在查询中意外地移动参数。

package chapter09;

import chapter09.model.Product;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestNamedParams extends TestBase {
  @Test
  public void testNamedParams() {
    Query<Product> query = session.createQuery(
        "from Product where price >= :price",
        Product.class);
    query.setParameter("price",25.0);
    List<Product> products = query.list();
    assertEquals(products.size(), 1);
  }
}

Listing 9-12chapter09/src/test/java/chapter09/TestNamedParams.java

你甚至可以像这样提供对象引用,如清单 9-13 所示,这里我们看到query.setParameter("supplier", supplier);——这里,我们有一个映射到实际实体的“命名参数”。

package chapter09;

import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals

;
import static org.testng.Assert.assertNotNull;

public class TestNamedEntity extends TestBase {
  @Test
  public void testNamedEntity() {
    Query<Supplier> supplierQuery=session.createQuery(
            "from Supplier where name=:name",
        Supplier.class);
    supplierQuery.setParameter("name", "Supplier 2");
    Supplier supplier= supplierQuery.getSingleResult();
    assertNotNull(supplier);

    Query<Product> query = session.createQuery(
        "from Product where supplier = :supplier",
        Product.class);
    query.setParameter("supplier", supplier);

    List<Product> products = query.list();
    assertEquals(products.size(), 3);
  }
}

Listing 9-13chapter09/src/test/java/chapter09/TestNamedEntity.java

您还可以在 HQL 查询中使用常规的 JDBC 查询参数。我们看不出有什么理由让你想这么做,但是它们确实有效。

在结果集中翻页

对数据库查询的结果集进行分页是一种非常常见的应用模式。例如,通常情况下,您会将分页用于为查询返回大量数据的 web 应用。web 应用将浏览数据库查询结果集,为用户构建合适的页面。如果 web 应用将每个用户的所有数据都加载到内存中,那么应用会非常慢。相反,您可以浏览结果集并检索结果,您将一次显示一个块。

Query<R>界面上有两种分页方式:setFirstResult()setMaxResults()setFirstResult()方法接受一个表示结果集中第一行的整数,从第 0 行开始。您可以用setMaxResults()方法告诉 Hibernate 只检索固定数量的对象。您的 HQL 是不变的——您只需要修改执行查询的 Java 代码。这里有一个测试,展示了分页获取第五到第八个Supplier实体名称的过程。

package chapter09;

import chapter09.model.Supplier;
import com.autumncode.hibernate.util.SessionUtil;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.List;
import java.util.stream.Collectors;

import static org.testng.Assert.assertEquals;

public class TestPagination {
  @Test
  public void testPagination() {
    try (Session session = SessionUtil.getSession()) {
      Transaction tx = session.beginTransaction();
      session.createQuery("delete from Product").executeUpdate();
      session.createQuery("delete from Supplier").executeUpdate();

      for (int i = 0; i < 30; i++) {
        Supplier supplier = new Supplier();
        supplier.setName(String.format("supplier %02d", i));
        session.save(supplier);
      }

      tx.commit();
    }

    try (Session session = SessionUtil.getSession()) {
      Query<String> query = session.createQuery(
          "select s.name from Supplier s order by s.name",
          String.class);
      query.setFirstResult(4);
      query.setMaxResults(4);
      List<String> suppliers = query.list();
      String list = suppliers
          .stream()
          .collect(Collectors.joining(","));
      assertEquals(list,
          "supplier 04,supplier 05,supplier 06,supplier 07");
    }
  }
}

Listing 9-14chapter09/src/test/java/chapter09/TestPagination.java

你可以改变数字,玩分页。如果您打开 SQL 日志记录(就像我们的示例配置那样),您可以看到 Hibernate 使用哪些 SQL 命令进行分页。对于开源的 H2 数据库,Hibernate 使用了offsetfetch first ? rows only。对于其他数据库,Hibernate 使用适当的命令进行分页。如果您的应用在分页方面存在性能问题,这对于调试非常有帮助。

获得独特的结果

正如我们在TestNamedEntity.java源代码中看到的,HQL 的Query<R>接口提供了一个getSingleResult()方法,用于从 HQL 查询中获取一个对象。尽管您的查询可能只产生一个对象,但是如果您将结果限制为第一个结果,您也可以对其他结果集使用getSingleResult()方法。您可以使用上一节讨论的setMaxResults()方法。Query<R>对象上的getSingleResult()方法返回单个对象,如果没有结果,则返回null。如果有多个结果,那么getSingleResult()方法抛出一个NonUniqueResultException

package chapter09;

import chapter09.model.Product;
import org.hibernate.NonUniqueResultException;
import org.hibernate.query.Query;
import org.testng.annotations.Test;

public class TestSingleResult extends TestBase {
  @Test(expectedExceptions = NonUniqueResultException.class)
  public void testGetSingleResultBad() {
    Query<Product> query = session.createQuery(
        "from Product",
        Product.class);

    Product products = query.getSingleResult();
  }

  @Test
  public void testGetSingleResultGood() {
    Query<Product> query = session.createQuery(
        "from Product",
        Product.class);
    query.setMaxResults(1);
    Product products = query.getSingleResult();
  }
}

Listing 9-15chapter09/src/test/java/chapter09/TestSingleResult.java

*这不是好代码!*这个例子非常做作,结果是无序的;这个例子仅仅是为了说明以几种不同的方式使用getSingleResult()

使用 order by 子句对结果进行排序

要对 HQL 查询的结果进行排序,您需要使用order by子句。您可以根据结果集中对象的任何属性对结果进行排序:升序(asc)或降序(desc)。如果需要,可以在查询中对多个属性进行排序。用于排序结果的典型 HQL 查询如下所示:

from Product p where p.price>25.0 order by p.price desc

如果您想按多个属性排序,您只需将附加属性添加到order by子句的末尾,用逗号分隔。例如,您可以先按供应商的名称排序,然后按产品价格排序,如下所示:

from Product p order by p.supplier.name asc, p.price asc

与使用标准查询 API 的等效方法相比,HQL 更易于订购。

关联和加入

在对象关系映射器中,通常以两种方式使用连接。一种是基于连接的标准查询对象——我们在“使用命名参数”一节中看到了这一点。另一种方法是生成“投影”,这是一种数据结构,其唯一的功能是从单个查询中引用自定义对象。

然而,投影可以不仅仅是单个对象的字段子集(这是我们在本章前面的“SELECT”一节中看到的):它可以是 Hibernate 可以表示的任何类型。

这是我们对Product的一组查询。

@NamedQueries({
    @NamedQuery(name = "product.searchByPhrase",
        query = "from Product p "
            + "where p.name like :text or p.description like :text"),
    @NamedQuery(name = "product.findProductAndSupplier",
        query = "from Product p, Supplier s where p.supplier=s"),
})

Listing 9-16chapter09/src/main/java/chapter09/model/Product.java

名为product.findProductAndSupplier的查询从表面上看并不特别有用,因为我们可以通过Product中的属性获得产品的供应商。然而,为了便于讨论,让我们假设我们有一个用例,我们希望将产品及其供应商作为单独的字段——可能是因为产品的供应商不能被急切地获取。(同样,这完全是为了一个示例而构建的;在现实世界中,根据我们的数据模型,不需要这样的查询。)

注意,我们返回了两种类型:ProductSupplier,连接在p.supplier上——这意味着“返回每一个产品,对于每一个产品,返回该产品所涉及的供应商。”

这种查询返回的每一行的类型都是Object[]——一个对象数组。

清单 9-17 展示了它的实际效果。

package chapter09;

import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestJoinArray extends TestBase {
  @Test
  public void testJoinArray() {
    Query<Object[]> query = session.getNamedQuery(
        "product.findProductAndSupplier"
    );
    List<Object[]> suppliers = query.list();
    for (Object[] o : suppliers) {
      Assert.assertTrue(o[0] instanceof Product);
      Assert.assertTrue(o[1] instanceof Supplier);
    }
    assertEquals(suppliers.size(), 5);
  }
}

Listing 9-17chapter09/src/test/java/chapter09/TestJoinArray.java

当然,使用对象数组并不有趣;事实证明,我们实际上可以在查询中指定对象的类型(并构造它)。 10 这里,我们有一个用作对象的ProductAndSupplier类型——一个元组,一种数据组织——我们的查询指定了如何创建它。注意,我们需要使用完整的包名,因为 Hibernate 不知道它的类型(它不是一个实体),必须准确地告诉它如何创建它。

package chapter09;

import chapter09.model.Product;
import chapter09.model.Supplier;
import org.hibernate.query.Query;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.util.List;

import static org.testng.Assert.assertEquals;

class ProductAndSupplier {
  final Product p;
  final Supplier s;

  ProductAndSupplier(Product p, Supplier s) {

    this.p = p;
    this.s = s;
  }

  @Override
  public String toString() {
    return "ProductAndSupplier{" +
        "p=" + p +
        ",\n   s=" + s +
        '}';
  }
}

public class TestJoinObject extends TestBase {
  @Test
  public void testJoinObject() {
    Query<ProductAndSupplier> query = session.createQuery(
        "select new chapter09.ProductAndSupplier(p,s) " +
            "from Product p, Supplier s where p.supplier=s",
        ProductAndSupplier.class);
    List<ProductAndSupplier> suppliers = query.list();
    for (ProductAndSupplier o : suppliers) {
      System.out.println(o);
    }
    assertEquals(suppliers.size(), 5);
  }
}

Listing 9-18chapter09/src/test/java/chapter09/TestJoinObject.java

正如我们已经说过的,这个特定的查询示例没有太多意义,来自TestJoinObject的输出证实了这一点;Product输出实际上代表了我们期望的数据。

聚合方法

HQL 支持一系列聚合方法,类似于 SQL。它们在 HQL 中的工作方式与在 SQL 中相同,因此您不必学习任何特定的 Hibernate 术语。不同之处在于,在 HQL 中,聚合方法适用于持久对象的属性。count(...)方法返回给定列名在结果集中出现的次数。您可以使用“count(*)”语法对结果集中的所有对象进行计数,或者使用count(product.name)属性对结果集中的对象数量进行计数。下面是一个使用count(*)方法计算所有产品的示例:

select count(*) from Product product

distinct关键字只计算行集中的唯一值——例如,如果有 100 种产品,但其中 10 种与结果中的另一种产品价格相同,那么select count(distinct product.price) from Product查询将返回 90。在我们的数据库中,以下查询将返回 2,每个供应商一个:

select count(distinct product.supplier.name) from Product product

如果我们删除关键字distinct,它将返回 5,每个产品一个。

所有这些查询都返回列表中的一个Long对象。(换句话说,结果是一个整数值。)您可以在这里使用getSingleResult()方法来获得结果。

通过 HQL 可获得的集合函数包括:

  • avg(property name):房产价值的平均值

  • count(property name or *):属性在结果中出现的次数

  • max(property name):属性值的最大值

  • min(property name):属性值的最小值

  • sum(property name):属性值的总和

如果您有不止一个聚合方法,那么得到的List将包含一个Object数组,其中包含您请求的每个聚合。向 select 子句添加另一个聚合非常简单:

select min(product.price), max(product.price) from Product product

您还可以将这些属性与结果集中的其他投影属性结合起来。

使用 HQL 批量更新和删除

Query<R>接口包含一个名为executeUpdate()的方法,用于执行 HQL UPDATEDELETE语句。11executeUpdate()方法返回一个包含受更新或删除影响的行数的int,如下所示:

public int executeUpdate() throws HibernateException

基于 SQL UPDATE 语句,HQL 更新看起来就像您期望的那样。更新时不要包含别名;相反,将 set 关键字放在类名之后,如下所示:

String hql = "update Supplier set name = :newName where name = :name";
Query query = session.createQuery(hql);
query.setString("name","SuperCorp");
query.setString("newName","MegaCorp");
int rowCount = query.executeUpdate();
System.out.println("Rows affected: " + rowCount);
//See the results of the update
Query<Supplier> q = session.createQuery("from Supplier", Supplier.class);
List<Supplier> results = q.list();

执行此查询后,任何以前命名为 SuperCorp 的供应商都将被命名为 MegaCorp。您可以在 updates 中使用一个where子句来控制更新哪些行,也可以不使用它来更新所有行。注意,我们打印出了受查询影响的行数。对于这次批量更新,我们还在 HQL 中使用了命名参数。

批量删除的工作方式类似。将delete from子句与您想要删除的类名一起使用。然后使用where子句来缩小表中想要删除的条目的范围。使用executeUpdate()方法也可以对数据库执行删除。

对关系中的对象使用批量删除时要小心。Hibernate 不会知道您删除了数据库中的底层数据,您可能会得到外键完整性错误。

我们围绕 HQL 删除语句的代码基本上是相同的——我们使用命名参数,并打印出受删除影响的行数:

String hql = "delete from Product where name = :name";
Query query = session.createQuery(hql);
query.setString("name","Mouse");
int rowCount = query.executeUpdate();
System.out.println("Rows affected: " + rowCount);
//See the results of the delete
Query<Product> prodQuery = session.createQuery("from Product", Product.class);
List results = prodQuery.list();

在 HQL 中使用批量更新和删除几乎与在 SQL 中一样,所以请记住,这些功能非常强大,如果您在 where 子句中犯了一个错误,就可以删除表中的数据。

使用原生 SQL

尽管您应该尽可能使用 HQL,但是 Hibernate 确实提供了一种直接通过 Hibernate 使用原生 SQL 语句的方法。使用原生 SQL 的一个原因是,您的数据库通过其 SQL 方言支持一些 HQL 不支持的特殊功能。另一个原因是,您可能希望从 Hibernate 应用中调用存储过程。与其他 Java ORM 工具不同,Hibernate 不仅仅提供底层 JDBC 连接的接口,它还提供了一种定义查询使用的实体(或连接)的方法。这使得与其他面向 ORM 的应用的集成变得容易。

您可以修改您的 SQL 语句,使它们与 Hibernate 的 ORM 层一起工作。您确实需要修改您的 SQL 来包含对应于对象或对象属性的 Hibernate 别名。可以用objectname.*指定对象的所有属性,也可以直接用objectname.property指定别名。Hibernate 使用映射将对象属性名转换成底层 SQL 列。这可能不是您所期望的 Hibernate 的确切工作方式,所以请注意,您确实需要修改 SQL 语句以获得对 ORM 的完全支持。在带有子类的类上使用原生 SQL 时,您尤其会遇到问题——请确保您了解如何跨单个表或多个表映射继承,以便从表中选择正确的属性。

Hibernate 的底层原生 SQL 支持是org.hibernate.query.NativeQuery<T>接口,它扩展了org.hibernate.query.Query<T>接口。您的应用将使用会话接口上的createNativeQuery()方法从会话中创建一个本地 SQL 查询(从 QueryProducer 接口继承而来,但这可能比我们需要的更详细)。

public NativeQuery createNativeQuery(String sqlString)

你也可以使用命名的原生的查询。下面是一个使用来自Supplier的命名查询来查找每个Supplier的平均产品价格的示例。

package chapter09;

import org.hibernate.query.Query;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.List;

import static org.testng.Assert.assertEquals;

public class TestNativeQuery extends TestBase {
  @Test
  public void testNativeQuery() {
    Query query = session.getNamedQuery("supplier.findAverage");
    List<Object[]> suppliers = query.list();
    for (Object[] o : suppliers) {
      System.out.println(Arrays.toString(o));
    }
    assertEquals(suppliers.size(), 2);
  }

  @Test
  public void testHSQLAggregate() {
    Query query = session.getNamedQuery("supplier.averagePrice");
    List<Object[]> suppliers = query.list();
    for (Object[] o : suppliers) {
      System.out.println(Arrays.toString(o));
    }
    assertEquals(suppliers.size(), 2);
  }

}

Listing 9-19chapter09/src/test/java/chapter09/TestNativeQuery.java

testNativeQuery()中运行的实际查询是:

SELECT p.supplier_id, avg(p.price)
  FROM Product p
  GROUP BY p.supplier_id

再次注意,这需要底层数据库模式的知识,没有理由不使用 HQL 来获得相同的数据,如测试代码所示,等效的 HQL 为

select p.supplier.id, avg(p.price)
  from Product p
  GROUP BY p.supplier.id

这里唯一的区别是访问供应商标识符的方式;在第一个查询中,我们使用实际的底层supplier_id列,在后一个查询中,我们遍历图来获得它。在第二个查询中返回实际的Supplier会更有效,但是这里的重点是本地 SQL 执行。

摘要

HQL 是一种强大的面向对象的查询语言,它提供了 SQL 的强大功能,同时利用了 Hibernate 的对象关系映射和固有的缓存。如果要将现有的应用移植到 Hibernate,可以使用 Hibernate 的原生 SQL 工具来对数据库执行 SQL。SQL 功能对于执行特定于给定数据库的 SQL 语句也很有用,在 HQL 中没有对等的 SQL 语句。(它对于执行存储过程也很有用,这个概念依赖于您的特定数据库实现。)

您可以为 Hibernate 打开 SQL 日志记录,Hibernate 将记录它对数据库执行的生成的 SQL。如果向 HQL 查询对象添加注释,Hibernate 将在日志中 SQL 语句旁边显示注释;这有助于在应用中将 SQL 语句追溯到 HQL。

我们的下一章探索从 Hibernate 过滤数据。

Footnotes 1

这个例子是人为的;为什么不在USERS表上设置一个状态来表明有问题的用户记录是不活动的呢?

  2

Lombok 的工作原理是分析 Java 源代码。它不走等级制度;虽然这非常有用,但是实现起来有一些真正的技术挑战。虽然有一天 Lombok 可能真的能够像处理简单对象一样方便地处理对象层次结构,但是在撰写本文时,它还不能很好地处理对象层次结构,所以我们不打算在本章中使用它。

  3

有几种方法可以避免上TestBase课。我们可以为每个测试创建一个新的SessionFactory或者使用 dbUnit ( https://dbunit.sourceforge.net ),但是这看起来更直接。

  4

如果您使用的是 JPA 配置,那么属性名就是“hibernate.show_sql

  5

好吧,debug或者“你的日志库可能使用的任何等效物”,尽管看到如此大相径庭的东西以至于debug不可行会令人惊讶。

  6

用关系术语来说,SQL 投影基本上是一组行中的列的子集:如果您有一个由地址、城市和州组成的表,那么地址、城市和州就构成了列的列表。例如,该表的“投影”可能只包含该表中的城市。

  7

如果你想一睹为快,请参见 https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#hql 。JPA 提供了很多表达式,Hibernate 自己也添加了一些。

  8

https://xkcd.com/327/。不客气

  9

当然,可以自己对查询进行分类;您所要做的就是用语法验证查询,以允许您想要的表达式。在现实世界中,我们大多数人都无法证明这需要的时间和努力是值得的。别这么做。使用 Hibernate 为您提供的工具。

  10

我们将在后面的章节中再次讨论这个观点。

  11

我们的测试代码使用批量删除来清除数据,包括前面提到的executeUpdate()的使用。

 

*