Cloud Spanner avec Hibernate ORM

1. Présentation

Hibernate est devenu la solution ORM standard de facto pour les projets Java. Cette solution est compatible avec toutes les principales bases de données relationnelles et permet d'utiliser des outils ORM encore plus puissants tels que Spring Data JPA. Il existe également de nombreux frameworks compatibles avec Hibernate, tels que Spring Boot, Microprofile et Quarkus.

Le dialecte Cloud Spanner pour Hibernate ORM permet d'utiliser Hibernate avec Cloud Spanner. Vous bénéficiez des avantages de Cloud Spanner (évolutivité et sémantique relationnelle) avec la persistance idiomatique de Hibernate. Vous pouvez ainsi migrer vos applications existantes vers le cloud ou en créer de nouvelles en profitant d'une productivité accrue des développeurs offerte par les technologies basées sur Hibernate.

Points abordés

  • Écrire une application Hibernate simple qui se connecte à Cloud Spanner
  • Créer une base de données Cloud Spanner
  • Utiliser le dialecte Cloud Spanner pour Hibernate ORM
  • Comment implémenter des opérations CRUD (create-read-update-delete) avec Hibernate

Prérequis

  • Un projet Google Cloud Platform
  • Un navigateur tel que Chrome ou Firefox

2. Préparation

Configuration de l'environnement d'auto-formation

  1. Connectez-vous à Cloud Console, puis créez un projet ou réutilisez un projet existant. (Si vous n'avez pas encore de compte Gmail ou G Suite, vous devez en créer un.)

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxd4D4

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

Mémorisez l'ID du projet. Il s'agit d'un nom unique permettant de différencier chaque projet Google Cloud (le nom ci-dessus est déjà pris ; vous devez en trouver un autre). Il sera désigné par le nom PROJECT_ID tout au long de cet atelier de programmation.

  1. Vous devez ensuite activer la facturation dans Cloud Console pour pouvoir utiliser les ressources Google Cloud.

L'exécution de cet atelier de programmation est très peu coûteuse, voire gratuite. Veillez à suivre les instructions de la section "Nettoyer" qui indique comment désactiver les ressources afin d'éviter les frais une fois ce tutoriel terminé. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai sans frais pour bénéficier d'un crédit de 300 $.

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell R47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

Si vous n'avez encore jamais démarré Cloud Shell, un écran intermédiaire s'affiche en dessous de la ligne de séparation pour décrire de quoi il s'agit. Si tel est le cas, cliquez sur Continuer (cet écran ne s'affiche qu'une seule fois). Voici à quoi il ressemble :

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

Cette machine virtuelle contient tous les outils de développement nécessaires. Elle intègre un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier dans un simple navigateur ou sur votre Chromebook.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié et le projet est défini avec votre ID de projet.

  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list

Résultat de la commande

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

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

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

3. Créer une base de données

Après le lancement de Cloud Shell, vous pouvez commencer à utiliser gcloud pour interagir avec votre projet GCP.

Commencez par activer l'API Cloud Spanner.

gcloud services enable spanner.googleapis.com

Créons maintenant une instance Cloud Spanner appelée codelab-instance.

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

Nous devons maintenant ajouter une base de données à cette instance. Nous l'appellerons codelab-db.

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

4. Créer une application vide

Nous allons utiliser l'archetype de démarrage rapide Maven pour créer une application de console Java simple.

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

Accédez au répertoire de l'application.

cd spanner-hibernate-codelab

Compilez et exécutez l'application à l'aide de Maven.

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

Hello World! doit s'afficher dans la console.

5. Ajouter des dépendances

Découvrons le code source en ouvrant l'éditeur Cloud Shell et en parcourant le répertoire spanner-hibernate-codelab.

b5cb37d043d4d2b0.png

Jusqu'à présent, nous n'avons qu'une application de base de la console Java qui affiche "Hello World!". Cependant, nous voulons vraiment écrire une application Java qui utilise Hibernate pour communiquer avec Cloud Spanner. Pour cela, nous avons besoin du dialecte Cloud Spanner pour Hibernate, du pilote JDBC Cloud Spanner et du cœur Hibernate. Ajoutons donc les dépendances suivantes au bloc <dependencies> dans le fichier 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. Configurer Hibernate ORM

Nous allons ensuite créer les fichiers de configuration Hibernate hibernate.cfg.xml et hibernate.properties. Exécutez la commande suivante pour créer les fichiers vides, puis modifiez-les à l'aide de l'éditeur Cloud Shell.

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

Nous allons donc indiquer à Hibernate les classes d'entités annotées que nous allons mapper à la base de données en renseignant hibernate.cfg.xml. (Nous créerons les classes d'entités ultérieurement.)

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 doit également savoir comment se connecter à l'instance Cloud Spanner et choisir le dialecte à utiliser. Nous allons donc lui demander d'utiliser SpannerDialect pour la syntaxe SQL, le pilote JDBC Spanner et la chaîne de connexion JDBC avec les coordonnées de la base de données. Ce code est placé dans le fichier 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

