Giovanni Rozza
Giovanni Rozza20/06/2023 11:08
Compartilhe

Uso avançado do ModelMapper

  • #Spring Framework
  • #Java

Pessoal, gostaria de compartilhar um problema que acabei resolvendo com o ModelMapper, que é um framework de mapeamento de classes, muito usando junto com o Spring. Bom vou contextualizar antes de ir pro código.

Eu estava implementando uma API REST para atualização de uma tabela de restaurantes, que no meu código é modelado pela seguinte classe:

public class Restaurante 
{
  private Long id;
  private String nome;
  private BigDecimal taxaFrete;
  private Cozinha cozinha;
  private List<Produto> produtos = new ArrayList<Produto>();
  private OffsetDateTime dataCadastro;
  private OffsetDateTime dataAtualizacao;
  private Endereco endereco;
  private List<FormaPagamento> formasPagamento = new ArrayList<FormaPagamento>();
}

A classe Cozinha também é uma tabela, modelada pela classe abaixo:

public class Cozinha 
{
  private Long id;
  private String nome;
  private List<Restaurante> restaurantes = new ArrayList<>();
}

Como vcs já devem saber, o padrão MVC tem três layers, a VIEW ou CONTROLER, a camada de serviço SERVICE e a interface com o banco de dados que é a REPOSITORY. Uma boa prática é deixar as regras de negócio dentro da camada SERVICE, dessa forma toda manipulação de classes de apresentação da camada CONTROLLER eu evito fazer na camada SERVICE. No meu código para esse contexto eu tenho as seguintes classes na camada CONTROLLER:

public class RestauranteInput 
{
  private String nome;
  private BigDecimal taxaFrete;
  private CozinhaIdInput cozinha;

}

public class CozinhaIdInput 
{
  private Long id;
}

São as classes que eu mapeio o Json do corpo da requisição PUT e POST neste caso eu implemento somente os atributos necessários para eu atender a requisição. Por exemplo, não preciso passar o nome da cozinha no Json, somente o id dela. Note que a classe CozinhaInput tem somente o id da classe Cozinha. Foi esse pequeno detalhe que fez com que o uso convencional do ModelMapper não funcionasse.

Vamos ao código, a requisição PUT para eu atualizar a entidade restaurante é a seguinte:

@PutMapping("/{restauranteId}")

  public RestauranteDto atualizar(@PathVariable Long restauranteId,
          @RequestBody @Valid RestauranteInput restauranteInput) {

      Restaurante restaurante = restauranteService.buscarOuFalhar(restauranteId);
      restaurante.setCozinha(new Cozinha());
      modelMapper.map(restauranteInput,restaurante );

      try {

          return modelMapper.map(restauranteService.salvar(restaurante ), RestauranteDto.class);

      } catch (CozinhaNaoEncontradaException e) {

          throw new NegocioException(e.getMessage(), e);

      }

  }

No caso de eu tentar atualizar o id de Cozinha de um determinado restaurante exemplo de cozinha.id=1 para cozinha.id=3 A seguinte exception do Jpa é lançada

Resolved [org.springframework.orm.jpa.JpaSystemException: identifier of an instance of domain.model.Cozinha was altered from 1 to 3; nested exception is org.hibernate.HibernateException: identifier of an instance of domain.model.Cozinha was altered from 1 to 3]

O que acontece?

Ao invés de atualizar o atributo cozinha.id do objeto restaurante, o JPA tenta atualizar cozinha.id do objeto cozinha. Em outras palavras, em vez de atualizar a chave estrangeira cozinha_id da tabela Restaurante, ele tenta modificar a chave primária id da tabela Cozinha. Obviamente o JPA reclama e lança a exceção.

Como resolver? De duas formas, eu posso antes de fazer o map do objeto restauranteInput para restaurante, criar um novo objeto cozinha e atribuir via set para o objeto restaurante.

Assim:

 @PutMapping("/{restauranteId}")

  public RestauranteDto atualizar(@PathVariable Long restauranteId,
          @RequestBody @Valid RestauranteInput restauranteInput) {

      Restaurante restaurante = restauranteService.buscarOuFalhar(restauranteId);

      // Para evitar org.hibernate.HibernateException: identifier of an instance of 
      // domain.model.Cozinha was altered from 1 to 2	
      restaurante.setCozinha(new Cozinha());

      modelMapper.map(restauranteInput,restaurante );

      try {

          return modelMapper.map(restauranteService.salvar(restaurante ), RestauranteDto.class);

      } catch (CozinhaNaoEncontradaException e) {

          throw new NegocioException(e.getMessage(), e);

      }

  }

O ponto negativo é que vou ter que deixar um comentário explicando que não é para remover essa linha senão a exception é lançada e blá blá...pois sem o comentário o dev que pegar esse código não vai entender o porque daquela linha não existir né? Não é muito legal. Eu até abri um issue lá no github do ModelMapper explicando o problema. Mas depois eu pensei um pouco, não é um cenário complexo, nem exótico, se fosse um bug isso já teria sido visto. Bom, o problema não era ModelMapper, mas sim o entendimento mais profundo do framework. Fuçando um pouco na documentação do mapper, eu achei que existe sim uma maneira de trabalhar com mapeamentos mais complexos. Usando Converters

https://modelmapper.org/user-manual/converters/

Conversion to a destination type or property can be delegated to a Converter. Converters generally take the place of any implicit or explicit mappings between two types

Refatorando o código, usando Converters. Definimos uma função de conversão entre o atributo CozinhaInput e Cozinha da classe Restaurante, e adicionamos ao mapper essa função de conversão e pronto, o mapper agora atualiza de forma que o Jpa ira modificar a chave estrangeira cozinha_id da tabela Restaurante, não gerando mais a exception.

  @PutMapping("/{restauranteId}")
  public RestauranteDto atualizar(@PathVariable Long restauranteId,
          @RequestBody @Valid RestauranteInput restauranteInput) {
  
      Restaurante restaurante = restauranteService.buscarOuFalhar(restauranteId);
      
      // Define o conversor
      Converter<CozinhaIdInput, Cozinha> cozinhaConverter = new Converter<CozinhaIdInput, Cozinha>() {
          @Override
          public Cozinha convert(MappingContext<CozinhaIdInput, Cozinha> context) {
          	Cozinha cozinha = new Cozinha(); 
          	cozinha.setId(context.getSource().getId());
              return cozinha;
          }
      };

      // adicina o conversor ao bean modelMapper
      modelMapper.addConverter(cozinhaConverter, CozinhaIdInput.class, Cozinha.class);
      
      modelMapper.map(restauranteInput,restaurante );
      
      try {
          return modelMapper.map(restauranteService.salvar(restaurante ), RestauranteDto.class);
      
      } catch (CozinhaNaoEncontradaException e) {
          
          throw new NegocioException(e.getMessage(), e);
      }


  }
Compartilhe
Comentários (1)

MR

Marcos Romero - 23/10/2023 19:00

Este codigo e do curso especialista rest da https://www.algaworks.com


Sempre importante citar a fonte e dar o credito a quem de direito.


Ha espaco para todos e devemos nos respeitar sempre.