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 habilita herramientas de ORM aún más potentes, como Spring Data JPA. Además, existen muchos frameworks compatibles con Hibernate, como Spring Boot, Microprofile y Quarkus.

Cloud Spanner Dialect para Hibernate ORM 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 nuevas aprovechando la mayor productividad de los desarrolladores que ofrecen las tecnologías basadas en Hibernate.

Qué aprenderás

  • Cómo escribir una aplicación simple de Hibernate que se conecta a Cloud Spanner
  • Cómo crear una base de datos de Cloud Spanner
  • Cómo usar Cloud Spanner Dialect para Hibernate ORM
  • Cómo implementar operaciones de creación, lectura, actualización y eliminación (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

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

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 que no se te facture 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-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA.

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 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. Para ello, abre el editor de Cloud Shell y navega dentro del directorio spanner-hibernate-codelab.

b5cb37d043d4d2b0.png

Hasta ahora, solo tenemos una app de consola de Java básica que imprime "Hello World!". Sin embargo, lo que realmente queremos es escribir una aplicación de Java que use Hibernate para comunicarse con Cloud Spanner. Para ello, necesitaremos el dialecto de Cloud Spanner para 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, completemos hibernate.cfg.xml para indicarle a Hibernate las clases de entidades anotadas que asignaremos a la base de datos. (Crearemos las clases de entidades 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 JDBC de Spanner y la cadena de conexión 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 de tu proyecto, que puedes obtener ejecutando el 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 ejecutemos la app por primera vez.

Por lo general, también te asegurarías de que las credenciales de autenticación estén configuradas, ya sea con un archivo JSON de cuenta de servicio en la variable de entorno GOOGLE_APPLICATION_CREDENTIALS o con las credenciales predeterminadas de la aplicación configuradas con el comando gcloud auth application-default login. Sin embargo, como ejecutamos el comando en Cloud Shell, las credenciales predeterminadas del proyecto ya están configuradas.

7. Crea clases de entidades anotadas

Ahora, ya podemos escribir código.

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

Agrega las clases de entidades Singer y Album.

Crea los archivos de 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, usamos un UUID generado automáticamente para la clave primaria. Este es el tipo de ID preferido en Cloud Spanner, ya que evita los hotspots a medida que el sistema divide los datos entre servidores por rangos de claves. También funcionaría una clave de números enteros que aumente de forma monótona, pero puede tener un rendimiento menor.

8. Cómo guardar y consultar entidades

Con todo configurado y los objetos de entidad definidos, podemos comenzar a escribir en la base de datos y a consultarla. Abriremos un Session de Hibernate y, luego, 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 consultas de 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 subprocesos de daemon que Maven intentará realizar.

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

En el resultado, deberías ver algo similar a lo siguiente:

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 ves 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 recursos de forma innecesaria.

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 usar Hibernate
  • Creaste dos entidades: Artista y Álbum
  • Generaste automáticamente el esquema de la base de datos para tu aplicación
  • Guardaste entidades en Cloud Spanner y las consultaste correctamente

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

¿Qué sigue?