Toda entidade tem uma identidade
Aprenda a definir a identidade das suas entidades de maneira correta e segura
Quando desenvolvemos um sistema utilizando uma linguagem orientada a objetos nós temos tantos detalhes para se preocupar que acabamos deixando passar alguns deles. Temos que modelar o domínio do negócio em classes e objetos, também precisamos mapear estas classes com um framework ORM, como a JPA, e ainda temos que nos preocupar em qual framework Web MVC usaremos no projeto! Adicione a isso decisões arquiteturais e de design de software, testes automatizados, integração de sistemas e a coisa toda vai ficando mais complexa.
Entre todos os pequenos detalhes que deixamos passar, é comum esquecermos de definir a forma como identificar uma entidade dentro do nosso modelo orientado a objetos, ou seja, sua identidade. Definir de forma incorreta ou simplesmente não definir a identidade de uma entidade pode trazer problemas nas lógicas de negócio da aplicação e causar erros nos frameworks que usamos no projeto.
Por exemplo, imagine a seguinte entidade de negócio mapeada com a JPA na sua aplicação:
@Entity
public class Cliente {
@Id
private Integer id;
private String nome;
private String cpf;
// getters e setters
}
Por se tratar de uma entidade persistível da JPA, ela representa uma tabela no banco de dados. Para que o banco de dados identifique de forma única cada tupla na tabela ele se utiliza da chave primária (PK). De forma similar, a JPA nos obriga a mapear um identificador único na entidade através da anotação @Id
, pois a JPA precisa ter conhecimento de como diferenciar uma entidade de outra dentro do contexto de persistência e no banco de dados.
Por esse motivo, ao buscarmos pela primeira vez uma entidade por id usando a EntityManager
ela será consultada do banco e será colocada no cache de primeiro nível; ao buscarmos pela segunda vez esta mesma entidade, ela será obtida do cache e não mais do banco de dados:
Cliente rafael = this.entityManager.find(Cliente.class, 7); // vai no banco
// ...
Cliente rponte = this.entityManager.find(Cliente.class, 7); // vai no cache
Tanto a JPA quanto o banco de dados sabem como identificar uma entidade dentro do sistema, o que pode parecer suficiente a primeira vista, mas a verdade é que não é!
Para entender melhor o que estou querendo dizer, imagine que precisamos implementar uma regra de negócio no sistema para remover um cliente inadimplente de uma loja. Esta regra poderia ser implementada de forma orientada a objetos como abaixo:
@Transactional
public void tornaClienteAdimplente(Integer id) {
Cliente devedor = new Cliente();
devedor.setId(id); // definimos o id
Loja loja = this.lojaDao.busca(MATRIZ);
List<Cliente> inadimplentes = loja.getClientesInadimplentes();
boolean removido = inadimplentes.remove(devedor);
if (removido) {
// envia email de notificacao
}
this.lojaDao.atualiza(loja);
}
Apesar de termos definido o id da entidade corretamente e termos certeza que o cliente existe na loja, o cliente não será removido da coleção. O problema é que embora a JPA tenha ciência que o atributo id
representa a identidade da nossa entidade, o nosso modelo orientado a objetos não sabe!
Uma regra importante ao trabalhar no mundo orientado a objetos é que, toda entidade deve ter uma identidade. Essa identidade não necessariamente precisa ser a PK da tabela no banco, mas é comum que ela seja nas entidades mais simples.
Para que seja possível remover uma instância de Cliente
da coleção, precisamos primeiramente definir sua identidade de negócio (business key), que no nosso caso, é o próprio id
da entidade. Após esta definição, precisamos agora sobrescrever dois métodos importantes de um objeto Java, os métodos equals
e hashCode
, como no código abaixo:
@Entity
public class Cliente {
// ...
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Cliente other = (Cliente) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}
Estes dois métodos são bastante utilizados pela API de Collections do Java, em operações para adicionar, remover ou buscar itens da coleção. Quando não sobrescrevemos os métodos equals
e hashCode
, a comparação entre objetos ocorre através do endereço de memória, o que na maioria das vezes não é o que queremos!
Definir corretamente a identidade de uma entidade é um dos passos mais importantes ao modelarmos nossas classes de negócio. Não levar isso a sério pode acarretar problemas nas nossas regras de negócio e em frameworks ou APIs que usamos na nossa aplicação.
A identidade nem sempre é a chave primária
É comum que para entidades mais simples a identidade seja a própria chave primária (PK). Mas em modelos de domínios mais complexos isso muda de figura e precisamos entender bem o negócio para definirmos apropriadamente a identidade das nossas entidades. Não é por acaso que abordagens de desenvolvimento como Domain-Driven Design (DDD) tem ganhado tanto interesse pela comunidade de desenvolvedores.
Por exemplo, dependendo do contexto a identidade da nossa entidade Cliente
poderia ser expressa através do seu atributo cpf
; noutros contextos ela poderia até ser representada pelo nome do cliente ou mesmo pelo telefone de contato. Em casos ainda mais complexos, a identidade pode ser definida por dois ou mais atributos da entidade. É o que chamamos de identidade de negócio, ou do inglês, business key.
O importante é que essa identidade seja única e imutável durante o ciclo de vida da entidade. Os demais atributos da entidade podem mudar, mas não sua identidade.
Boas práticas ao implementar equals
e hashCode
Ter um bom conhecimento do negócio e do contexto na qual nossas entidades estão envolvidas é a base para definirmos a identidade de maneira correta de cada uma delas. Porém ao trabalharmos com frameworks ORM nós precisamos ter alguns cuidados a mais para não termos problemas!
Dependendo do cenário, uma simples comparação dentro do método equals
pode não funcionar devido ao uso de proxies ou pior, relacionamentos LAZY não inicializados podem ser disparados a cada comparação de objetos, levando ao pior problema de performance destes frameworks: Select N+1.
Vale a pena ler o artigo sobre boas práticas ao implementar equals
e hashCode
na página dos desenvolvedores da JBoss. Você encontrará dicas valiosas que te pouparão de algumas dores de cabeça!
Enfim, ter a identidade das entidades bem definidas e os métodos equals
e hashCode
bem implementados na aplicação é algo que impacta diretamente em todo o sistema, desde as lógicas de negócio, entendimento do desenvolvedor até nos frameworks que usamos. Por esse motivo, este tema é abordado de forma prática em todos os nossos cursos.
E você, como tem implementado a identidade das suas entidades no seu modelo orientado a objetos?
Desenvolvedor e instrutor na TriadWorks