JavaRush /Blogue Java /Random-PT /API REST e validação de dados
Денис
Nível 37
Киев

API REST e validação de dados

Publicado no grupo Random-PT
Link para a primeira parte: API REST e próxima tarefa de teste Bem, nossa aplicação está funcionando, podemos obter algum tipo de resposta dela, mas o que isso nos dá? Não faz nenhum trabalho útil. Mal dito e feito, vamos implementar algo útil. Em primeiro lugar, vamos adicionar algumas novas dependências ao nosso build.gradle, elas serão úteis para nós:
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.springframework.boot:spring-boot-starter-validation'
E começaremos com os dados reais que devemos processar. Vamos voltar ao nosso pacote de persistência e começar a preencher a entidade. Como você lembra, deixamos ele existir com apenas um campo, e então o auto é gerado via `@GeneratedValue(strategy = GenerationType.IDENTITY)` Vamos relembrar as especificações técnicas do primeiro capítulo:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Temos campos suficientes pela primeira vez, então vamos começar a implementá-los. Os três primeiros campos não levantam dúvidas - são linhas comuns, mas o campo salarial já é sugestivo. Por que a linha real? No trabalho real isso também acontece, um cliente chega até você e diz - quero te enviar esse payload, e você processa. Você pode, claro, encolher os ombros e fazê-lo, pode tentar chegar a um acordo e explicar que é melhor transmitir os dados no formato exigido. Vamos imaginar que nos deparamos com um cliente inteligente e concordamos que é melhor transmitir números em formato numérico, e já que estamos falando de dinheiro, que seja o Dobro. O próximo parâmetro do nosso payload será a data de contratação, o cliente nos enviará no formato acordado: aaaa-mm-dd, onde y é responsável por anos, m por dias e d é esperado por dias - 2022- 08-12. O último campo no momento será uma lista de tarefas atribuídas ao cliente. Obviamente, Task é outra entidade em nosso banco de dados, mas ainda não sabemos muito sobre ela, então criaremos a entidade mais básica como fizemos com Employee antes. A única coisa que podemos assumir agora é que mais de uma tarefa pode ser atribuída a um funcionário, por isso aplicaremos a chamada abordagem Um-para-Muitos, uma proporção de um para muitos. Mais especificamente, um registro na tabela de funcionários pode corresponder a vários registros da tabela de tarefas . Também decidi adicionar um campo uniqueNumber, para que pudéssemos distinguir claramente um funcionário do outro. No momento nossa classe Employee está assim:
@Entity
@Data
@Accessors(chain = true)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    Long id;

    @NotBlank
    @Column(unique = true)
    private String uniqueNumber;
    private String firstName;
    private String lastName;
    private String department;
    private Double salary;
    private LocalDate hired;

    @OneToMany
    @JoinColumn(name = "employee_id")
    List<Task> tasks = new ArrayList<>();
}
A seguinte classe foi criada para a entidade Task:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Como eu disse, não veremos nada de novo em Task, também foi criado um novo repositório para esta classe, que é uma cópia do repositório para Employee - não vou dar, você mesmo pode criar por analogia. Mas faz sentido falar sobre a classe Employee. Como eu disse, adicionamos vários campos, mas apenas o último é de interesse agora - tarefas. Este é um List<Task> tasks , é imediatamente inicializado com um ArrayList vazio e marcado com várias anotações. 1. @OneToMany Como eu disse, esta será a nossa proporção de funcionários em relação às tarefas. 2. @JoinColumn - a coluna pela qual as entidades serão unidas. Neste caso, será criada uma coluna Employee_id na tabela Task apontando para o id do nosso funcionário; ela nos servirá como ForeighnKey Apesar da aparente sacralidade do nome, você pode nomear a coluna como quiser. A situação será um pouco mais complicada se você precisar usar não apenas um ID, mas algum tipo de coluna real; abordaremos esse assunto mais tarde. 3. Você também deve ter notado uma nova anotação acima do id – @JsonIgnore. Como id é nossa entidade interna, não precisamos necessariamente devolvê-lo ao cliente. 4. @NotBlank é uma anotação especial para validação, que diz que o campo não deve ser nulo ou a string vazia 5. @Column(unique = true) diz que esta coluna deve ter valores únicos. Então, já temos duas entidades, elas estão até conectadas entre si. Chegou a hora de integrá-los ao nosso programa - vamos cuidar dos serviços e controladores. Primeiro de tudo, vamos remover nosso stub do método getAllEmployees() e transformá-lo em algo que realmente funcione:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Assim, nosso repositório coletará tudo o que estiver disponível no banco de dados e nos entregará. Vale ressaltar que também irá pegar a lista de tarefas. Mas juntá-lo é certamente interessante, mas o que é juntá-lo se não há nada lá? Isso mesmo, isso significa que precisamos descobrir como colocar algo ali. Primeiramente, vamos escrever um novo método em nosso controlador.
@PostMapping("${application.endpoint.employee}")
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
Este é @PostMapping, ou seja, ele processa solicitações POST que chegam ao endpoint de nossos funcionários. Em geral, pensei que, como todas as solicitações para esse controlador chegariam ao mesmo endpoint, vamos simplificar um pouco. Lembra de nossas ótimas configurações em application.yml? Vamos consertá-los. Deixe a seção do aplicativo agora ficar assim:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
O que isso nos dá? O fato de que no controlador podemos remover o mapeamento para cada método específico, e o endpoint será definido no nível da classe na anotação @RequestMapping("${application.endpoint.employee}") . Essa é a beleza agora em nosso controlador:
@RestController
@RequestMapping("${application.endpoint.employee}")
@RequiredArgsConstructor
public class EmployeeController {

