JAVA Learning

Hub de Conteúdo

DTO e Mappers

Padrões essenciais para transferência de dados em aplicações Spring Boot - Segurança, Performance e Isolamento

1. O que é DTO?

DTO significa Data Transfer Object. É um padrão de projeto que cria objetos específicos para transferir dados entre camadas de uma aplicação.

Um DTO é uma classe simples que contém apenas getters e setters, sem lógica de negócio. Seu propósito é encapsular e transportar dados.

Diferente de uma Entity (que representa um banco de dados), um DTO pode conter apenas os dados que o cliente precisa receber, evitando exposição desnecessária.

2. Por Que Usar DTO?

Segurança

Não exponha entidades do banco de dados diretamente. Um DTO controla quais dados são enviados ao cliente.

Isolamento de Camadas

A API não fica acoplada com a estrutura do banco de dados. Mudanças na Entity não afetam a resposta da API.

Performance

Envie apenas os dados necessários, reduzindo o tamanho das respostas JSON.

Flexibilidade

Forneça diferentes DTOs para diferentes endpoints sem modificar a Entity.

Validação

Adicione regras de validação específicas para entrada de dados sem afetar a Entity.

3. Exemplo Básico: Entity vs DTO

Entity - Representa o Banco de Dados:

java
import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "usuarios")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Usuario {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String nome;
    private String email;
    
    @Column(name = "senha_hash")
    private String senhaHash;  // Nunca deve ser enviado ao cliente!
    
    @Column(name = "data_criacao")
    private LocalDateTime dataCriacao;
    
    @Column(name = "cpf")
    private String cpf;  // Informação sensível
    
    @Column(name = "ativo")
    private Boolean ativo;
    
    @OneToMany(mappedBy = "usuario")
    private List pedidos;  // Relacionamento complexo
}

DTO - Apenas Dados Necessários:

java
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UsuarioDTO {
    private Long id;
    private String nome;
    private String email;
    
    // Nota: senhaHash NÃO está aqui
    // Nota: cpf NÃO está aqui
    // Nota: pedidos NÃO está aqui
}

Nunca Exponha:

  • Senhas ou hashes de senhas
  • Informações sensíveis (CPF, CNPJ, etc)
  • Tokens de autenticação
  • Relacionamentos complexos desnecessários
  • Campos internos da aplicação

4. Request DTO - Receber Dados

Use DTOs para validar e receber dados do cliente. Isso protege sua Entity e permite validação específica.

Exemplo: Criar Novo Usuário

java
import jakarta.validation.constraints.*;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CriarUsuarioDTO {
    
    @NotBlank(message = "Nome é obrigatório")
    @Size(min = 3, max = 100)
    private String nome;
    
    @NotBlank(message = "Email é obrigatório")
    @Email(message = "Email inválido")
    private String email;
    
    @NotBlank(message = "Senha é obrigatória")
    @Size(min = 8, message = "Senha deve ter no mínimo 8 caracteres")
    private String senha;
}

Usar no Controller:

java
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
    
    private final UsuarioService service;
    
    public UsuarioController(UsuarioService service) {
        this.service = service;
    }
    
    @PostMapping
    public ResponseEntity criar(
        @Valid @RequestBody CriarUsuarioDTO dto) {
        
        // O Spring valida automaticamente
        // Se inválido, retorna 400 Bad Request
        
        UsuarioDTO novoUsuario = service.criar(dto);
        return ResponseEntity.status(201).body(novoUsuario);
    }
}

5. O que é Mapper?

Um Mapper é responsável por converter dados de um tipo para outro. Transforma Entity em DTO e vice-versa.

Responsabilidades do Mapper:

  • Entity → DTO (resposta para cliente)
  • DTO → Entity (receber dados do cliente)
  • Tratamento de valores nulos
  • Conversões de tipos quando necessário
  • Lógica de transformação de dados

Exemplo Básico - Mapper Manual:

