Cloud Spanner con Hibernate ORM

1. Descripción general

Hibernate se convirtió en la solución ORM estándar de facto para proyectos de Java. Es compatible con todas las bases de datos relacionales principales y permite herramientas de ORM aún más potentes, como Spring Data JPA. Además, hay muchos frameworks compatibles con Hibernate, como Spring Boot, Microprofile y Quarkus.

El dialecto de Cloud Spanner para el ORM de Hibernate permite usar Hibernate con Cloud Spanner. Obtienes los beneficios de Cloud Spanner: escalabilidad y semántica relacional, con la persistencia idiomática de Hibernate. Esto puede ayudarte a migrar aplicaciones existentes a la nube o escribir otras nuevas aprovechando la productividad de los desarrolladores que ofrecen las tecnologías basadas en Hibernate.

Qué aprenderás

  • Cómo escribir una aplicación de Hibernate simple que se conecte a Cloud Spanner
  • Cómo crear una base de datos de Cloud Spanner
  • Cómo usar el dialecto de Cloud Spanner para el ORM de Hibernate
  • Cómo implementar operaciones create-read-update-delete (CRUD) con Hibernate

Requisitos

  • Un proyecto de Google Cloud Platform
  • Un navegador como Chrome o Firefox

2. Configuración y requisitos

Configuración del entorno de autoaprendizaje

  1. Accede a la consola de Cloud y crea un proyecto nuevo o reutiliza uno existente. (Si todavía no tienes una cuenta de Gmail o de G Suite, debes crear una).

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8ftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CHt5kuQRxd4DDU

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

Recuerde el ID de proyecto, un nombre único en todos los proyectos de Google Cloud (el nombre anterior ya se encuentra en uso y no lo podrá usar). Se mencionará más adelante en este codelab como PROJECT_ID.

  1. A continuación, deberás habilitar la facturación en la consola de Cloud para usar los recursos de Google Cloud recursos.

Ejecutar este codelab no debería costar mucho, tal vez nada. Asegúrate de seguir las instrucciones de la sección “Realiza una limpieza” en la que se aconseja cómo cerrar recursos para no incurrir en facturación más allá de este instructivo. Los usuarios nuevos de Google Cloud son aptos para participar en el programa Prueba gratuita de $300.

Activar Cloud Shell

  1. En la consola de Cloud, haz clic en Activar Cloud ShellR47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iIWA1OdZczXP2pSwqZ_mCULQrQ.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

Si nunca ha iniciado Cloud Shell, aparecerá una pantalla intermedia (debajo de la mitad inferior de la página) que describe qué es. Si ese es el caso, haz clic en Continuar (y no volverás a verlo). Así es como se ve la pantalla única:

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

El aprovisionamiento y la conexión a Cloud Shell solo tomará unos minutos.

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

Esta máquina virtual está cargada con todas las herramientas de desarrollo que necesitarás. Ofrece un directorio principal persistente de 5 GB y se ejecuta en Google Cloud, lo que permite mejorar considerablemente el rendimiento de la red y la autenticación. Gran parte de tu trabajo en este codelab, si no todo, se puede hacer simplemente con un navegador o tu Chromebook.

Una vez conectado a Cloud Shell, debería ver que ya se autenticó y que el proyecto ya se configuró con tu ID del proyecto.

  1. En Cloud Shell, ejecuta el siguiente comando para confirmar que está autenticado:
gcloud auth list

Resultado del comando

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

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

Resultado del comando

[core]
project = <PROJECT_ID>

De lo contrario, puedes configurarlo con el siguiente comando:

gcloud config set project <PROJECT_ID>

Resultado del comando

Updated property [core/project].

3. Crea una base de datos

Después de que se inicie Cloud Shell, puedes comenzar a usar gcloud para interactuar con tu proyecto de GCP.

Primero, habilita la API de Cloud Spanner.

gcloud services enable spanner.googleapis.com

Ahora, creemos una instancia de Cloud Spanner llamada codelab-instance.

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

Ahora, debemos agregar una base de datos a esta instancia. La llamaremos codelab-db.

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

4. Crea una app vacía

Usaremos el arquetipo de la guía de inicio rápido de Maven para crear una aplicación de consola de Java simple.

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

Cambia al directorio de la app.

cd spanner-hibernate-codelab

Compila y ejecuta la app con Maven.

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

Deberías ver Hello World! impreso en la consola.

5. Cómo agregar dependencias

Exploremos el código fuente abriendo el editor de Cloud Shell y navegando dentro del directorio spanner-hibernate-codelab.

b5cb37d043d4d2b0.png

