3.🔍 Criando e validando modelos de Dados
Para continuarmos a abordar os conceitos do desenvolvimento de APIS Rest com Spring Boot, relembremos os Desafios 1 e 2 da Aula 03.
O Desafio 1 tinha como objetivo criar uma nova entidade Address relacionada bidirecionalmente com Contact, implementar um repositório AddressRepository e criar um controlador AddressController para gerenciar endereços. Também era necessário criar um endpoint GET /api/contacts/{id}/addresses para listar todos os endereços associados a um contato específico.
Já o Desafio 2 exigia a adição de validações na entidade Contact utilizando a anotação @Valid. As regras incluem: nome não pode estar vazio, email deve ter um formato válido (@Email) e telefone deve ter entre 8 e 15 caracteres. A API deve retornar respostas adequadas para entradas inválidas.
Para facilitar a explicação e evitar repetição desnecessário, vamos implementar código que atenda ambos os desafios. Para conseguir implementar esses desafios, entretanto, temos que entender dois conceitos fundamentais: relacionamento entre Entidades com uso da JPA e a Validação de dados com a Jakarta Bean Validation.
3.1 Uma breve introdução ao relacionamento entre Entidades
Relacionamentos de entidades na JPA são fundamentais para representar como os dados interagem entre si no banco de dados. A JPA (Java Persistence API) fornece suporte para mapeamento de associações entre entidades usando anotações específicas que permitem definir os tipos de relacionamento que podem ocorrer entre essas entidades. Os relacionamentos mais comuns são OneToOne, OneToMany, ManyToOne e ManyToMany. Quando mapeamos uma relação OneToOne, indicamos que uma entidade está associada exclusivamente a outra entidade. Para isso, utilizamos a anotação @OneToOne, que pode ser configurada com o atributo mappedBy para especificar o lado proprietário da relação.
No relacionamento ManyToOne, uma entidade pode estar associada a várias instâncias de outra entidade, mas a relação inversa geralmente é OneToMany, ou seja, um único objeto de uma entidade pode ter várias referências de outra entidade. Por exemplo, em uma aplicação de contatos e endereços, cada endereço está associado a um único contato, mas um contato pode ter múltiplos endereços. Isso é representado na JPA com @ManyToOne na classe Address e @OneToMany na classe Contact. Além disso, é necessário configurar adequadamente as anotações @JoinColumn para definir a chave estrangeira que conecta as tabelas.
O relacionamento ManyToMany é usado quando múltiplas instâncias de uma entidade podem estar associadas a múltiplas instâncias de outra. Esse tipo de relacionamento geralmente é mapeado por meio de uma tabela intermediária que contém as chaves estrangeiras de ambas as entidades relacionadas. Na JPA, usamos a anotação @ManyToMany para definir esse tipo de relacionamento, e podemos usar a propriedade mappedBy para especificar o lado não proprietário da associação.
É importante entender que os relacionamentos na JPA podem ser configurados para serem unidirecionais ou bidirecionais. Uma associação unidirecional significa que apenas uma entidade conhece a existência da outra, enquanto uma associação bidirecional permite que ambas as entidades se conheçam mutuamente, o que é útil quando queremos acessar dados relacionados de forma mais natural e eficiente. Quando implementamos um relacionamento bidirecional, precisamos garantir que a sincronização entre os dois lados do relacionamento seja tratada adequadamente. Isso é feito configurando o atributo mappedBy no lado que não é o proprietário da relação, informando à JPA qual entidade é responsável pelo gerenciamento do relacionamento.
Além disso, ao configurar relacionamentos, é fundamental definir adequadamente o comportamento de cascade e orphanRemoval, que especificam se operações realizadas em uma entidade principal devem ser propagadas para as entidades relacionadas. Por exemplo, ao excluir um contato, podemos querer que todos os endereços associados também sejam removidos automaticamente, o que é configurado com o uso da propriedade cascade = CascadeType.ALL e orphanRemoval = true na anotação @OneToMany.
3.2 Validação de dados de forma simplificada
A validação de dados é um processo essencial para garantir que as informações fornecidas por usuários ou sistemas externos sejam corretas, seguras e adequadas antes de serem processadas ou armazenadas. A linguagem Java fornece várias maneiras de realizar validações, mas a abordagem mais comum e eficiente é por meio das anotações de validação fornecidas pelo pacote Jakarta Bean Validation (anteriormente conhecido como Java EE Bean Validation) e integrado ao Spring Framework por meio da biblioteca Hibernate Validator. Esse mecanismo oferece uma maneira declarativa e robusta para validar dados de entrada sem necessidade de escrever código complexo para cada regra de validação.
O Jakarta Bean Validation utiliza anotações que são aplicadas diretamente sobre os atributos das classes, facilitando o processo de validação e mantendo o código organizado e legível. As anotações mais comuns incluem @NotNull, que garante que o valor de um campo não pode ser nulo; @NotBlank, que assegura que um campo de texto não é vazio ou apenas contém espaços em branco; @Size, que define o tamanho mínimo e máximo permitido para uma string ou coleção; @Pattern, que permite especificar uma expressão regular para validação de formato; e @Email, que verifica se um dado fornecido corresponde a um formato válido de endereço de e-mail. Além dessas, existem várias outras anotações específicas que podem ser usadas dependendo das necessidades da aplicação, podendo ser consultadas em Hibernate Validator - Definição de Restrições (Documentação Oficial)
A configuração da validação no Spring Boot é bastante simples e, geralmente, basta adicionar a dependência do Hibernate Validator no arquivo pom.xml do projeto.
Ao longo da disciplina vamos, evidentemente, utilizar as validações para o desenvolvimento de APIs com o Spring Boot. Nesse contexto, o uso da anotação @Valid nos controladores REST é fundamental para ativar o mecanismo de validação automática dos dados recebidos. Quando um cliente envia dados para o servidor, o Spring automaticamente verifica se os dados atendem aos critérios estabelecidos pelas anotações de validação na entidade ou no DTO (Data Transfer Object, padrão que abordaremos nas próximas aulas) e, caso algum critério não seja satisfeito, uma exceção é lançada, normalmente a MethodArgumentNotValidException. Essa exceção precisa ser tratada por um mecanismo de tratamento de erros personalizado, como o GlobalExceptionHandler que implementamos anteriormente, para que a aplicação possa responder de forma adequada e amigável ao cliente, geralmente retornando um código de status HTTP 400 (Bad Request) junto com uma mensagem explicativa. Ou seja, a partir daí basta aplicar as anotações necessárias nos atributos das entidades ou DTOs e não se esquecer utilizar o @Valid nos métodos controladores. Além disso, o Spring Boot permite definir mensagens de erro personalizadas para cada tipo de validação, tornando as respostas da API mais claras e amigáveis para os consumidores do serviço.
Além das validações padrão fornecidas pela especificação Jakarta Bean Validation, é possível definir validações personalizadas quando os requisitos do sistema são mais específicos. Para isso, criamos uma anotação customizada e implementamos um validador que implementa a interface ConstraintValidator. Esse mecanismo permite que os desenvolvedores criem suas próprias regras de validação e as apliquem a campos ou classes inteiras, mantendo a flexibilidade e a escalabilidade da aplicação. Futuramente exploraremos essa possibilidade na disciplina.
É importante destacar que a validação de dados deve ser realizada tanto no lado do cliente quanto no lado do servidor. Embora bibliotecas de frontend como React e Angular forneçam recursos para validação de formulários, a validação no servidor é indispensável para garantir a segurança e integridade dos dados, pois os clientes podem ser manipulados ou burlados por usuários mal-intencionados. Por isso, as validações feitas no servidor são a principal linha de defesa contra dados inválidos ou maliciosos. Lembrem-se: temos controle real apenas sobre o lado do servidor. O lado cliente é do cliente!
Vamos agora verificar o código que implementa os Desafios 1 e 2 da Aula 03.