Cloud Spanner 与 Hibernate ORM

1. 概览

Hibernate 已成为 Java 项目事实上的标准 ORM 解决方案。它支持所有主要的关系型数据库,还支持更强大的 ORM 工具,例如 Spring Data JPA。此外,还有许多与 Hibernate 兼容的框架,例如 Spring Boot、Microprofile 和 Quarkus。

借助 Cloud Spanner Dialect for Hibernate ORM,您可以将 Hibernate 与 Cloud Spanner 搭配使用。您可以获享 Cloud Spanner 的优势(可伸缩性和关系语义),以及 Hibernate 的惯用持久性。这可以帮助您将现有应用迁移到云端,或者利用基于 Hibernate 的技术提供的提高开发者工作效率来编写新应用。

学习内容

  • 如何编写连接到 Cloud Spanner 的简单 Hibernate 应用
  • 如何创建 Cloud Spanner 数据库
  • 如何使用适用于 Hibernate ORM 的 Cloud Spanner Dialect
  • 如何使用 Hibernate 实现 create-read-update-delete (CRUD) 操作

所需条件

  • 一个 Google Cloud Platform 项目
  • 一个浏览器,例如 ChromeFirefox

2. 设置和要求

自定进度的环境设置

  1. 登录 Cloud 控制台,然后创建一个新项目或重复使用现有项目。 (如果您还没有 Gmail 或 G Suite 账号,则必须创建一个。)

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxd3P0kuQRxdt

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一的名称(上述名称已被占用,您无法使用,抱歉!)。它稍后将在此 Codelab 中被称为 PROJECT_ID

  1. 接下来,您需要在 Cloud 控制台中启用结算功能,才能使用 Google Cloud 资源。

运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。请务必按照“清理”部分部分,其中会指导您如何关停资源,以免产生超出本教程范围的结算费用。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

激活 Cloud Shell

  1. 在 Cloud Console 中,点击激活 Cloud ShellR47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZczXP2tzqZ_mj8e

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

如果您以前从未启动过 Cloud Shell,您将看到一个中间屏幕(非首屏),上面描述了它是什么。如果是这种情况,请点击继续(您将永远不会再看到它)。一次性屏幕如下所示:

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

预配和连接到 Cloud Shell 只需花几分钟时间。

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器或 Google Chromebook 即可完成本 Codelab 中的大部分(甚至全部)工作。

在连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID:

  1. 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list

命令输出

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
gcloud config list project

命令输出

[core]
project = <PROJECT_ID>

如果不是上述结果,您可以使用以下命令进行设置:

gcloud config set project <PROJECT_ID>

命令输出

Updated property [core/project].

3. 创建数据库

Cloud Shell 启动后,您可以开始使用 gcloud 与 GCP 项目进行交互。

首先,启用 Cloud Spanner API。

gcloud services enable spanner.googleapis.com

现在,我们来创建一个名为 codelab-instance 的 Cloud Spanner 实例。

gcloud spanner instances create codelab-instance \
 --config=regional-us-central1 \
 --description="Codelab Instance" --nodes=1

现在,我们需要向此实例添加一个数据库。将其命名为 codelab-db

gcloud spanner databases create codelab-db --instance=codelab-instance

4. 创建空应用

我们将使用 Maven 快速入门原型创建一个简单的 Java 控制台应用。

mvn archetype:generate \
 -DgroupId=codelab \
 -DartifactId=spanner-hibernate-codelab \
 -DarchetypeArtifactId=maven-archetype-quickstart \
 -DarchetypeVersion=1.4 \
 -DinteractiveMode=false

切换到应用目录。

cd spanner-hibernate-codelab

使用 Maven 编译并运行应用。

mvn compile exec:java -Dexec.mainClass=codelab.App

您应该会在控制台中看到 Hello World!

5. 添加依赖项

我们打开 Cloud Shell 编辑器并浏览 spanner-hibernate-codelab 目录,以探索源代码。

b5cb37d043d4d2b0.png

到目前为止,我们只有一个可以输出 "Hello World!" 的基本 Java 控制台应用。但是,我们真的想要编写一个使用 Hibernate 与 Cloud Spanner 通信的 Java 应用。为此,我们需要 Cloud Spanner Dialect for Hibernate、Cloud Spanner JDBC 驱动程序和 Hibernate 核心。因此,我们将以下依赖项添加到 pom.xml 文件内的 <dependencies> 代码块中。

pom.xml

    <!-- Spanner Dialect -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-hibernate-dialect</artifactId>
      <version>1.5.0</version>
    </dependency>

    <!-- JDBC Driver -->
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-spanner-jdbc</artifactId>
      <version>2.0.0</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>5.4.29.Final</version>
    </dependency>

6. 配置 Hibernate ORM

