搭配使用 Cloud Spanner 和 Hibernate ORM

1. 總覽

Hibernate 已成為 Java 專案的實際標準 ORM 解決方案。可支援所有主要關聯資料庫,並支援 Spring Data JPA 等更強大的 ORM 工具。此外,還有其他許多與 Hibernate 相容的架構,例如 Spring Boot、Microprofile 和 Quarkus。

您可以透過 Cloud Spanner 的 Hibernate ORM 方言,將 Hibernate 與 Cloud Spanner 搭配使用。您可以享有 Cloud Spanner 的擴充性和關聯語意,這是 Hibernate 的慣用持續性。如此一來,您就能將現有應用程式遷移至雲端,或藉由 Hibernate 式技術提高開發人員的工作效率,進而編寫新的應用程式。

課程內容

  • 如何編寫連結至 Cloud Spanner 的簡易 Hibernate 應用程式
  • 如何建立 Cloud Spanner 資料庫
  • 如何將 Cloud Spanner 方言用於 Hibernate ORM
  • 如何使用 Hibernate 實作 create-read-update-delete (CRUD) 作業

軟硬體需求

  • Google Cloud Platform 專案
  • 瀏覽器,例如 ChromeFirefox

2. 設定和需求

自修環境設定

  1. 登入 Cloud 控制台建立新專案,或是重複使用現有專案。(如果您還沒有 Gmail 或 G Suite 帳戶,請先建立帳戶)。

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQCX2t3CH0kuQR4Ud4Ud4Ud4Ud4

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

提醒您,專案 ID 是所有 Google Cloud 專案的專屬名稱 (已經有人使用上述名稱,很抱歉對您不符!)。稍後在本程式碼研究室中會稱為 PROJECT_ID

  1. 接下來,您需要在 Cloud 控制台中啟用計費功能,才能使用 Google Cloud 資源。

執行這個程式碼研究室並不會產生任何費用,如果有的話。請務必依照「清除所用資源」一節指示本節將說明如何關閉資源,這樣您就不會產生本教學課程結束後產生的費用。Google Cloud 的新使用者符合 $300 美元免費試用計畫的資格。

啟用 Cloud Shell

  1. 在 Cloud 控制台中,按一下「啟用 Cloud Shell」圖示 R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqPwMTZw8O8qPw87O8epRMTZO8epRMTZO8epR7O8epU0pRMTZO8eM6

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

如果您從未啟動 Cloud Shell,您會看見中繼畫面 (需捲動位置),說明螢幕內容。如果出現這種情況,請按一下「繼續」 (之後不會再顯示)。以下是單次畫面的外觀:

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

佈建並連線至 Cloud Shell 只需幾分鐘的時間。

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

這部虛擬機器都裝載了您需要的所有開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提高網路效能和驗證能力。在本程式碼研究室中,您的工作幾乎都可以透過瀏覽器或 Chromebook 完成。

連線至 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 的 Java 應用程式,與 Cloud Spanner 通訊。為了做到這一點,我們需要 Hibernate 的 Cloud Spanner 方言、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 編輯器進行編輯。

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 的 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 嘗試執行的 Daemon 執行緒清理的警告。

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.png

9. 清除所用資源

讓我們刪除一開始建立的 Cloud Spanner 執行個體,確保執行個體不會佔用任何不必要的資源。

gcloud spanner instances delete codelab-instance

10. 恭喜

恭喜!您已成功建構 Java 應用程式,這個應用程式會使用 Hibernate 在 Cloud Spanner 中保留資料。

  • 您已建立 Cloud Spanner 執行個體和資料庫
  • 您已將應用程式設為使用 Hibernate
  • 你建立了兩個實體:藝人和專輯
  • 系統會自動為應用程式產生資料庫結構定義
  • 您已成功將實體儲存至 Cloud Spanner 並進行查詢

您現已瞭解透過 Cloud Spanner 編寫 Hibernate 應用程式所需的重要步驟。

後續步驟