Camada de Persistência
Introdução
A camada de persistência é composta do mapeamento ORM (Object–Relational Mapping) com a utilização da especificação JPA (Java Persistence API).
A seguir estão dispostas regras, convenções e boas práticas para o uso eficiente do JPA, incluindo exemplos de mapeamento para todas as cardinalidades.
Diretrizes
-
Evite mapeamento na forma de herança, pois isso aumenta a complexidade do projeto e normalmente restringe as funcionalidades de querying do JPA. Utilize herança somente se necessário.
-
Não utilize mapeamento @OneToOne, devido a limitações de fetch lazy do JPA, que pode impactar substancialmente na performance. Vide Anexo I para mais informações.
-
Não utilize mapeamento @ManyToMany, pois traz risco de grandes impactos no código quando houver alterações na estrutura do banco de dados.
-
Não utilize cascade no mapeamento. A camada de serviço deverá implementar a lógica necessária.
-
Sempre mapeie associativas como lazy, pois no contrário há risco de degradação da performance. A camada de serviço deverá configurar o fetch das associativas conforme necessidade de cada funcionalidade.
-
Evite a utilização da API do Hibernate, como por exemplo Session e Criteria, pois ela foi descontinuada.
-
As operações básicas de CRUD estão disponibilizadas na forma de Active Record nas entidades persistentes, conforme explanado no capítulo seguinte.
-
A utilização dos métodos de Active Record como também da API Entity Manager do JPA deve ser realizada na camada de negócio, e nunca a partir de entidades de persistência.
-
Não utilize os patterns Rich Domain e Repository, pois esses padrões desfavorecem o encapsulamento de funcionamentes, tendem a resultar em classes muito grandes e incorrem eventualmente em alta taxa de referências cíclicas, dificultando a manutenibilidade do código.
-
Não utilize o pattern DAO, que se torna desnecessário dadas as funcionalidade do EntityManager do JPA.
-
Não há restrição quanto às formas de querying JPA. Sugere-se Criteria Query ou JPQL em funcionalidades essencialmente transacionais e SQL nativo em funcionalidades essencialmente de consulta de dados.
-
Adote a estratégia de nomes padrão do JPA, salvo se houver restrição técnica.
Convenções de Nomes
Em conformidade com a estratégia de nomes padrão do JPA 2.0:
-
Nomes de tabelas e colunas em CamelCase. Exemplo: “OrdemServico”.
-
Nomes de tabelas iniciam em maiúsculo, tipicamente no singular e sem prefixos. Exemplo: “Fatura”.
-
Nomes de colunas iniciam em minúsculo. Exemplo: “dataCancelamento”.
-
Tabela de junção recebe o nome pela concatenação das tabelas envolvidas. Exemplo: “FaturaCliente”.
-
Chave estrangeira é formada pela concatenação da tabela referenciada com “_” mais nome coluna referenciada. Exemplo:
ordemServico_id
(FK para tabela OrdemServico na colunaid
). -
Nomes compostos seguem preferencialmente a ordem natural da palavras. Exemplo: “SituacaoConta”, “SituacaoHidrometro”, e não “ContaSituacao”, “HidrometroSituacao”.
-
Hierarquias ou grupos podem ser prefixados. Exemplo: tabelas “EnderecoQuadra”, “EnderecoSetor” e “EnderecoRua” ou colunas “rgNumero”, “rgExpedicaoOrgao” e “rgExpedicaoData”.
-
Abrevie somente palavras grandes (tipicamente acima de ~10 letras).
-
Siglas são tratadas como palavras no CamelCase. Exemplos: “Rg”, “Uf”, “CpfDestino” e não “RG”, “UF” e “CPFDestino”.
Colunas Padrões
Por padrão as tabelas possuem as seguintes colunas mínimas, opcionais:
-
Chave primária simples, de nome
id
e auto incrementada. Exemplo:BIGINT id NOT NULL AUTO_INCREMENT -- (ou IDENTITY(1,1))
-
Colunas para atendimento ao Spring JPA Auditing:
VARCHAR createdBy TIMESTAMP (WITH TIME ZONE) createdDate VARCHAR lastModifiedBy TIMESTAMP (WITH TIME ZONE) lastModifiedDate
-
Recomenda-se, adicionalmente, nas entidades fortes, um coluna de UID, que será alimentada pela aplicação com uma identificação única pseudo-aleatória:
BIGINT uid -- incluir índice unique
O UID será utilizado no lugar do
id
nas comunicações que atravessam a fronteira do backend (ex: webservices).O objetivo é mitigar falha de segurança ao possibilitar que aplicações clientes possam varrer o banco de dados a partir de id sequencial.
Se houver restrição técnica ou necessidade específica, as colunas padrões não precisarão ser criadas.
Estrutura de Pacotes
A camada de persistência tem a seguinte estrutura de pacotes:
Pacote | Descrição |
---|---|
.persistence.entities | Entidades mapeadas em JPA, sejam classes ou enums. |
.persistence.converters | Conversores JPA para enums. |
Mapeamento JPA
As tabelas do banco de dados são mapeadas tipicamente como classes Java, chamadas de “entidades persistentes”. Recomenda-se que algumas tabelas, entretanto, sejam mapeadas como enums quando elas:
-
Forem estáticas no tempo ou mudarem eventualmente.
-
Possuírem poucas colunas e poucos registros (até ~50).
-
Não possuírem funcionalidade de manutenção pelo usuário.
Tabelas de “dados de código”, como por exemplo de “tipos” ou “situações”, são bons exemplos para mapeamento como enum.
O mapeamento na forma de enum visa aumentar o desempenho da aplicação através da redução de acessos ao banco de dados. Além disso, a criação dos enums é importante quando seus valores são referenciados no Java em regras de negócio.
Observação: O banco deve ser implementado normalmente, independentemente de a aplicação mapear determinada tabela como enum ou classe.
Classe
Ao mapear tabela do banco de dados como classe Java:
-
Declarar dentro do pacote
.persistence.entities
e evitar criação de pacotes internos. -
Herdar de
BaseEntity
e anotar com@Entity
.- Para casos especiais onde não se aplicam as colunas padrões, herdar de
BaseGenericEntity
.
- Para casos especiais onde não se aplicam as colunas padrões, herdar de
-
Configurar getters e setters automáticos via Lombok.
-
Mapear anotações JPA diretamente nos atributos e não nos getters.
-
Lembrar de mapear associaçães de qualquer cardinalidade sempre como
lazy
e semcascade
. No JPA o lazy é padrão apenas em coleções. -
Mapear coleções na forma de
Set
(List possui limitações com FetchType). -
Não implementar equals e hashCode.
-
Não implementar a interface
Comparable
ou o método compareTo. -
Mapear chave composta simplesmente repetindo
@Id
nos getters.- Evitar
@IdClass
e@Embeddable
e caso o fizer deve-se implementarhashCode
eequals
envolvendo todos e somente os atributos da chave composta.
- Evitar
-
Não mapear campos como atributos primitivos.
-
Mapear campo
uid
comoLong
(preferencialmente) ouString
. -
Mapear campos decimais como
BigDecimal
. -
Mapear campos de data como
java.time.OffsetDateTime
. -
Não implementar RN (regra de negócio).
-
Não fazer LOG.
-
Não tratar exceção.
-
É permitido métodos de transformação de dados (use
@Transient
).
Exemplo de entidade com chave simples:
@Entity
@Getter
@Setter
public class Modalidade extends BaseEntity {
@Column(unique = true)
private Long uid; // opcional, conforme necessidade
private String sigla;
private String descricao;
}
Exemplo de entidade com Chave Composta, sem as colunas padrões:
@Entity
@Getter
@Setter
public class Fatura extends BaseGenericEntity {
@Id
private Integer referencia;
@Id
private Integer sequencial;
}
Utilize chave composta somente se estritamente necessário, por alguma restrição técnica.
Observação: Ao herder de @BaseGenericEntity
, a entidade recebe o Active Record bem
como a implementação automática de equals e hashCode, porém não recebe o mapeamento
do atributo padrão id
nem dos campos do JPA Auditing. O mapeamento padrão do id
e do JPA Auditing está incluso em @BaseEntity
.
Enum
Ao mapear tabela do banco de dados como enum Java:
-
Declarar dentro do pacote
.persistence.entities
e evitar criação de pacotes internos. -
Não implementar setters.
-
Não implementar toString.
-
Implementar o método
public static Optional<NomeDoEnum> getEnum(TipoDoId id)
para uso pelo Converter JPA.
Para cada enum, implementar seu respectivo Converter JPA conforme as regras:
-
Declarar dentro do pacote
.persistence.converters
e evitar criação de pacotes internos. -
Anotar com
@Converter(autoApply = true)
. -
Implementar a interface
AttributeConverter
.
Normalmente os enums terão id
do tipo Integer
. Em alguns casos, como no exemplo abaixo,
é conveniente utilizar uma chave semântica, que deve estar em consonância com o banco de dados.
Exemplo de enum:
@Getter
@AllArgsConstructor
public enum EstadoCivil {
SOLTEIRO ( 'S', "Solteiro"),
CASADO ( 'C', "Casado"),
UNIAO ( 'U', "União Estável"),
VIUVO ( 'V', "Viúvo"),
DIVORCIADO ( 'D', "Divorciado"),
SEPARADO ( 'E', "Separado");
private Character id;
private String descricao;
public static Optional<EstadoCivil> getEnum(Character id) {
if (id == null)
return Optional.empty();
return Stream.of(values())
.filter(i -> i.getId().equals(id))
.findFirst();
}
}
@Entity
@Getter
@Setter
public class Cliente extends BaseEntity
{
@Column(name="estadoCivil_id")
private EstadoCivil estadoCivil;
}
Exemplo de Converter JPA para o enum acima:
@Converter(autoApply = true)
public class EstadoCivilConverter implements AttributeConverter<EstadoCivil, Character> {
@Override
public Character convertToDatabaseColumn(EstadoCivil valor) {
return valor == null ? null : valor.getId();
}
@Override
public EstadoCivil convertToEntityAttribute(Character id) {
return (id == null ? null : EstadoCivil.getEnum(id)
.orElseThrow(() -> new UnexpectedEnumValueException(id)));
}
}
Associações
As associações entre entidades estão relacionadas às chaves estrangeiras no banco de dados.
-
Recomenda-se realizar o mapeamento JPA de maneira tão próxima possível do modelo físico do banco, ou seja, as chaves estrangeiras são mapeadas pela anotação
@ManyToOne
, que representa uma relação do tipo “Muitos-para-um”.-
Essa abordagem reduz complexidade do código e evita incorrer em limitações do JPA, especialmente de performance.
-
Lembrar de mapear a associação
@ManyToOne
sempre comolazy
e semcascade
.
-
-
Os mapeamentos podem ser unidirecionais ou bidirecionais, conforme a navegabilidade desejada entre os objetos.
-
Evitar mapeamentos
@OneToOne
e@ManyToMany
, sejam unidirecionais ou bidirecionais, em razão de limitações do lazy do JPA bem como de manutenibilidade do código quando houver alterações na estrutura do banco de dados.
Exemplo de associação unidirecional “Muitos-para-um”:
@Entity
@Getter
@Setter
public class Cliente extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
private Empresa empresa;
}
Considere neste exemplo a relação “MUITOS clientes associados a UMA empresa”.
A tabela Cliente deve possuir uma chave estrangeira (
empresa_id
) que aponte para a tabela Empresa.
Para navegação bidirecional, a entidade Empresa
ficaria assim:
@Entity
@Getter
@Setter
public class Empresa extends BaseEntity {
@OneToMany(targetEntity = Cliente.class, mappedBy = "empresa")
private Set<Cliente> clientes;
}
No caso bidirecional, sempre um lado é considerado dono da relação (“owner”) e o outro lado é chamado de inverso (onde há o atributo
_mappedBy
_). O JPA não sincroniza o banco de dados com base nas alterações que são feitas no lado inverso.
Exemplo de associação “Muitos-para-um” com chave composta:
@Entity
@Getter
@Setter
public class ItemFatura extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumns( {
@JoinColumn(name = "fatura_referencia", referencedColumnName = "fatura_referencia"),
@JoinColumn(name = "fatura_sequencial", referencedColumnName = "fatura_sequencial") })
private Fatura fatura;
}
Atributo
name
refere-se a coluna na tabela “itemFatura” ereferencedColumnName
refere-se a coluna na tabela “fatura”.Importante: Os atributos
insertable=false, updatable=false
podem ser especificados noJoinColumn
caso a coluna também faça parte de outra associativa.
Anexos
O presente anexo trata de mapeamentos nas demais cardinalidades, que não “Muitos-para-um” apresentada na seção “Mapeamento JPA”. Esses demais cenários de cardinalidade, a seguir elencados, não são recomendados, conforme observações dispostas em cada caso.
I - “Um-para-um”
Considere a relação “UM cliente associado a UM telefone”.
O JPA não suporta o mapeamento lazy em @OneToOne
, o que provoca o conhecido
problema de performance select N+1, devido ao efeito EAGER da relação @OneToOne
.
Por esse motivo, opte pelo mapeamento “Muitos-para-um” e inclua um comentário
sobre a cardinalidade esperada.
@Entity
@Getter
@Setter
public class Cliente extends BaseEntity {
// Relação 1:1
@ManyToOne(fetch = FetchType.LAZY)
private Telefone telefone;
}
Para navegação bidirecional, a entidade Telefone
ficaria assim:
@Entity
@Getter
@Setter
public class Telefone extends BaseEntity {
// Relação 1:1
@OneToMany(mappedBy = "telefone")
private Set<Cliente> clientes;
// Método responsável por tornar a relação 1:1 transparente
@Transient
public Cliente getCliente() {
if (clientes == null) {
return null;
}
if (clientes.size() > 1) {
throw new IlegallStateException("Esperava-se cardinalidade 1:1");
}
return clientes.iterator().next();
}
}
O atributo
mappedBy
indica o lado inverso da associação. Ou seja, alterações nesse lado não serão persistidas em banco.
II - “Muitos-para-muitos”
Considere a relação “MUITOS clientes associados a MUITAS empresas”.
Sugere-se evitar este caso de mapeamento em favor da opção “Muitos-para-um”. Para isso,
mapeie a tabela de ligação ClienteEmpresa na forma da entidade ClienteEmpresa
contendo as
duas relações @ManyToOne
(para Cliente e para Empresa), seguindo o modelo físico do banco de dados.
Isso evita impacto no código-fonte caso a tabela de ligação venha a conter novos atributos.
@Entity
@Getter
@Setter
public class Cliente extends BaseEntity {
@ManyToMany(targetEntity = Empresa.class)
@JoinTable(name = "ClienteEmpresa",
joinColumns = @JoinColumn(name = "cliente_id"),
inverseJoinColumns = @JoinColumn(name = "empresa_id"))
private Set<Empresa> empresas;
}
Neste caso, é necessária uma tabela de ligação ClienteEmpresa que contenha a chave estrangeira para Cliente (
cliente_id
) e outra para Empresa (empresa_id
).O JPA controla os inserts e deletes automaticamente na tabela
ClienteEmpresa
.
Para navegação bidirecional, a entidade Empresa
ficaria assim:
@Entity
@Getter
@Setter
public class Empresa extends BaseEntity {
@ManyToMany(targetEntity = Cliente.class, mappedBy = "empresa")
private Set<Cliente> clientes;
}
O atributo
mappedBy
indica o lado inverso da associação. Ou seja, alterações nesse lado não serão persistidas em banco.
III - “Um-para-muitos”
Considere a relação “UM cliente associado a MUITAS empresas”.
Sugere-se evitar este caso de mapeamento em favor da opção “Muitos-para-um” bidirecional. O motivo reside no fato que de semântica do lado “forte” da relação, aquele pelo qual o JPA considera para fins de sincronização do banco de dados, mudará quando o mapeamento evoluir para bidirecional, o que poderia causar bug silecionso, de difícil identificação.
Exemplo de mapeamento “Um-para-muitos” unidirecional:
@Entity
@Getter
@Setter
public class Cliente extends BaseEntity {
@OneToMany(targetEntity = Empresa.class, orphanRemoval = true)
@JoinColumn(name = "cliente_id")
private Set<Empresa> empresas;
}
Neste caso, a tabela Empresa deve possuir uma chave estrangeira (
cliente_id
) que aponte para a tabela Cliente.O atributo
orphanRemoval = true
é opcional. Com ele, a operaçãocliente.getEmpresas().remove(empresa1)
provoca umdelete
do objetoempresa1
. O padrão do JPA é fazerupdate
gravando nulo na chave estrangeira (set cliente_id = null
). Ou seja, o framework “prefere” não apagar os dados, apenas desvinculá-los.
Para navegação bidirecional, o mapeamento deve ser realizado conforme descrito em “Mapeamento JPA” - cenário “Muitos-para-um” bidirecional.
Próximos Passos
A próxima leitura sugerida é a seção Negócio, que detalha a camada de serviços.
Mais informações sobre JPA estão disponíveis no manual do ObjectDB (EN) e no manual do Hibernate (EN). Antes de aplicar instruções destes dois manuais, por motivos de segurança, performance e legibilidade do código-fonte, considere atender às diretrizes e boas práticas apresentadas nesta seção.