接下来,我们将创建 Hibernate 配置文件 hibernate.cfg.xmlhibernate.properties。运行以下命令创建空文件,然后使用 Cloud Shell Editor 进行修改。

mkdir src/main/resources \
 && touch src/main/resources/hibernate.cfg.xml \
 && touch src/main/resources/hibernate.properties

因此,让我们通过填写 hibernate.cfg.xml 来告知 Hibernate 将要映射到数据库的带有注解的实体类。(我们稍后将创建实体类。)

src/main/resources/hibernate.cfg.xml

<hibernate-configuration>
  <session-factory>
    <!-- Annotated entity classes -->
    <mapping class="codelab.Album"/>
    <mapping class="codelab.Singer"/>
  </session-factory>
</hibernate-configuration>

Hibernate 还需要知道如何连接到 Cloud Spanner 实例以及要使用的方言。因此,我们要指示它使用 SpannerDialect for SQL 语法、Spanner JDBC 驱动程序,以及包含数据库坐标的 JDBC 连接字符串。这些信息会写入 hibernate.properties 文件中。

src/main/resources/hibernate.properties

hibernate.dialect=com.google.cloud.spanner.hibernate.SpannerDialect
hibernate.connection.driver_class=com.google.cloud.spanner.jdbc.JdbcDriver
hibernate.connection.url=jdbc:cloudspanner:/projects/{PROJECT_ID}/instances/codelab-instance/databases/codelab-db
# auto-create or update DB schema
hibernate.hbm2ddl.auto=update
hibernate.show_sql=true

请记得将 {PROJECT_ID} 替换为您的项目 ID(可通过运行以下命令获取):

gcloud config get-value project

由于我们还没有数据库架构,因此添加了 hibernate.hbm2ddl.auto=update 属性,以便 Hibernate 在首次运行应用时在 Cloud Spanner 中创建这两个表。

通常,您还需要确保使用 GOOGLE_APPLICATION_CREDENTIALS 环境变量中的服务账号 JSON 文件或使用 gcloud auth application-default login 命令配置的应用默认凭据设置身份验证凭据。不过,由于我们是在 Cloud Shell 中运行,因此已创建默认项目凭据。

7. 创建带注解的实体类

现在,我们可以编写一些代码了。

我们将定义两个普通的旧 Java 对象 (POJO),它们将映射到 Cloud Spanner 中的表:SingerAlbumAlbum 将与 Singer 建立 @ManyToOne 关系。我们还可以使用 @OneToMany 注解将 Singer 映射到其 Album 列表,但在本例中,我们并不是每次需要从数据库中提取歌手时都加载所有专辑。

添加 SingerAlbum 实体类。

创建类文件。

touch src/main/java/codelab/Singer.java \
&& touch src/main/java/codelab/Album.java

粘贴文件的内容。

src/main/java/codelab/Singer.java

package codelab;

import java.util.Date;
import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.Type;

@Entity
public class Singer {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  private UUID singerId;

  private String firstName;

  private String lastName;

  @Temporal(TemporalType.DATE)
  private Date birthDate;

  public Singer() {
  }

  public Singer(String firstName, String lastName, Date birthDate) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.birthDate = birthDate;
  }

  public UUID getSingerId() {
    return singerId;
  }

  public void setSingerId(UUID singerId) {
    this.singerId = singerId;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }

  public Date getBirthDate() {
    return birthDate;
  }

  public void setBirthDate(Date birthDate) {
    this.birthDate = birthDate;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Singer)) {
      return false;
    }

    Singer singer = (Singer) o;

    if (!firstName.equals(singer.firstName)) {
      return false;
    }
    if (!lastName.equals(singer.lastName)) {
      return false;
    }
    return birthDate.equals(singer.birthDate);
  }

  @Override
  public int hashCode() {
    int result = firstName.hashCode();
    result = 31 * result + lastName.hashCode();
    result = 31 * result + birthDate.hashCode();
    return result;
  }
}

src/main/java/codelab/Album.java

package codelab;

import java.util.UUID;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.annotations.Type;

@Entity
public class Album {

  @Id
  @GeneratedValue
  @Type(type = "uuid-char")
  UUID albumId;

  @ManyToOne
  Singer singer;

  String albumTitle;

  public Album() {
  }

  public Album(Singer singer, String albumTitle) {
    this.singer = singer;
    this.albumTitle = albumTitle;
  }

  public UUID getAlbumId() {
    return albumId;
  }

  public void setAlbumId(UUID albumId) {
    this.albumId = albumId;
  }

  public Singer getSinger() {
    return singer;
  }

  public void setSinger(Singer singer) {
    this.singer = singer;
  }

  public String getAlbumTitle() {
    return albumTitle;
  }

