Aprimoramento de Bytecode no Hibernate

Por Gaspar Barancelli Junior em 22 de fevereiro de 2023

Nas versões iniciais o Hibernate não possuía o recurso de aprimoramento de bytecode, portanto, era muito usado o padrão Proxy para entregar determinadas funcionalidades, como carregamentos lentos, e também fazia muito uso de cálculo sujo baseado em diff, para que fosse possível saber se o valor de uma determinada entidade teve alguma alteração para refleti-la no banco de dados.

O recurso de aprimoramento de bytecode foi introduzido na versão 3.x do Hibernate, mas apenas como experimento, uma versão de incubação digamos assim, e que seguiu assim até se tornar maduro o suficiente e ser lançada como estável na versão 5.0.

O propósito do aprimoramento de bytecode é adicionar vários recursos relacionados à persistência diretamente na classe, evitando com que o Hibernate tenha que aplicar controles complexos, pesados e paralelos à sua entidade.

Benefícios

Carregamento Lento (Lazy)

Através deste recurso podemos carregar parcialmente atributos de uma entidade, por meio da anotação @Lazy. Podemos dizer ao Hibernate que apenas parte dos atributos de uma entidade devem ser carregados assim que o objeto for instanciado, e que outros atributos da entidade possam ser carregados somente quando forem utilizados.

Os atributos Lazy podem ser carregados simultaneamente, ou separados por grupo utilizando a anotação @LazyGroup. Por padrão quando não especificamos nenhum LazyGroup todos os atributos da entidade fazem parte de um único grupo, sendo assim, quando um desses atributos for acessado todos os outros serão carregados. Mas quando dividimos os atributos em grupos, quando acessamos um atributo que faz parte do grupo X, somente os outros atributos vinculados a esse grupo serão carregados.

Segue o exemplo do mapeamento de uma entidade fazendo o uso do recurso Lazy.

@Entity
public class Venda {

  @Id
  private Integer id;

  private LocalDateTime data;

  @Basic(fetch = FetchType.LAZY)
  private Cliente cliente;

  @Lob
  @Basic(fetch = FetchType.LAZY)
  @LazyGroup("lobs")
  private Blob notaFiscal;

}
Rastreamento Sujo

Como mencionado anteriormente o Hibernate fazia muito uso de cálculo sujo baseado em diff para determinar quais entidades foram alteradas para refletir as alterações no banco de dados. Isso essencialmente significa que o Hibernate manterá o controle do último estado conhecido de uma entidade em relação ao banco de dados (normalmente a última leitura ou gravação). Então, como parte do esvaziamento do contexto de persistência, o Hibernate percorreria cada entidade associada ao contexto de persistência e verificaria seu estado atual contra aquele "último estado conhecido do banco de dados". Esta é de longe a abordagem mais completa para a verificação suja, porque leva em conta os tipos de dados que podem alterar seu estado interno (java.util.Date é o principal exemplo disso). No entanto, num contexto de persistência com um grande número de entidades associadas, também pode ser uma abordagem de inibição de desempenho.

Fazendo uso do aprimoramento de bytecode o Hibernate adiciona o "rastreamento sujo" diretamente à entidade, permitindo assim que a própria entidade monitore quais atributos foram alterados, fazendo com que se ganhe performance no tempo de esvaziamento de contexto de persistência, pois o Hibernate irá perguntar a entidade o que mudou em vez de ter que realizar diversos cálculos de diferença de estado em seus atributos.

Associação Bidirecional

Com o recurso de aprimoramento de bytecode o seu aplicativo se mantem o mais próximo possível do "uso normal do Java". Considere o seguinte modelo de domínio.

@Entity(name = "Autor")
public static class Autor {

  @Id
  private Long id;

  private String nome;

  @OneToMany(mappedBy = "autor")
  private List<Livro> livros = new ArrayList<>();

}

@Entity(name = "Livro")
public static class Livro {

  @Id
  private Long id;

  private String titulo;

  @NaturalId
  private String isbn;

  @ManyToOne
  private Autor autor;

}

