Cloud Spanner с Hibernate ORM

1. Обзор

Hibernate стал де-факто стандартом ORM для Java-проектов. Он поддерживает все основные реляционные базы данных и позволяет использовать ещё более мощные ORM-инструменты, такие как Spring Data JPA . Кроме того, существует множество совместимых с Hibernate фреймворков, таких как Spring Boot, Microprofile и Quarkus.

Диалект Cloud Spanner для ORM Hibernate позволяет использовать Hibernate с Cloud Spanner. Вы получаете преимущества Cloud Spanner — масштабируемость и реляционную семантику — с идиоматической персистентностью Hibernate. Это может помочь вам мигрировать существующие приложения в облако или писать новые, используя преимущества повышения производительности разработчиков, обеспечиваемые технологиями на основе Hibernate.

Что вы узнаете

  • Как написать простое приложение Hibernate, которое подключается к Cloud Spanner
  • Как создать базу данных Cloud Spanner
  • Как использовать диалект Cloud Spanner для ORM Hibernate
  • Как реализовать операции создания, чтения, обновления и удаления (CRUD) с помощью Hibernate.

Что вам понадобится

  • Проект Google Cloud Platform
  • Браузер, например Chrome или Firefox.

2. Настройка и требования

Настройка среды для самостоятельного обучения

  1. Войдите в Cloud Console и создайте новый проект или используйте существующий. (Если у вас еще нет учетной записи Gmail или G Suite, вам необходимо ее создать .)

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

Запомните идентификатор проекта (Project ID) — уникальное имя для всех проектов Google Cloud (указанное выше имя уже занято и вам не подойдёт, извините!). В дальнейшем в этом практическом занятии оно будет обозначаться как PROJECT_ID .

  1. Далее вам потребуется включить оплату в Cloud Console, чтобы использовать ресурсы Google Cloud.

Выполнение этого практического задания не должно стоить дорого, если вообще что-либо. Обязательно следуйте инструкциям в разделе «Очистка», где указано, как отключить ресурсы, чтобы избежать дополнительных расходов после завершения этого урока. Новые пользователи Google Cloud имеют право на бесплатную пробную версию стоимостью 300 долларов США .

Активировать Cloud Shell

  1. В консоли Cloud нажмите «Активировать 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 ГБ и работает в облаке Google, что значительно повышает производительность сети и аутентификацию. Большая часть, если не вся, работа в этом практическом задании может быть выполнена с помощью обычного браузера или вашего Chromebook.

После подключения к Cloud Shell вы увидите, что ваша аутентификация пройдена и что проект уже настроен на ваш идентификатор проекта.

  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.

Сначала активируйте API Cloud Spanner.

gcloud services enable spanner.googleapis.com

Теперь давайте создадим экземпляр Cloud Spanner под названием codelab-instance .

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

Пока у нас есть только базовое консольное Java-приложение, которое выводит "Hello World!" . Однако нам действительно нужно написать Java-приложение, которое будет использовать Hibernate для взаимодействия с Cloud Spanner. Для этого нам понадобится диалект Cloud Spanner для Hibernate, драйвер Cloud Spanner JDBC и ядро ​​Hibernate. Итак, давайте добавим следующие зависимости в блок <dependencies> внутри файла pom.xml .

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 об аннотированных классах сущностей, которые мы будем сопоставлять с базой данных, заполнив файл hibernate.cfg.xml . (Классы сущностей мы создадим позже.)

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} на идентификатор вашего проекта, который можно получить, выполнив следующую команду:

gcloud config get-value project

Поскольку у нас нет существующей схемы базы данных, мы добавили свойство hibernate.hbm2ddl.auto=update , чтобы Hibernate мог создать две таблицы в Cloud Spanner при первом запуске приложения.

Обычно также необходимо убедиться, что учетные данные для аутентификации настроены, используя либо JSON-файл учетной записи службы в переменной среды GOOGLE_APPLICATION_CREDENTIALS , либо учетные данные приложения по умолчанию, настроенные с помощью команды gcloud auth application-default login . Однако, поскольку мы работаем в Cloud Shell, учетные данные проекта по умолчанию уже настроены.

7. Создайте аннотированные классы сущностей.

Теперь мы готовы написать код.

Мы определим два простых Java-объекта (POJO), которые будут сопоставляться с таблицами в Cloud Spanner — Singer и Album . У Album будет отношение @ManyToOne с Singer . Мы могли бы также сопоставить объекты Singer со списками их Album с помощью аннотации @OneToMany , но для этого примера нам не нужно загружать все альбомы каждый раз, когда нам нужно получить информацию о певце из базы данных.

Добавьте классы сущностей 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, поскольку он позволяет избежать «горячих точек», так как система распределяет данные между серверами по диапазонам ключей. Монотонно возрастающий целочисленный ключ также подошел бы, но может быть менее производительным.

8. Сохранение и запрос сущностей.

После того, как все настроено и объекты сущностей определены, мы можем начать запись в базу данных и выполнение запросов. Мы откроем Session Hibernate, а затем используем ее для того, чтобы сначала удалить все строки таблицы в методе clearData() , сохранить некоторые сущности в методе writeData() и выполнить несколько запросов с использованием языка запросов Hibernate (HQL) в методе readData() .

Замените содержимое файла 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.png

9. Уборка

Давайте удалим созданный нами в начале экземпляр Cloud Spanner, чтобы убедиться, что он не потребляет ресурсы без необходимости.

gcloud spanner instances delete codelab-instance

10. Поздравляем!

Поздравляем, вы успешно создали Java-приложение, использующее Hibernate для сохранения данных в Cloud Spanner.

  • Вы создали экземпляр Cloud Spanner и базу данных.
  • Вы настроили приложение для использования Hibernate.
  • Вы создали две сущности: Исполнитель и Альбом.
  • Вы автоматически сгенерировали схему базы данных для вашего приложения.
  • Вы успешно сохранили объекты в Cloud Spanner и выполнили к ним запрос.

Теперь вы знаете основные шаги, необходимые для написания приложения Hibernate с использованием Cloud Spanner.

Что дальше?