9 de março de 2019

@Transient com JPA: 3 coisas que você não deve fazer!

Usar @Transient com JPA pode parecer fácil, mas existem 3 coisas que se você fizer, podem deixar seu código confuso. Aprenda agora como utilizar da melhor forma, escrever um código de alta qualidade e entregar seu projeto no prazo!

Por que JPA?

Como tudo que vamos utilizar na programação, temos que entender o conceito, para não termos dor de cabeça mais tarde. Utilizamos JPA basicamente para fazer mapeamento objeto-relacional. Em geral, queremos estabelecer uma relação entre nossos objetos e uma base de dados relacional. O que fazemos é basicamente isso:

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
}

@Transient com JPA

A partir do momento que você anota sua classe com @Entity, o JPA espera que todos os atributos dela estejam relacionados a uma coluna do banco. Os atributos anotados com @Transient são uma exceção a essa regra. De certa forma, essa anotação funciona como uma ferramenta para burlar o conceito básico do JPA. A grande questão é que cada pequena diferença que você cria entre sua tabela e sua classe traz mais complexidade para seu mapeamento.

Por isso, usar @Transient com JPA pode ser um sinal de que tem algo cheirando mal no seu código e você não está percebendo. Ao mesmo tempo, nem sempre utilizar é necessariamente errado. Vamos ver alguns cenários e suas alternativas.


1. Usar @Transient com JPA para definir um tipo de operação

Um cenário comum. Você está na classe onde executa suas regras de negócio e recebe um objeto de Usuario. Você precisa saber se é para criar um novo usuário, ou atualizar um existente, e pra isso cria um atributo transient.

public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Transient
	private Boolean atualizar; // não faça isso
}
public class CadastroUsuarioService {
	public void salvarUsuario(Usuario usuario) {
		if (usuario.getAtualizar()) {
			// atualiza dados do usuario
		} else {
			// cadastra novo usuario
		}
	}
}

Se você escreveu um código assim, é uma boa hora para refatorar. O atributo atualizar não tem absolutamente nada a ver com a classe Usuario. Então a coerência do seu código foi para o espaço. Você criou um forte acoplamento entre o comportamento do método salvarUsuarioe uma flag que está dentro de uma entidade. Além disso, essa entidade só deveria ter atributos relacionados a ela.

Nesse caso, provavelmente seu fluxo de negócio para um novo cadastro e para uma atualização são diferentes. Até mesmo na camada de apresentação, raramente a página em que um usuário se cadastra é a mesma onde ele atualiza seus dados. Então não faz sentido vincular isso no seu negócio. Seria muito melhor ter dois métodos separados para cada operação e uma classe Usuario limpa.

@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
}
public class CadastroUsuarioService {
	public void cadastrarNovoUsuario(Usuario usuario) {
		// cadastra novo usuario
	}
	public void atualizarUsuario(Usuario usuario) {
		// atualiza dados do usuario
	}
}

Além disso, se você está recebendo a entidade de uma chamada externa ou da sua camada de apresentação, considere utilizar o padrão DTO.


2. Usar @Transient com JPA para passar um dado para validação

Outro cenário em que você pode considerar usar @Transient com JPA é quando percebe que precisa passar um dado apenas para validação, mas não para persistência. Vejamos.

@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Column
	private String email;
	@Transient
	private String confirmacaoEmail; // preocupante
	@Transient
	private Boolean aceitouTermosDeUso; // preocupante	
}

Nesse caso temos dois exemplos. O atributos acima foram criados apenas para que sua camada de negócio faça alguma validação. Provavelmente seria algo mais ou menos assim:

public class CadastroUsuarioService {
	public void cadastrarNovoUsuario(Usuario usuario) {
		if (!usuario.getAceitouTermosDeUso()) {
			// lança exceção de termos de uso
		}
		if (!usuario.getEmail().equals(usuario.getConfirmacaoEmail())) {
			// lança exceção de emails diferentes
		}
	}
}

Esse código tem alguns problemas:

  • Primeiro, esses dois atributos não tem de fato a ver com a entidade que está sendo persistida.
  • Depois, essas validações não precisam estar tão a fundo no backend. A confirmação do email serve para impedir um possível erro de digitação, algo que está muito mais próximo da camada de apresentação. A aceitação de termos de uso serve apenas para garantir que o usuário acessou os termos e/ou clicou em um checkbox, algo que não muda o comportamento dessa parte da aplicação.
  • E por último, quem quer que esteja chamando esse método pode burlar as validações. A camada de apresentação poderia simplesmente copiar o conteúdo de email para confirmacaoEmail, e marcar um true em aceitouTermosDeUso. Depois disso, faria a chamada. Sua validação só serviria para dar uma falsa sensação de segurança.

Qual seria então a alternativa? Depende muito do seu cenário:

  • Se você usa serviços rest, deixe que apresentação faça esse trabalho. Se quiser muito manter os atributos para lembrar ao usuário do seu serviço de fazer essas validações, considere receber uma DTO ao invés da sua entidade.
  • Se você usa algum framework como JSF, simplesmente faça essa validação no seu controller, sem criar atributos na classe.
  • No pior dos casos, se você ainda acredita ter alguma razão para fazer isso, utilize parâmetros separados, deixando sua entidade fora disso:
public class CadastroUsuarioService {
	public void cadastrarNovoUsuario(Usuario usuario, String confirmacaoEmail, Boolean aceitouTermosDeUso) {
		if (!aceitouTermosDeUso) {
			// lança exceção de termos de uso
		}
		if (!usuario.getEmail().equals(confirmacaoEmail)) {
			// lança exceção de emails diferentes
		}
	}
}

3. Usar @Transient com JPA para armazenar outras entidades temporariamente

Outro caso em que usar @Transient com JPA costuma ser utilizado, é para armazenar uma lista de outras entidades. Por exemplo:

@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Column
	private String email;
	@Transient
	private List<Endereco> enderecos; // cuidado
}
@Entity
public class Endereco {
	@Id
	private Long id;
	@Column
	private String rua;
	@Column
	private String numero;
	@ManyToOne
	private Usuario usuario;
}

Por vários motivos que não são o foco deste artigo, muitas vezes mapeamentos bidirecionais não são recomendados. Isso faz com que alguns programadores criem esse atributo transient para preencher quando necessário. Eu considero esse caso um problema “menor”, pois conceitualmente o usuário realmente tem uma lista de endereços.

O grande problema aqui é quando essa lista é preenchida. Ao fazer isso, geralmente você está criando um acoplamento entre o código que preencheu a lista, e o código que utilizou. Minhas sugestões:

  • Se o consumidor da entidade Usuario for uma fronteira externa ao sistema, ou uma camada de apresentação, você pode utilizar uma DTO.
  • Se o consumidor for outra parte interna do sistema que tem acesso ao banco de dados, deixe que ela mesma recupere essa informação.
  • Considere utilizar um mapeamento bidirecional com FetchType.LAZY, e avalie quais seriam os impactos no seu código:
@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Column
	private String email;
	@OneToMany(mappedBy = "usuario", fetch = FetchType.LAZY)
	private List<Endereco> enderecos; // use conscientemente
}
  • Raramente não será possível utilizar uma das alternativas citadas acima. Caso você ainda acredite que não tem outra solução, preencha essa lista apenas em métodos que explicitamente fazem isso, e documente bem na classe Usuario:
@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Column
	private String email;
	@Transient
	private List<Endereco> enderecos; // cuidado
	/**
	 * Os endereços são preenchidos apenas por operações específicas do sistema, onde seria difícil recuperá-los de outra forma.
	 */
	public List<Endereco> getEnderecos() {
		return enderecos;
	}
}
	public List<Usuario> recupereUsuarioComEnderecos(Long idUsuario) {
		Usuario usuario = db.findById(idUsuario); 
		List<Endereco> enderecos = db.findByIdUsuario(idUsuario);
		usuario.setEnderecos(enderecos);
		return usuario;
	}

Uso de DTO

Algumas vezes nesse artigo citei o uso do padrão DTO. Ele foi criado especificamente para isso: transferir informações entre subsistemas, ou fronteiras claras da aplicação. A única razão válida que vejo para não utilizá-lo é o aumento na quantidade de código. O chamado boilerplate code. Porém, ao utilizá-lo, você evita usar @Transient com JPA.

Além disso, você não precisa escrever todo o código para converter uma entidade para DTO. Existem muitas ferramentas para ajudar nesse mapeamento, que facilitam esse trabalho e suavizam o impacto no tamanho do código. Você pode encontrar uma lista dessas ferramentas aqui, e muitas outras na internet.


Quando usar @Transient com JPA

Bom, então não tem nenhum caso bom para se utilizar @Transient? Na verdade, tem. Um cenário onde essa anotação é muito útil, é para fazer cache de atributos derivados de outros atributos. Utilizando a mesma classe Usuario, é possível fazer algo do tipo:

@Entity
public class Usuario {
	@Id
	private Long id;
	@Column
	private String primeiroNome;
	@Column
	private String sobrenome;
	@Column
	private String email;
	@Transient
	private String nomeCompleto; // bom uso de transient
	public String getNomeCompleto() {
		if (nomeCompleto == null) {
			nomeCompleto = primeiroNome + " " + sobrenome;
		}
		return nomeCompleto;
	}
}

É claro que esse cenário é extremamente simples, e talvez o aumento de complexidade não compense. Afinal, lembre-se que nesse caso você teria que atribuir null ao atributo nomeCompleto se sua classe for mutável e alguém alterar o primeiroNome ou sobrenome. Mas, em um cenário onde o ganho de performance pode ser considerável, é uma opção válida.


Parabéns! Agora você sabe usar @Transient com JPA, pode entregar seu projeto mais rápido e crescer na sua carreira.


E você? Conhece outras situações em que devemos ter cuidado com @Transient com JPA? Tem dicas de onde podemos usar sem problemas? Compartilhe também! Deixe um comentário!

Share