N'oubliez pas de remplacer {PROJECT_ID} par l'ID de votre projet, que vous pouvez obtenir en exécutant la commande suivante:

gcloud config get-value project

Comme nous ne disposons d'aucun schéma de base de données, nous avons ajouté la propriété hibernate.hbm2ddl.auto=update pour permettre à Hibernate de créer les deux tables dans Cloud Spanner lorsque nous exécutons l'application pour la première fois.

En règle générale, vous devez également vous assurer que les identifiants d'authentification sont configurés à l'aide d'un fichier JSON de compte de service dans la variable d'environnement GOOGLE_APPLICATION_CREDENTIALS ou des identifiants par défaut de l'application configurés à l'aide de la commande gcloud auth application-default login. Toutefois, comme nous s'exécutons dans Cloud Shell, les identifiants de projet par défaut sont déjà configurés.

7. Créer des classes d'entités annotées

Nous sommes maintenant prêts à écrire du code.

Nous allons définir deux anciens objets Java (POJO) standards qui seront mappés à des tables dans Cloud Spanner : Singer et Album. Album aura une relation @ManyToOne avec Singer. Nous aurions également pu mapper des Singer à des listes de leurs Album avec une annotation @OneToMany, mais pour cet exemple, nous ne voulons pas vraiment charger tous les albums chaque fois que nous avons besoin d'extraire un chanteur de la base de données.

Ajoutez les classes d'entités Singer et Album.

Créez les fichiers du cours.

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

Collez le contenu des fichiers.

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

Notez que dans cet exemple, nous utilisons un UUID généré automatiquement pour la clé primaire. Il s'agit d'un type d'ID privilégié dans Cloud Spanner, car il évite les hotspots, car le système divise les données entre les serveurs par plages de clés. Une clé entière augmentant de manière monotone fonctionne également, mais peut être moins performante.

8. Enregistrer et interroger des entités

Maintenant que tout est configuré et que les objets d'entité sont définis, nous pouvons commencer à écrire dans la base de données et à l'interroger. Nous allons ouvrir un Session Hibernate, puis l'utiliser pour supprimer d'abord toutes les lignes de la table dans la méthode clearData(), enregistrer certaines entités dans la méthode writeData() et exécuter des requêtes à l'aide du langage de requête Hibernate (HQL) dans la méthode readData().

Remplacez le contenu de App.java par le code suivant :

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

Maintenant, compilons et exécutons le code. Nous allons ajouter l'option -Dexec.cleanupDaemonThreads=false pour supprimer les avertissements concernant le nettoyage des threads daemon que Maven tentera de réaliser.

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

Le résultat doit ressembler à ceci:

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

À ce stade, si vous accédez à la console Cloud Spanner et affichez les données des tables "Singer" et "Album" dans la base de données, vous obtenez un résultat semblable à celui-ci:

f18276ea54cc266f.png

952d9450dd659e75.png

9. Effectuer un nettoyage

Supprimons l'instance Cloud Spanner que nous avons créée au début pour nous assurer qu'elle n'utilise pas inutilement de ressources.

gcloud spanner instances delete codelab-instance

10. Félicitations

Félicitations ! Vous venez de créer une application Java qui utilise Hibernate pour conserver des données dans Cloud Spanner.

  • Vous avez créé une instance et une base de données Cloud Spanner
  • Vous avez configuré l'application pour utiliser Hibernate.
  • Vous avez créé deux entités : "Artiste" et "Album".
  • Vous avez automatiquement généré le schéma de base de données de votre application
  • Vous avez enregistré des entités dans Cloud Spanner et vous les avez interrogées

Vous connaissez maintenant les principales étapes à suivre pour écrire une application Hibernate avec Cloud Spanner.

Et ensuite ?