Cloud Spanner と Hibernate ORM

1. 概要

Hibernate は、Java プロジェクトの事実上の標準 ORM ソリューションになっています。すべての主要なリレーショナル データベースをサポートし、Spring Data JPA などの強力な ORM ツールをさらに活用できます。また、Spring Boot、Microprofile、Quarkus など、Hibernate と互換性のあるフレームワークも多数あります。

Hibernate ORM 用の Cloud Spanner 言語を使用すると、Cloud Spanner で Hibernate を使用できます。Cloud Spanner の利点(スケーラビリティとリレーショナル セマンティクス)と Hibernate の慣用的な永続性を両立できます。これにより、既存のアプリケーションをクラウドに移行したり、Hibernate ベースのテクノロジーによって向上したデベロッパーの生産性を活用して新しいアプリケーションを作成したりできます。

学習内容

  • Cloud Spanner に接続する単純な Hibernate アプリケーションを作成する方法
  • Cloud Spanner データベースの作成方法
  • Hibernate ORM 用の Cloud Spanner 言語の使用方法
  • Hibernate で作成、読み取り、更新、削除(CRUD)オペレーションを実装する方法

必要なもの

  • Google Cloud Platform プロジェクト
  • ChromeFirefox などのブラウザ

2. 設定と要件

セルフペース型の環境設定

  1. Cloud Console にログインし、新しいプロジェクトを作成するか、既存のプロジェクトを再利用します(Gmail アカウントまたは G Suite アカウントをお持ちでない場合は、アカウントを作成する必要があります)。

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

プロジェクト ID を忘れないようにしてください。プロジェクト ID はすべての Google Cloud プロジェクトを通じて一意の名前にする必要があります(上記の名前はすでに使用されているので使用できません)。以降、このコードラボでは PROJECT_ID と呼びます。

  1. 次に、Google Cloud リソースを使用するために、Cloud Console で課金を有効にする必要があります。

このコードラボを実行しても、費用はほとんどかからないはずです。このチュートリアル以外で請求が発生しないように、リソースのシャットダウン方法を説明する「クリーンアップ」セクションの手順に従うようにしてください。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell をアクティブにする

  1. Cloud Console で、[Cloud Shell をアクティブにする] R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA をクリックします。

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 Quickstart Archetype を使用して、シンプルな 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 アプリケーションを作成したいと考えています。そのためには、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 インスタンスへの接続方法と使用する言語も把握する必要があります。そのため、SQL 構文に SpannerDialect を使用し、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 に 2 つのテーブルを作成できるようにしました。

通常、認証情報が設定されていることも確認します。これには、GOOGLE_APPLICATION_CREDENTIALS 環境変数のサービス アカウント JSON ファイルを使用するか、gcloud auth application-default login コマンドを使用して構成されたアプリケーションのデフォルト認証情報を使用します。ただし、Cloud Shell で実行しているため、デフォルトのプロジェクト認証情報はすでに設定されています。

7. アノテーション付きエンティティ クラスを作成する

これで、コードを記述する準備が整いました。

Cloud Spanner のテーブルにマッピングされる 2 つの POJO(Plain Old Java Object)SingerAlbum を定義します。AlbumSinger@ManyToOne の関係になります。@OneToMany アノテーションを使用して SingerAlbum のリストにマッピングすることもできますが、この例では、データベースから歌手を取得するたびにすべてのアルバムを読み込むことは望ましくありません。

Singer エンティティ クラスと Album エンティティ クラスを追加します。

クラスファイルを作成します。

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;
    }
  }
}

それでは、コードをコンパイルして実行しましょう。Maven が実行しようとするデーモン スレッドのクリーンアップに関する警告を抑制するために、-Dexec.cleanupDaemonThreads=false オプションを追加します。

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. 完了

お疲れさまでした。Hibernate を使用して Cloud Spanner にデータを永続化する Java アプリケーションを正常に構築できました。

  • Cloud Spanner のインスタンスとデータベースを作成した
  • Hibernate を使用するようにアプリケーションを構成した
  • Artist と Album の 2 つのエンティティを作成しました
  • アプリケーションのデータベース スキーマを自動的に生成した
  • エンティティを Cloud Spanner に保存してクエリを実行できました

これで、Cloud Spanner で Hibernate アプリケーションを作成するために必要な主な手順を理解しました。

次のステップ