Segue exemplo da manipulação das entidades acima sem utilização do aprimoramento de bytecode.

Autor autor = new Autor();
autor.setNome("Gaspar");

Livro livro = new Livro();
autor.getLivros().add(livro);
try {
  livro.getAutor().getNome();
} catch (NullPointerException expected) {
  // Lançaria uma NPE pois a referência não é atualizada
}

Utilizando o aprimoramento de bytecode o Hibernate faz todo o gerenciamento das associações, quando um lado da associação for alterado ele atualizará o outro, sendo assim, o exemplo abaixo não lançaria nenhuma Exception.

Autor autor = new Autor();
autor.setNome("Gaspar");

Livro livro = new Livro();
autor.getLivros().add(livro);

livro.getAutor().getNome();
Otimizações de Desempenho

Hibernate tem um processo de aprimoramento que pode adicionar códigos que permitem otimizar certas características de desempenho no contexto de persistência. Mas vamos discutir isso num futuro post, pois precisamos entrar em detalhes nos componentes internos do Hibernate.

Habilitando o aprimoramento em tempo de execução

Por padrão o suporte ao aprimoramento de bytecode é desabilitado. Para habilitar essa funcionalidade o Hibernate disponibiliza as seguintes propriedades de configuração.

  • hibernate.enhancer.enableDirtyTracking: Habilita o recurso de rastreamento sujo.

  • hibernate.enhancer.enableLazyInitialization: Habilita o recurso de carregamento lento. Desta forma, até mesmo os tipos básicos podem ser carregados lentamente.

  • hibernate.enhancer.enableAssociationManagement: Habilita o recurso de gerenciamento de associação, que sincroniza automaticamente uma associação bidirecional quando apenas um lado é alterado.

Os valores a serem definidos a todas propriedades de configuração são do tipo booleano, sendo false o valor padrão.

Habilitando o aprimoramento em tempo de compilação

Plugin para Maven

O Hibernate fornece um plugin Maven capaz de fornecer as melhorias em tempo de construção, conforme os domínios forem sendo compilados.

<build>
   <plugins>
       [...]
       <plugin>
           <groupId>org.hibernate.orm.tooling</groupId>
           <artifactId>hibernate-enhance-maven-plugin</artifactId>
           <version>$currentHibernateVersion</version>
           <executions>
               <execution>
                   <configuration>
                       <failOnError>true</failOnError>
                       <enableLazyInitialization>true</enableLazyInitialization>
                       <enableDirtyTracking>true</enableDirtyTracking>
                       <enableAssociationManagement>true</enableAssociationManagement>
                   </configuration>
                   <goals>
                       <goal>enhance</goal>
                   </goals>
               </execution>
           </executions>
       </plugin>
       [...]
   </plugins>
</build>

O plugin fornece as seguintes propriedades de configuração:

  • failOnError: Controla o que acontece em caso de erro. O comportamento padrão é falhar na construção, mas pode ser configurado para que apenas um aviso seja emitido.

  • enableLazyInitialization: Se o aprimoramento para carregamento lento de atributos deve ser feito.

  • enableDirtyTracking: Se o aprimoramento para rastreamento auto-sujo deve ser feito.

  • enableAssociationManagement: Se o aprimoramento para gerenciamento de associação bidirecional deve ser feito.

Plugin para Gradle

O Hibernate também oferece um plugin para o Gradle capaz de fornecer as melhorias em tempo de construção, conforme os domínios forem sendo compilados. A única diferença em relação ao plugin do Maven é que o do Gradle não possui a configuração failOnError mencionada anteriormente.

apply plugin: 'org.hibernate.orm'

ext {
   hibernateVersion = 'hibernate-version-you-want'
}

buildscript {
   dependencies {
       classpath "org.hibernate:hibernate-gradle-plugin:$hibernateVersion"
   }
}

hibernate {
   enhance {
       enableLazyInitialization = true
       enableDirtyTracking = true
       enableAssociationManagement = true
   }
}

// Livros recomendados relacionados ao assunto do post

// Compartilhe esse Post