    private final EmployeeService employeeService;

    @GetMapping
    public ResponseEntity<List<Employee>> getEmployees() {
        return ResponseEntity.ok().body(employeeService.getAllEmployees());
    }

    @PostMapping
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}
No entanto, vamos em frente. O que exatamente acontece no método createOrUpdateEmployee? Obviamente, nosso EmployeeService possui um método save, que deve ser responsável por todo o trabalho de salvamento. Também é óbvio que este método pode lançar uma exceção com um nome autoexplicativo. Aqueles. algum tipo de validação está sendo realizada. E a resposta depende diretamente dos resultados da validação, se será 201 Criado ou 400 badRequest com lista do que deu errado. Olhando para o futuro, este é o nosso novo serviço de validação, ele verifica os dados recebidos quanto à presença de campos obrigatórios (lembra do @NotBlank?) e decide se tais informações são adequadas para nós ou não. Antes de passarmos ao método de salvamento, vamos validar e implementar este serviço. Para fazer isso, proponho criar um pacote de validação separado no qual colocaremos nosso serviço.
import com.example.demo.exception.ValidationException;
import com.example.demo.persistence.entity.Employee;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

@Service
@RequiredArgsConstructor
public class ValidationService {

    private final Validator validator;

    public boolean isValidEmployee(Employee employee) throws ValidationException {
        Set<Constraintviolation<Employee>> constraintViolations = validator.validate(employee);

        if (CollectionUtils.isNotEmpty(constraintViolations)) {
            throw new ValidationException(buildViolationsList(constraintViolations));
        }
        return true;
    }