java
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class UsuarioMapper {
    
    private final PasswordEncoder passwordEncoder;
    
    public UsuarioMapper(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    
    // Entity para DTO (resposta)
    public UsuarioDTO toDTO(Usuario entity) {
        if (entity == null) return null;
        
        return new UsuarioDTO(
            entity.getId(),
            entity.getNome(),
            entity.getEmail()
        );
    }
    
    // DTO para Entity (criar novo)
    public Usuario toEntity(CriarUsuarioDTO dto) {
        if (dto == null) return null;
        
        Usuario usuario = new Usuario();
        usuario.setNome(dto.getNome());
        usuario.setEmail(dto.getEmail());
        usuario.setSenhaHash(passwordEncoder.encode(dto.getSenha()));
        usuario.setDataCriacao(LocalDateTime.now());
        usuario.setAtivo(true);
        
        return usuario;
    }
    
    // DTO para Entity (atualizar existente)
    public void atualizarEntity(AtualizarUsuarioDTO dto, Usuario entity) {
        if (dto == null || entity == null) return;
        
        if (dto.getNome() != null) {
            entity.setNome(dto.getNome());
        }
        if (dto.getEmail() != null) {
            entity.setEmail(dto.getEmail());
        }
    }
}

6. Mapper com Listas

Muitas vezes você precisa converter listas inteiras de Entities em DTOs.

Métodos Úteis para Coleções:

java
import java.util.stream.Collectors;

@Component
public class UsuarioMapper {
    
    // Converter uma lista de Entities para DTOs
    public List toDTOList(List entities) {
        if (entities == null || entities.isEmpty()) {
            return new ArrayList<>();
        }
        
        return entities.stream()
            .map(this::toDTO)
            .collect(Collectors.toList());
    }
    
    // Converter uma página de Entities para DTOs
    public Page toDTOPage(Page page) {
        return page.map(this::toDTO);
    }
    
    // Método auxiliar para DTO
    public UsuarioDTO toDTO(Usuario entity) {
        if (entity == null) return null;
        
        return new UsuarioDTO(
            entity.getId(),
            entity.getNome(),
            entity.getEmail()
        );
    }
}

Usando no Service:

java
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class UsuarioService {
    
    private final UsuarioRepository repository;
    private final UsuarioMapper mapper;
    
    public UsuarioService(UsuarioRepository repository, UsuarioMapper mapper) {
        this.repository = repository;
        this.mapper = mapper;
    }
    
    public Page listarTodos(Pageable pageable) {
        Page usuarios = repository.findAll(pageable);
        return mapper.toDTOPage(usuarios);
    }
    
    public List buscarPorNome(String nome) {
        List usuarios = repository.findByNomeContaining(nome);
        return mapper.toDTOList(usuarios);
    }
}

7. MapStruct - Mapper Automático

MapStruct é uma biblioteca que gera Mappers automaticamente em tempo de compilação, sem reflexão.

Configuração no pom.xml:

xml
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>

<dependencies>
    <!-- MapStruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Criar Mapper com MapStruct:

java
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import java.util.List;

@Mapper(
    componentModel = "spring",
    unmappedTargetPolicy = ReportingPolicy.IGNORE
)
public interface UsuarioMapperMapStruct {
    
    // Entity para DTO (simples)
    UsuarioDTO toDTO(Usuario entity);
    
    // Lista de Entities para DTOs
    List toDTOList(List entities);
    
    // DTO para Entity (com mapeamento customizado)
    @Mapping(target = "senhaHash", 
        expression = "java(org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(dto.getSenha()))")
    @Mapping(target = "dataCriacao", 
        expression = "java(java.time.LocalDateTime.now())")
    @Mapping(target = "ativo", constant = "true")
    Usuario toEntity(CriarUsuarioDTO dto);
    
    // Atualizar Entity existente
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "senhaHash", ignore = true)
    @Mapping(target = "dataCriacao", ignore = true)
    void atualizarEntity(AtualizarUsuarioDTO dto, @MappingTarget Usuario entity);
}

Vantagens do MapStruct:

  • Gera código em tempo de compilação (sem reflexão)
  • Muito mais rápido que mapeamento manual
  • Type-safe (verificado em tempo de compilação)
  • Permite customizações com @Mapping
  • Integração natural com Spring
  • Zero overhead em runtime

8. Exemplo Completo: API de Usuários

1. DTOs:

java
// Response DTO
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UsuarioDTO {
    private Long id;
    private String nome;
    private String email;
}

// Request DTO - Criar
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class CriarUsuarioDTO {
    @NotBlank
    @Size(min = 3, max = 100)
    private String nome;
    
    @NotBlank
    @Email
    private String email;
    
    @NotBlank
    @Size(min = 8)
    private String senha;
}

// Request DTO - Atualizar
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AtualizarUsuarioDTO {
    @Size(min = 3, max = 100)
    private String nome;
    
    @Email
    private String email;
}

2. Mapper:

java
@Mapper(componentModel = "spring")
public interface UsuarioMapper {
    UsuarioDTO toDTO(Usuario entity);
    List toDTOList(List entities);
    
    @Mapping(target = "senhaHash", ignore = true)
    Usuario toEntity(CriarUsuarioDTO dto);
    
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "senhaHash", ignore = true)
    void update(AtualizarUsuarioDTO dto, @MappingTarget Usuario entity);
}

3. Service:

java
@Service
public class UsuarioService {
    
    private final UsuarioRepository repository;
    private final UsuarioMapper mapper;
    private final PasswordEncoder passwordEncoder;
    
    public UsuarioService(UsuarioRepository repository, 
                         UsuarioMapper mapper,
                         PasswordEncoder passwordEncoder) {
        this.repository = repository;
        this.mapper = mapper;
        this.passwordEncoder = passwordEncoder;
    }
    
    public UsuarioDTO criar(CriarUsuarioDTO dto) {
        Usuario entity = mapper.toEntity(dto);
        entity.setSenhaHash(passwordEncoder.encode(dto.getSenha()));
        Usuario salvo = repository.save(entity);
        return mapper.toDTO(salvo);
    }
    
    public UsuarioDTO buscarPorId(Long id) {
        Usuario usuario = repository.findById(id)
            .orElseThrow(() -> new NotFoundException("Usuário não encontrado"));
        return mapper.toDTO(usuario);
    }
    
    public Page listarTodos(Pageable pageable) {
        Page page = repository.findAll(pageable);
        return page.map(mapper::toDTO);
    }
    
    public UsuarioDTO atualizar(Long id, AtualizarUsuarioDTO dto) {
        Usuario usuario = repository.findById(id)
            .orElseThrow(() -> new NotFoundException("Usuário não encontrado"));
        
        mapper.update(dto, usuario);
        Usuario atualizado = repository.save(usuario);
        return mapper.toDTO(atualizado);
    }
    
    public void deletar(Long id) {
        if (!repository.existsById(id)) {
            throw new NotFoundException("Usuário não encontrado");
        }
        repository.deleteById(id);
    }
}

4. Controller:

java
@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
    
    private final UsuarioService service;
    
    public UsuarioController(UsuarioService service) {
        this.service = service;
    }
    
    @PostMapping
    public ResponseEntity criar(
        @Valid @RequestBody CriarUsuarioDTO dto) {
        UsuarioDTO resultado = service.criar(dto);
        return ResponseEntity.status(201).body(resultado);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity buscarPorId(@PathVariable Long id) {
        UsuarioDTO usuario = service.buscarPorId(id);
        return ResponseEntity.ok(usuario);
    }
    
    @GetMapping
    public ResponseEntity> listarTodos(Pageable pageable) {
        Page usuarios = service.listarTodos(pageable);
        return ResponseEntity.ok(usuarios);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity atualizar(
        @PathVariable Long id,
        @Valid @RequestBody AtualizarUsuarioDTO dto) {
        UsuarioDTO atualizado = service.atualizar(id, dto);
        return ResponseEntity.ok(atualizado);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity deletar(@PathVariable Long id) {
        service.deletar(id);
        return ResponseEntity.noContent().build();
    }
}

9. Casos de Uso Comuns

Caso 1: DTO Parcial - Listagem

Às vezes você quer menos campos na listagem que na visualização individual:

java
// DTO com todos os dados
@Getter
@Setter
public class UsuarioDTO {
    private Long id;
    private String nome;
    private String email;
    private LocalDateTime dataCriacao;
}

// DTO simplificado para listagem
@Getter
@Setter
public class UsuarioResumoDTO {
    private Long id;
    private String nome;
    private String email;
}

Caso 2: DTO com Relacionamentos

Incluir dados relacionados sem expor a Entity completa:

java
@Getter
@Setter
public class PedidoDTO {
    private Long id;
    private LocalDateTime data;
    private BigDecimal total;
    
    // Dados do usuário relacionado (simplificado)
    private UsuarioResumoDTO usuario;
    
    // Lista de itens do pedido
    private List itens;
}

@Getter
@Setter
public class ItemPedidoDTO {
    private Long id;
    private String nomeProduto;
    private Integer quantidade;
    private BigDecimal precoUnitario;
}

Caso 3: DTO com Validação Customizada

java
@Getter
@Setter
public class CriarProdutoDTO {
    
    @NotBlank
    @Size(min = 3, max = 100)
    private String nome;
    
    @NotBlank
    private String descricao;
    
    @NotNull
    @DecimalMin("0.01")
    private BigDecimal preco;
    
    @NotNull
    @Min(0)
    private Integer estoque;
    
    @Email
    private String emailFornecedor;
}

10. Boas Práticas com DTO e Mappers

✓ FAÇA:

  • Separe Request e Response DTOs
  • Use validação Bean Validation (@Valid)
  • Nunca envie senhas ou dados sensíveis
  • Use Mappers para conversão consistente
  • Crie DTOs específicos para cada caso de uso
  • Documente quais campos são opcionais
  • Use tipos específicos (LocalDateTime, BigDecimal, etc)
  • Implemente equals() e hashCode() se necessário

✗ EVITE:

  • Usar Entity diretamente como response
  • Expor detalhes internos do banco de dados
  • Mappers sem estrutura ou padrão
  • DTOs com lógica de negócio
  • DTOs muito genéricos que servem para tudo
  • Enviar dados desnecessários (performance)
  • Mappers sem testes
  • Esquecer validação em Request DTOs

11. Erros Comuns

❌ Erro 1: Expor a Entity Diretamente

java
// ERRADO - Expõe tudo, inclusive senha!
@GetMapping("/{id}")
public ResponseEntity buscar(@PathVariable Long id) {
    Usuario usuario = repository.findById(id).orElseThrow();
    return ResponseEntity.ok(usuario);
}

// CORRETO - Retorna DTO
@GetMapping("/{id}")
public ResponseEntity buscar(@PathVariable Long id) {
    Usuario usuario = repository.findById(id).orElseThrow();
    return ResponseEntity.ok(mapper.toDTO(usuario));
}

❌ Erro 2: DTO com Lógica de Negócio

java
// ERRADO - DTO com lógica
public class UsuarioDTO {
    private String nome;
    
    public String getNomeFormatado() {
        return nome.toUpperCase();
    }
}

// CORRETO - DTO simples, lógica no Service
public class UsuarioDTO {
    private String nome;
}

❌ Erro 3: Mapper sem Validação

java
// ERRADO - Sem tratamento de nulo
public UsuarioDTO toDTO(Usuario entity) {
    return new UsuarioDTO(
        entity.getId(),  // Pode dar NullPointerException
        entity.getNome(),
        entity.getEmail()
    );
}

// CORRETO - Valida antes
public UsuarioDTO toDTO(Usuario entity) {
    if (entity == null) return null;
    
    return new UsuarioDTO(
        entity.getId(),
        entity.getNome(),
        entity.getEmail()
    );
}

12. Estrutura de Projeto Recomendada

plaintext
src/main/java/com/example/projeto/
├── controller/
│   └── UsuarioController.java
├── service/
│   └── UsuarioService.java
├── repository/
│   └── UsuarioRepository.java
├── mapper/
│   ├── UsuarioMapper.java
│   └── PedidoMapper.java
├── dto/
│   ├── request/
│   │   ├── CriarUsuarioDTO.java
│   │   └── AtualizarUsuarioDTO.java
│   └── response/
│       ├── UsuarioDTO.java
│       └── UsuarioResumoDTO.java
├── entity/
│   ├── Usuario.java
│   └── Pedido.java
└── config/
    └── MapperConfig.java

13. Resumo - DTO vs Mapper

DTO (Data Transfer Object)

  • Classe simples com getters/setters
  • Transfere dados entre camadas
  • Sem lógica de negócio
  • Protege a Entity
  • Pode ter validação específica

Mapper

  • Converte Entity em DTO e vice-versa
  • Centraliza lógica de transformação
  • Pode ser manual ou automático (MapStruct)
  • Trata valores nulos e conversões
  • Facilita testes e manutenção

Conclusão

DTO e Mappers são padrões essenciais em APIs Spring Boot modernas.

Eles garantem:

  • ✓ Segurança - Dados sensíveis protegidos
  • ✓ Isolamento - Entity independente da API
  • ✓ Performance - Apenas dados necessários
  • ✓ Flexibilidade - Múltiplos DTOs para casos diferentes
  • ✓ Manutenibilidade - Código organizado e consistente

Use sempre DTOs em suas APIs REST e padronize as conversões com Mappers! 🚀