1. 概要
Hibernate は、Java プロジェクトの事実上の標準 ORM ソリューションになりました。主要なリレーショナル データベースすべてをサポートし、Spring Data JPA などのより強力な ORM ツールを可能にします。さらに、Spring Boot、Microprofile、Quarkus などの Hibernate 互換フレームワークも数多くあります。
Hibernate ORM 用の Cloud Spanner Dialect により、Cloud Spanner で Hibernate を使用できるようになります。Hibernate の慣用的な永続性により、スケーラビリティとリレーショナル セマンティクスである Cloud Spanner のメリットを享受できます。これにより、Hibernate ベースのテクノロジーがもたらすデベロッパーの生産性向上を活用して、既存のアプリケーションをクラウドに移行したり、新しいアプリケーションを作成したりできます。
学習内容
- Cloud Spanner に接続するシンプルな Hibernate アプリケーションを作成する方法
 - Cloud Spanner データベースを作成する方法
 - Hibernate ORM に Cloud Spanner 言語を使用する方法
 - Hibernate を使用して create-read-update-delete(CRUD)オペレーションを実装する方法
 
必要なもの
2. 設定と要件
セルフペース型の環境設定
- Cloud Console にログインし、新しいプロジェクトを作成するか、既存のプロジェクトを再利用します(Gmail アカウントまたは G Suite アカウントをお持ちでない場合は、アカウントを作成する必要があります)。
 
プロジェクト ID を忘れないようにしてください。プロジェクト ID はすべての Google Cloud プロジェクトを通じて一意の名前にする必要があります(上記の名前はすでに使用されているので使用できません)。以降、このコードラボでは PROJECT_ID と呼びます。
- 次に、Google Cloud リソースを使用するために、Cloud Console で課金を有効にする必要があります。
 
このコードラボを実行しても、費用はほとんどかからないはずです。このチュートリアル以外で請求が発生しないように、リソースのシャットダウン方法を説明する「クリーンアップ」セクションの手順に従うようにしてください。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。
Cloud Shell をアクティブにする
- Cloud Console で、[Cloud Shell をアクティブにする] 
をクリックします。
 
Cloud Shell を起動したことがない場合、その内容を説明する中間画面が(スクロールしなければ見えない範囲に)が表示されます。その場合は、[続行] をクリックします(以後表示されなくなります)。この中間画面は次のようになります。
Cloud Shell のプロビジョニングと接続に少し時間がかかる程度です。
この仮想マシンには、必要な開発ツールがすべて用意されています。5 GB の永続ホーム ディレクトリが用意されており、Google Cloud で稼働するため、ネットワーク パフォーマンスが充実しており認証もスムーズです。このコードラボでの作業のほとんどは、ブラウザまたは Chromebook から実行できます。
Cloud Shell に接続すると、すでに認証は完了しており、プロジェクトに各自のプロジェクト ID が設定されていることがわかります。
- 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 ディレクトリ内をブラウジングして、ソースコードを詳しく見てみましょう。

ここまでは、"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.xml と hibernate.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 が Cloud Spanner に 2 つのテーブルを作成できるように、hibernate.hbm2ddl.auto=update プロパティを追加しました。
通常は、GOOGLE_APPLICATION_CREDENTIALS 環境変数のサービス アカウント JSON ファイル、または gcloud auth application-default login コマンドを使用して構成されたアプリケーションのデフォルト認証情報を使用して、認証情報を設定します。ただし、Cloud Shell で実行しているので、デフォルトのプロジェクト認証情報がすでに設定されています。
7. アノテーション付きのエンティティ クラスを作成する
それでは、コードを記述していきます。
Cloud Spanner のテーブルにマッピングされる 2 つの Plain Old Java オブジェクト(POJO)を定義します。つまり、Singer と Album です。Album は Singer と @ManyToOne の関係になります。また、@OneToMany アノテーションを使用して Singer を Album のリストにマッピングすることもできますが、この例では、データベースから歌手を取得する必要があるたびにすべてのアルバムを読み込む必要はありません。
エンティティ クラス 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;
    }
  }
}
では、コードをコンパイルして実行します。-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 テーブルのデータを表示すると、次のように表示されます。


9. クリーンアップ
最初に作成した Cloud Spanner インスタンスを削除して、リソースが不必要に使用されないようにしてください。
gcloud spanner instances delete codelab-instance
10.完了
これで、Hibernate を使用して Cloud Spanner でデータを保持する Java アプリケーションを無事構築できました。
- Cloud Spanner インスタンスとデータベースを作成しました。
 - Hibernate を使用するようにアプリケーションを構成した
 - アーティストとアルバムの 2 つのエンティティを作成しました。
 - アプリケーションのデータベース スキーマを自動生成しました
 - エンティティを Cloud Spanner に正常に保存してクエリを実行しました
 
ここでは、Cloud Spanner を使用して Hibernate アプリケーションを記述するために必要な主な手順を学びました。