    private <T> List<Violation> buildViolationsList(Set<Constraintviolation<T>> constraintViolations) {
        return constraintViolations.stream()
                                   .map(violation -> new Violation(
                                                   violation.getPropertyPath().toString(),
                                                   violation.getMessage()
                                           )
                                   )
                                   .toList();
    }
}
A classe acabou ficando muito grande, mas não entre em pânico, vamos descobrir agora :) Aqui usamos as ferramentas da biblioteca de validação pronta javax.validation Esta biblioteca veio até nós a partir das novas dependências que nós adicionado à implementação build.graddle 'org.springframework.boot:spring-boot-starter -validation' Nossos velhos amigos Service e RequiredArgsConstructor Já nos contam tudo o que precisamos saber sobre esta classe, há também um campo validador final privado. Ele fará a mágica. Criamos o método isValidEmployee, para o qual podemos passar a entidade Employee; esse método lança uma ValidationException, que escreveremos um pouco mais tarde. Sim, esta será uma exceção personalizada para nossas necessidades. Usando o método validator.validate(employee), obteremos uma lista de objetos ConstraintViolation - todas aquelas inconsistências com as anotações de validação que adicionamos anteriormente. A lógica adicional é simples, se esta lista não estiver vazia (ou seja, há violações), lançamos uma exceção e construímos uma lista de violações - o método buildViolationsList. Observe que este é um método genérico, ou seja, pode trabalhar com listas de violações de diferentes objetos - pode ser útil no futuro se validarmos outra coisa. O que esse método realmente faz? Usando a API stream, examinamos a lista de violações. Transformamos cada violação no método map em nosso próprio objeto de violação e coletamos todos os objetos resultantes em uma lista. Estamos devolvendo-o. O que mais é o nosso próprio objeto de violação, você pergunta? Aqui está um registro simples
public record Violation(String property, String message) {}
Registros são inovações especiais em Java, caso você precise de um objeto com dados, sem nenhuma lógica ou qualquer outra coisa. Embora eu ainda não tenha entendido por que isso foi feito, às vezes é uma coisa bastante conveniente. Deve ser criado em um arquivo separado, como uma classe normal. Voltando ao ValidationException personalizado - fica assim:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Ele armazena uma lista de todas as violações, a anotação Lombok - Getter está anexada à lista, e através de outra anotação Lombok “implementamos” o construtor necessário :) É importante notar aqui que não implemento corretamente o comportamento do isValid ... método, ele retorna verdadeiro ou exceção, mas valeria a pena nos limitarmos ao Falso usual. A abordagem de exceção é feita porque quero retornar esse erro ao cliente, o que significa que preciso retornar algo diferente de verdadeiro ou falso do método booleano. No caso de métodos de validação puramente internos, não há necessidade de lançar uma exceção; o registro será necessário aqui. Porém, vamos voltar ao nosso EmployeeService, ainda precisamos começar a salvar os objetos :) Vamos ver como fica essa classe agora:
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final ValidationService validationService;

    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Transactional
    public void save(Employee employee) throws ValidationException {
        if (validationService.isValidEmployee(employee)) {
            Employee existingEmployee = employeeRepository.findByUniqueNumber(employee.getUniqueNumber());
            if (existingEmployee == null) {
                employeeRepository.save(employee);
            } else {
                existingEmployee = updateFields(existingEmployee, employee);
                employeeRepository.save(existingEmployee);
            }
        }
    }

    private Employee updateFields(Employee existingEmployee, Employee updatedEmployee) {
        return existingEmployee.setDepartment(updatedEmployee.getDepartment())
                               .setSalary(updatedEmployee.getSalary())
            				 //TODO implement tasks merging instead of replacement
                               .setTasks(updatedEmployee.getTasks());
    }
}
Observe a nova propriedade final private final ValidationService validaçãoService; O próprio método save é marcado com a anotação @Transactional para que, se uma RuntimeException for recebida, as alterações sejam revertidas. Primeiro de tudo, validamos os dados recebidos usando o serviço que acabamos de escrever. Se tudo correr bem, verificamos se existe algum funcionário no banco de dados (usando um número único). Caso contrário, salvamos o novo, se houver, atualizamos os campos da classe. Ah, sim, como podemos realmente verificar? Sim, é muito simples, adicionamos um novo método ao repositório Employee:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
O que é notável? Não escrevi nenhuma lógica ou consulta SQL, embora esteja disponível aqui. Spring, simplesmente lendo o nome do método, determina o que desejo - encontre ByUniqueNumber e passe a string correspondente para o método. Voltando à atualização dos campos - aqui resolvi usar o bom senso e atualizar apenas o departamento, salário e tarefas, pois mudar o nome, embora seja uma coisa aceitável, ainda não é muito comum. E alterar a data de contratação geralmente é um assunto polêmico. O que seria bom fazer aqui? Combine listas de tarefas, mas como ainda não temos tarefas e não sabemos distingui-las, deixaremos o TODO. Vamos tentar lançar nosso Frankenstein. Se eu não esqueci de descrever nada, deve funcionar, mas primeiro, aqui está a árvore de classes que obtivemos: As API REST e validação de dados - 1 classes que foram modificadas estão destacadas em azul, as novas estão destacadas em verde, tais indicações podem ser obtidas se você trabalhar com um repositório git, mas git não é o assunto do nosso artigo, então não vamos nos alongar sobre isso. Portanto, no momento temos um endpoint que suporta dois métodos GET e POST. A propósito, algumas informações interessantes sobre o endpoint. Por que, por exemplo, não alocamos endpoints separados para GET e POST, como getAllEmployees ou createEmployees? Tudo é extremamente simples - ter um ponto único para todas as solicitações é muito mais conveniente. O roteamento ocorre baseado no método HTTP e é intuitivo, não há necessidade de lembrar todas as variações de getAllEmployees, getEmployeeByName, get... update... create... delete... Vamos testar o que obtivemos. Já escrevi no artigo anterior que precisaremos do Postman e é hora de instalá-lo. Na interface do programa, criamos uma nova solicitação POST API REST e validação de dados - 2 e tentamos enviá-la. Se tudo correr bem obteremos o Status 201 no lado direito da tela. Mas, por exemplo, tendo enviado a mesma coisa, mas sem um número único (no qual temos validação), recebo uma resposta diferente: API REST e validação de dados - 3 Bem, vamos verificar como funciona nossa seleção completa - criamos um método GET para o mesmo endpoint e o enviamos . API REST e validação de dados - 4 Espero sinceramente que tudo tenha dado certo para você assim como deu para mim, e nos vemos no próximo artigo .
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION