Avancadas

Hierárquicas

Conhecendo as anotações hierárquicas do JPA

Introdução

Mesmo sendo uma abordagem muito utilizada no meio acadêmico, utilizar herança com o intuíto de reaproveitamento de código é algo que precisa ser evitado ou caso necessário, analisar bem o contexto de utilização.

Quando se trabalha com JPA (Java Persistence API), o mapeamento de herança entre entidades pode ser uma das partes mais complexas e, ao mesmo tempo, poderosas no desenvolvimento de sistemas orientados a objetos. A herança no JPA permite que você modele hierarquias de classes, reutilizando atributos e comportamentos em classes filhas, ao mesmo tempo em que controla como esses dados são persistidos no banco de dados.

Neste tutorial, vamos explorar o mapeamento de herança com JPA, utilizando um cenário prático envolvendo duas entidades: Cliente e Fornecedor. Ambas compartilham atributos comuns, como id, cpfCnpj e nome, mas cada uma possui também atributos específicos: o limiteCredito para o Cliente e a primeiraParcela para o Fornecedor. A partir deste cenário, vamos abordar três estratégias comuns de mapeamento de herança no JPA: Single Table Inheritance (STI), Joined Table Inheritance (JTI) e Table Per Class Inheritance (TPC).

Cada uma dessas estratégias oferece vantagens e desvantagens, dependendo dos requisitos do seu sistema, como performance, simplicidade de implementação e flexibilidade na modelagem dos dados. Ao Integero deste tutorial, você aprenderá como implementar cada uma dessas abordagens e entenderá em quais situações elas são mais adequadas, ajudando a decidir qual é a melhor estratégia para o seu projeto.

Estratégias

Single Table

Single Table

Nessa estratégia, todas as classes (Cliente e Fornecedor) compartilham uma única tabela no banco de dados. Para diferenciar as entidades, uma coluna extra chamada geralmente discriminator é adicionada. A tabela terá todos os campos de ambas as entidades, incluindo os campos específicos de cada uma.

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "tipo", discriminatorType = DiscriminatorType.STRING)
@Entity
public class Pessoa {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String cpfCnpj;
    private String nome;
}

@Entity
@DiscriminatorValue("CLIENTE")
public class Cliente extends Pessoa {
    private Double limiteCredito;
}

@Entity
@DiscriminatorValue("FORNECEDOR")
public class Fornecedor extends Pessoa {
    private Integer primeiraParcela;
}
Conclusão: Na estratégia Single Table Inheritance (STI), todas as classes de uma hierarquia de herança são mapeadas para uma única tabela no banco de dados. Uma coluna extra, chamada discriminator, é usada para identificar o tipo da entidade (classe filha) para cada registro. Essa abordagem é simples e fácil de implementar, mas pode resultar em tabelas com muitos valores nulos, já que as colunas específicas de cada classe são armazenadas na mesma tabela.

Joined Table

Joined

Nessa estratégia, cada classe tem sua própria tabela. A tabela da classe filha contém uma chave estrangeira que referencia a tabela da classe pai. As tabelas para Cliente e Fornecedor armazenam apenas seus atributos específicos.

Não é necessário a anotação @DiscriminatorValue nas classes filhas.
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
public class Pessoa {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String cpfCnpj;
    private String nome;
}
Conclusão: Na estratégia Joined Table Inheritance (JTI), cada classe de uma hierarquia de herança é mapeada para uma tabela separada. A tabela da classe pai contém os atributos comuns, enquanto as tabelas das classes filhas armazenam seus próprios atributos específicos. Cada tabela filha contém uma chave estrangeira que referencia a tabela da classe pai, criando uma relação de junção entre elas.

Table Per Class

Table Per Class

Com esta estratégia, cada classe tem sua própria tabela e todas as colunas (inclusive as da classe pai) são duplicadas nas tabelas das classes filhas. Não há chave estrangeira entre as tabelas.

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Entity
public class Pessoa {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    
    //demais campos
}
Conclusão: Na estratégia Table Per Class Inheritance (TPC), cada classe da hierarquia de herança é mapeada para uma tabela separada. Cada tabela contém todos os atributos da classe, incluindo os da superclasse, ou seja, não há herança de colunas entre as tabelas. Em outras palavras, as tabelas filhas duplicam os atributos da classe pai.

Mapped Superclass

Mapped Superclass

A estratégia Mapped Superclass no JPA é uma abordagem usada quando você quer compartilhar atributos e comportamentos entre várias entidades, mas sem mapear uma tabela para a superclasse. Diferente das outras estratégias de herança (como SingleTable, Joined ou TablePerClass), a MappedSuperclass não cria uma tabela no banco de dados para a superclasse, mas permite que as subclasses herdem seus atributos.

@MappedSuperclass
public class Pessoa {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String cpfCnpj;
    private String nome;

    // Getters e setters
}

Você Sabia?

A principal diferença entre Table Per Class e Mapped Superclass no JPA está na forma como as tabelas são gerenciadas e como as entidades são persistidas no banco de dados

  • Table Per Class cria uma tabela para cada classe, incluindo a superclasse, o que pode levar a redundância de dados.
  • Mapped Superclass não cria uma tabela para a superclasse, apenas as subclasses herdam seus atributos, evitando a redundância, mas sem permitir consultas diretas à superclasse.

Conclusão

As estratégias de mapeamento por herança no JPA oferecem diferentes formas de modelar hierarquias de classes, cada uma com suas vantagens e desvantagens, dependendo dos requisitos do sistema.

  • Single Table Inheritance (STI) é uma solução simples, onde todas as classes da hierarquia são armazenadas em uma única tabela. Embora seja eficiente em termos de estrutura, pode levar a problemas de redundância e valores nulos para atributos não aplicáveis em todas as entidades.
  • Joined Table Inheritance (JTI) promove uma abordagem mais normalizada, com cada classe e seus atributos sendo armazenados em tabelas separadas. Essa estratégia reduz a redundância de dados, mas pode impactar o desempenho devido à necessidade de realizar junções nas consultas.
  • Table Per Class Inheritance (TPC), por sua vez, cria uma tabela exclusiva para cada classe, duplicando os dados da superclasse nas tabelas filhas. Embora elimine a necessidade de junções, essa estratégia pode resultar em um maior uso de armazenamento devido à duplicação de dados.
  • Mapped Superclass, por outro lado, é uma abordagem que não envolve criação de tabelas para a superclasse, permitindo que as subclasses herdem os atributos e comportamentos comuns sem redundância de dados, mas sem a possibilidade de realizar consultas diretas à superclasse.

A escolha entre essas estratégias deve ser baseada nas necessidades específicas do seu projeto, como performance, redundância de dados, complexidade das consultas e modelo de persistência desejado. Em sistemas mais simples, o Single Table Inheritance pode ser suficiente, enquanto em sistemas maiores, que exigem maior normalização e controle sobre as consultas, o Joined Table Inheritance ou Table Per Class Inheritance podem ser mais adequados. O Mapped Superclass é ideal para cenários onde a superclasse não deve ter uma tabela própria, mas seus atributos precisam ser compartilhados pelas subclasses.

Em última análise, entender as implicações de cada abordagem no desempenho, manutenção e estrutura do banco de dados é essencial para fazer a escolha mais apropriada, garantindo que o mapeamento de herança no JPA atenda às necessidades do sistema de forma eficiente e escalável.

Referências