Hasta ahora, tenemos una app básica de la consola de Java que imprime "Hello World!". Sin embargo, queremos escribir una aplicación de Java que use Hibernate para comunicarse con Cloud Spanner. Para ello, necesitaremos Cloud Spanner Dialect for Hibernate, el controlador JDBC de Cloud Spanner y el núcleo de Hibernate. Por lo tanto, agreguemos las siguientes dependencias al bloque <dependencies> dentro del archivo 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. Configura Hibernate ORM

A continuación, crearemos los archivos de configuración de Hibernate hibernate.cfg.xml y hibernate.properties. Ejecuta el siguiente comando para crear los archivos vacíos y, luego, edítalos con el editor de Cloud Shell.

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

Por lo tanto, le informaremos a Hibernate sobre las clases de entidades anotadas que asignaremos a la base de datos completando hibernate.cfg.xml. Crearemos las clases de entidad más adelante.

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 también necesita saber cómo conectarse a la instancia de Cloud Spanner y qué dialecto usar. Por lo tanto, le indicaremos que use SpannerDialect para la sintaxis de SQL, el controlador de JDBC de Spanner y la string de conexión de JDBC con las coordenadas de la base de datos. Esto se incluye en el archivo 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

Recuerda reemplazar {PROJECT_ID} por el ID del proyecto, que puedes obtener mediante la ejecución del siguiente comando:

gcloud config get-value project

Como no tenemos un esquema de base de datos existente, agregamos la propiedad hibernate.hbm2ddl.auto=update para permitir que Hibernate cree las dos tablas en Cloud Spanner cuando ejecutamos la app por primera vez.

Por lo general, también debes asegurarte de que las credenciales de autenticación estén configuradas mediante un archivo JSON de cuenta de servicio en la variable de entorno GOOGLE_APPLICATION_CREDENTIALS o las credenciales predeterminadas de la aplicación configuradas con el comando gcloud auth application-default login. Sin embargo, dado que lo estamos ejecutando en Cloud Shell, las credenciales predeterminadas del proyecto ya están configuradas.

7. Crea clases de entidades anotadas

Ahora estamos listos para escribir algo de código.

Definiremos dos objetos antiguos sin formato de Java (POJO) que se asignarán a tablas en Cloud Spanner: Singer y Album. Album tendrá una relación @ManyToOne con Singer. También podríamos haber asignado Singer a listas de sus Album con una anotación @OneToMany, pero, en este ejemplo, no queremos cargar todos los álbumes cada vez que necesitamos recuperar un cantante de la base de datos.

Agrega las clases de entidad Singer y Album.

Crea los archivos de la clase.

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

Pega el contenido de los archivos.

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

Ten en cuenta que, en este ejemplo, usaremos un UUID generado automáticamente para la clave primaria. Este es un tipo de ID preferido en Cloud Spanner porque evita los hotspots ya que el sistema divide los datos entre los servidores por rangos de claves. Una clave de número entero que aumenta de forma monótona también funcionaría, pero puede ser menos eficaz.

8. Guarda y consulta entidades

Con todo configurado y los objetos de entidad definidos, podemos comenzar a escribir en la base de datos y consultarla. Abriremos un Session de Hibernate y lo usaremos para borrar primero todas las filas de la tabla en el método clearData(), guardar algunas entidades en el método writeData() y ejecutar algunas consultas con el lenguaje de consulta Hibernate (HQL) en el método readData().

Reemplaza el contenido de App.java por lo siguiente:

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

Ahora, compilemos y ejecutemos el código. Agregaremos la opción -Dexec.cleanupDaemonThreads=false para suprimir las advertencias sobre la limpieza de los subprocesos del daemon que intentará hacer Maven.

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

En el resultado, deberías ver algo como esto:

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

En este punto, si vas a la consola de Cloud Spanner y consultas los datos de las tablas Singer y Album en la base de datos, verás algo como lo siguiente:

f18276ea54cc266f.png

952d9450dd659e75.png

9. Limpia

Borremos la instancia de Cloud Spanner que creamos al principio para asegurarnos de que no está usando los recursos innecesariamente.

gcloud spanner instances delete codelab-instance

10. Felicitaciones

Felicitaciones, compilaste correctamente una aplicación de Java que usa Hibernate para conservar datos en Cloud Spanner.

  • Creaste una instancia y una base de datos de Cloud Spanner
  • Configuraste la aplicación para que use Hibernate
  • Creaste dos entidades: Artista y Álbum.
  • Generaste automáticamente el esquema de base de datos de tu aplicación.
  • Guardaste correctamente las entidades en Cloud Spanner y las consultaste

Ahora conoces los pasos clave necesarios para escribir una aplicación de Hibernate con Cloud Spanner.

¿Qué sigue?