  public void setAlbumTitle(String albumTitle) {
    this.albumTitle = albumTitle;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (!(o instanceof Album)) {
      return false;
    }

    Album album = (Album) o;

    if (!singer.equals(album.singer)) {
      return false;
    }
    return albumTitle.equals(album.albumTitle);
  }

  @Override
  public int hashCode() {
    int result = singer.hashCode();
    result = 31 * result + albumTitle.hashCode();
    return result;
  }
}

请注意,在此示例中,我们将自动生成的 UUID 作为主键。这是 Cloud Spanner 中的首选 ID 类型,因为系统会按照键范围在服务器之间划分数据,从而避免热点。单调递增的整数键也可以用,但性能可能较低。

8. 保存和查询实体

定义所有内容并定义实体对象后,我们就可以向数据库写入数据并进行查询了。我们将打开一个 Hibernate Session,然后使用它先删除 clearData() 方法中的所有表行,在 writeData() 方法中保存一些实体,并在 readData() 方法中使用 Hibernate 查询语言 (HQL) 运行一些查询。

使用以下内容替换 App.java 的内容:

src/main/java/codelab/App.java

package codelab;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class App {

  public final static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  public static void main(String[] args) {
    // create a Hibernate sessionFactory and session
    StandardServiceRegistry registry = new StandardServiceRegistryBuilder().configure().build();
    SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata()
        .buildSessionFactory();
    Session session = sessionFactory.openSession();

    clearData(session);

    writeData(session);

    readData(session);

    // close Hibernate session and sessionFactory
    session.close();
    sessionFactory.close();
  }

  private static void clearData(Session session) {
    session.beginTransaction();

    session.createQuery("delete from Album where 1=1").executeUpdate();
    session.createQuery("delete from Singer where 1=1").executeUpdate();

    session.getTransaction().commit();
  }

  private static void writeData(Session session) {
    session.beginTransaction();

    Singer singerMelissa = new Singer("Melissa", "Garcia", makeDate("1981-03-19"));
    Album albumGoGoGo = new Album(singerMelissa, "Go, Go, Go");
    session.save(singerMelissa);
    session.save(albumGoGoGo);

    session.save(new Singer("Russell", "Morales", makeDate("1978-12-02")));
    session.save(new Singer("Jacqueline", "Long", makeDate("1990-07-29")));
    session.save(new Singer("Dylan", "Shaw", makeDate("1998-05-02")));

    session.getTransaction().commit();
  }

  private static void readData(Session session) {
    List<Singer> singers = session.createQuery("from Singer where birthDate >= '1990-01-01' order by lastName")
        .list();
    List<Album> albums = session.createQuery("from Album").list();

    System.out.println("Singers who were born in 1990 or later:");
    for (Singer singer : singers) {
      System.out.println(singer.getFirstName() + " " + singer.getLastName() + " born on "
          + DATE_FORMAT.format(singer.getBirthDate()));
    }

    System.out.println("Albums: ");
    for (Album album : albums) {
      System.out
          .println("\"" + album.getAlbumTitle() + "\" by " + album.getSinger().getFirstName() + " "
              + album.getSinger().getLastName());
    }
  }

  private static Date makeDate(String dateString) {
    try {
      return DATE_FORMAT.parse(dateString);
    } catch (ParseException e) {
      e.printStackTrace();
      return null;
    }
  }
}

现在,我们来编译并运行代码。我们将添加 -Dexec.cleanupDaemonThreads=false 选项,以抑制 Maven 尝试执行的有关守护程序线程清理的警告。

mvn compile exec:java -Dexec.mainClass=codelab.App -Dexec.cleanupDaemonThreads=false

在输出中,您应该会看到如下内容:

Singers who were born in 1990 or later:
Jacqueline Long born on 1990-07-29
Dylan Shaw born on 1998-05-02
Albums: 
"Go, Go, Go" by Melissa Garcia

此时,如果您转到 Cloud Spanner 控制台并查看数据库中 Singer 和 Album 表的数据,您会看到如下内容:

f18276ea54cc266f.png

952d9450dd659e75

9. 清理

让我们删除一开始创建的 Cloud Spanner 实例,以确保它不会不必要地耗尽资源。

gcloud spanner instances delete codelab-instance

10. 恭喜

恭喜!您已成功构建一个 Java 应用,该应用使用 Hibernate 在 Cloud Spanner 中保留数据。

  • 您创建了一个 Cloud Spanner 实例和一个数据库
  • 您已将应用配置为使用 Hibernate
  • 您创建了两个实体:“音乐人”和“专辑”
  • 您已为应用自动生成数据库架构
  • 您已成功将实体保存到 Cloud Spanner 并进行查询

现在,您已了解使用 Cloud Spanner 编写 Hibernate 应用所需的关键步骤。

后续操作