Cloud Spanner com Hibernate ORM

1. Visão geral

O Hibernate se tornou a solução ORM padrão para projetos Java. Ele é compatível com todos os principais bancos de dados relacionais e permite ferramentas ORM ainda mais poderosas, como o Spring Data JPA. Além disso, há muitos frameworks compatíveis com Hibernate, como Spring Boot, Microprofile e Quarkus.

O Dialeto do Cloud Spanner para Hibernate ORM permite usar o Hibernate com o Cloud Spanner. Você tem os benefícios do Cloud Spanner (escalonabilidade e semântica relacional) com a persistência idiomática do Hibernate. Isso pode ajudar você a migrar aplicativos atuais para a nuvem ou escrever novos, aproveitando o aumento da produtividade dos desenvolvedores proporcionado pelas tecnologias baseadas no Hibernate.

O que você vai aprender

  • Como gravar um aplicativo Hibernate simples que se conecta ao Cloud Spanner
  • Como criar um banco de dados do Cloud Spanner
  • Como usar o dialeto do Cloud Spanner para Hibernate ORM
  • Como implementar operações de criação, leitura, atualização e exclusão (CRUD) com o Hibernate

O que é necessário

  • Um projeto do Google Cloud Platform
  • Um navegador, como o Chrome ou o Firefox

2. Configuração e requisitos

Configuração de ambiente autoguiada

  1. Faça login no Console do Cloud e crie um novo projeto ou reutilize um existente. Crie uma se você ainda não tiver uma conta do Gmail ou do G Suite.

k6ai2NRmxIjV5iMFlVgA_ZyAWE4fhRrkrZZ5mZuCas81YLgk0iwIyvgoDet4s2lMYGC5K3xLSOjIbmC9kjiezvQuxuhdYRolbv1rft1lOmA_P2U3OYcaAzN9JgP-Ncm18i5qgf9LzA

UtcCMcSYtCOrEWuILx3XBwb3GILPqXDd6cJiQQxmylg8GNftqlnE7u8aJLhlr1ZLRkpncKdj8ERnqcH71wab2HlfUnO9CgXKd0-CAQC2t3CH0kuQRxdtP0ws43t5-O2d4d0WXDUfaw

KoK3nfWQ73s_x4QI69xqzqdDR4tUuNmrv4FC9Yq8vtK5IVm49h_8h6x9X281hAcJcOFDtX7g2BXPvP5O7SOR2V4UI6W8gN6cTJCVAdtWHRrS89zH-qWE0IQdjFpOs_8T-s4vQCXA6w

Lembre-se do código do projeto, um nome exclusivo em todos os projetos do Google Cloud. O nome acima já foi escolhido e não servirá para você. Faremos referência a ele mais adiante neste codelab como PROJECT_ID.

  1. Em seguida, será necessário ativar o faturamento no Console do Cloud para usar os recursos do Google Cloud.

A execução deste codelab não será muito cara, se for o caso. Siga todas as instruções na seção "Limpeza", que orienta você sobre como encerrar recursos para não incorrer em cobranças além deste tutorial. Novos usuários do Google Cloud estão qualificados para o programa de US$ 300 de avaliação sem custos.

Ativar o Cloud Shell

  1. No Console do Cloud, clique em Ativar o Cloud ShellR47NpBm6yyzso5vnxnRBikeDAXxl3LsM6tip3rJxnKuS2EZdCI0h-eIOGm9aECq8JXbMFlJkd68uTutXU8gGmQUVa5iI1OdZczXP2tzqZ_mj0pR4sZ8eSwOwUlWADR7ARCqrMTQPQA.

STsYbcAtkIQyN6nL9BJhld3Fv5KxedYynpUVcRWwvIR-sYMMc4kfK-unIYgtsD4P6T0P8z-A12388tPmAh-Trsx80qobaW4KQXHJ7qJI6rwm762LrxurYbxwiDG-v_HiUYsWnXMciw

Se você nunca tiver iniciado o Cloud Shell, verá uma tela intermediária (abaixo da dobra) com a descrição do que ele é. Se esse for o caso, clique em Continuar e você não o verá novamente. Esta é a aparência dessa tela única:

LnGMTn1ObgwWFtWpjSdzlA9TDvSbcY76GiLQLc_f7RP1QBK1Tl4H6kLCHzsi89Lkc-serOpqNH-F2XKmV5AnBqTbPon4HvCwSSrY_ERFHzeYmK1lnTfr-6x5eVoaHpRSrCUrolXUPQ

Leva apenas alguns instantes para provisionar e se conectar ao Cloud Shell.

hfu9bVHmrWw01Hnrlf4MBNja6yvssDnZzN9oupcG12PU88Vvo30tTluX9IySwnu5_TG3U2UXAasX9eCwqwZtc6Yhwxri95zG82DLUcKxrFYaXnVd7OqVoU6zanoZa0PtvubjLLHxnA

Essa máquina virtual contém todas as ferramentas de desenvolvimento necessárias. Ela oferece um diretório principal persistente de 5 GB, além de ser executada no Google Cloud. Isso aprimora o desempenho e a autenticação da rede. Praticamente todo o seu trabalho neste codelab pode ser feito em um navegador ou no seu Chromebook.

Depois de se conectar ao Cloud Shell, você já estará autenticado e o projeto já estará configurado com seu ID do projeto.

  1. Execute o seguinte comando no Cloud Shell para confirmar que você está autenticado:
gcloud auth list

Resposta ao 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

Resposta ao comando

[core]
project = <PROJECT_ID>

Se o projeto não estiver configurado, configure-o usando este comando:

gcloud config set project <PROJECT_ID>

Resposta ao comando

Updated property [core/project].

3. Criar um banco de dados

Depois que o Cloud Shell for iniciado, você poderá usar o gcloud para interagir com seu projeto do GCP.

Primeiro, ative a API Cloud Spanner.

gcloud services enable spanner.googleapis.com

Agora, vamos criar uma instância do Cloud Spanner chamada codelab-instance.

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

Agora, precisamos adicionar um banco de dados a essa instância. Vamos chamar de codelab-db.

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

4. Criar um app vazio

Vamos usar o arquétipo de início rápido do Maven para criar um aplicativo de console Java simples.

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

Mude para o diretório do app.

cd spanner-hibernate-codelab

Compile e execute o app usando o Maven.

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

Você vai ver Hello World! impresso no console.

5. Adicionar dependências

Vamos analisar o código-fonte abrindo o editor do Cloud Shell e navegando no diretório spanner-hibernate-codelab.

b5cb37d043d4d2b0.png

Até agora, temos apenas um app de console Java básico que imprime "Hello World!". No entanto, queremos escrever um aplicativo Java que use o Hibernate para se comunicar com o Cloud Spanner. Para isso, vamos precisar do Dialeto do Cloud Spanner para Hibernate, do driver JDBC do Cloud Spanner e do núcleo do Hibernate. Então, vamos adicionar as seguintes dependências ao bloco <dependencies> dentro do arquivo 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. Configurar o Hibernate ORM

Em seguida, vamos criar os arquivos de configuração do Hibernate hibernate.cfg.xml e hibernate.properties. Execute o comando a seguir para criar os arquivos vazios e edite-os usando o editor do Cloud Shell.

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

Vamos informar ao Hibernate sobre as classes de entidade anotadas que vamos mapear para o banco de dados preenchendo o hibernate.cfg.xml. Vamos criar as classes de entidade mais tarde.

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>

O Hibernate também precisa saber como se conectar à instância do Cloud Spanner e qual dialeto usar. Portanto, vamos dizer para ele usar o SpannerDialect para sintaxe SQL, o driver JDBC do Spanner e a string de conexão JDBC com as coordenadas do banco de dados. Isso vai para o arquivo 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

Substitua {PROJECT_ID} pelo ID do projeto, que pode ser obtido executando o seguinte comando:

gcloud config get-value project

Como não temos um esquema de banco de dados, adicionamos a propriedade hibernate.hbm2ddl.auto=update para permitir que o Hibernate crie as duas tabelas no Cloud Spanner quando executarmos o app pela primeira vez.

Normalmente, você também verifica se as credenciais de autenticação estão configuradas usando um arquivo JSON de conta de serviço na variável de ambiente GOOGLE_APPLICATION_CREDENTIALS ou as credenciais padrão do aplicativo configuradas com o comando gcloud auth application-default login. No entanto, como estamos executando no Cloud Shell, as credenciais padrão do projeto já estão configuradas.

7. Criar classes de entidades anotadas

Agora podemos escrever um pouco de código.

Vamos definir dois objetos Java antigos simples (POJOs) que serão mapeados para tabelas no Cloud Spanner: Singer e Album. O Album terá uma relação @ManyToOne com Singer. Também poderíamos ter mapeado Singers para listas de Albums com uma anotação @OneToMany, mas, para este exemplo, não queremos carregar todos os álbuns sempre que precisamos buscar um cantor no banco de dados.

Adicione as classes de entidade Singer e Album.

Crie os arquivos de classe.

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

Cole o conteúdo dos arquivos.

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

Neste exemplo, estamos usando um UUID gerado automaticamente para a chave primária. Esse é um tipo de ID preferido no Cloud Spanner porque evita pontos de acesso, já que o sistema divide os dados entre servidores por intervalos de chaves. Uma chave de número inteiro que aumenta monotonicamente também funciona, mas pode ter um desempenho menor.

8. Salvar e consultar entidades

Com tudo configurado e os objetos de entidade definidos, podemos começar a gravar e consultar o banco de dados. Vamos abrir um Session do Hibernate e usá-lo para excluir todas as linhas da tabela no método clearData(), salvar algumas entidades no método writeData() e executar algumas consultas usando a linguagem de consulta do Hibernate (HQL) no método readData().

Substitua os conteúdos de App.java pelo seguinte:

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

Agora, vamos compilar e executar o código. Vamos adicionar a opção -Dexec.cleanupDaemonThreads=false para suprimir avisos sobre a limpeza de linhas de execução de daemon que o Maven vai tentar fazer.

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

Na saída, você vai ver algo assim:

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

Neste ponto, se você acessar o console do Cloud Spanner e visualizar os dados das tabelas "Singer" e "Album" no banco de dados, verá algo assim:

f18276ea54cc266f.png

952d9450dd659e75.png

9. Limpar

Vamos excluir a instância do Cloud Spanner que criamos no início para garantir que ela não esteja usando recursos desnecessariamente.

gcloud spanner instances delete codelab-instance

10. Parabéns

Parabéns! Você criou um aplicativo Java que usa o Hibernate para persistir dados no Cloud Spanner.

  • Você criou uma instância e um banco de dados do Cloud Spanner.
  • Você configurou o aplicativo para usar o Hibernate
  • Você criou duas entidades: "Artista" e "Álbum".
  • Você gerou automaticamente o esquema de banco de dados para seu aplicativo
  • Você salvou e consultou entidades no Cloud Spanner.

Agora você sabe as principais etapas necessárias para escrever um aplicativo Hibernate com o Cloud Spanner.

Qual é a próxima etapa?