JavaRush /Blog Java /Random-ES /API REST y validación de datos
Денис
Nivel 37
Киев

API REST y validación de datos

Publicado en el grupo Random-ES
Enlace a la primera parte: API REST y la siguiente tarea de prueba Bueno, nuestra aplicación está funcionando, podemos obtener algún tipo de respuesta de ella, pero ¿qué nos aporta esto? No hace ningún trabajo útil. Dicho y hecho, implementemos algo útil. En primer lugar, agreguemos algunas dependencias nuevas a nuestro build.gradle, que nos serán útiles:
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'
Y comenzaremos con los datos reales que debemos procesar. Volvamos a nuestro paquete de persistencia y comencemos a llenar la entidad. Como recordarás, lo dejamos existir con un solo campo, y luego el auto se genera mediante `@GeneratedValue(strategy = GenerationType.IDENTITY)` Recordemos las especificaciones técnicas del primer capítulo:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Tenemos suficientes campos por primera vez, así que comencemos a implementarlo. Los primeros tres campos no plantean preguntas: son líneas ordinarias, pero el campo salarial ya es sugerente. ¿Por qué la línea real? En el trabajo real, esto también sucede: un cliente se acerca a usted y le dice: quiero enviarle esta carga útil y usted la procesa. Por supuesto, puedes encogerte de hombros y hacerlo, puedes intentar llegar a un acuerdo y explicar que es mejor transmitir los datos en el formato requerido. Imaginemos que nos encontramos con un cliente inteligente y acordamos que es mejor transmitir números en formato numérico, y como estamos hablando de dinero, que sea Doble. El siguiente parámetro de nuestra carga útil será la fecha de contratación, el cliente nos la enviará en el formato acordado: aaaa-mm-dd, donde y es responsable por años, m por días y d se espera por días - 2022- 08-12. El último campo por el momento será una lista de tareas asignadas al cliente. Obviamente, Tarea es otra entidad en nuestra base de datos, pero aún no sabemos mucho sobre ella, por lo que crearemos la entidad más básica como lo hicimos antes con Empleado. Lo único que podemos asumir ahora es que se puede asignar más de una tarea a un empleado, por lo que aplicaremos el llamado enfoque uno a muchos, una proporción de uno a muchos. Más concretamente, un registro de la tabla de empleados puede corresponder a varios registros de la tabla de tareas . También decidí agregar algo así como un campo Número único, para que pudiéramos distinguir claramente a un empleado de otro. Por el momento nuestra clase Empleado se ve así:
@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<>();
}
Se creó la siguiente clase para la entidad Tarea:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Como dije, no veremos nada nuevo en la Tarea, también se creó un nuevo repositorio para esta clase, que es una copia del repositorio para Empleado; no lo daré, puedes crearlo tú mismo por analogía. Pero tiene sentido hablar de la clase Empleado. Como dije, agregamos varios campos, pero ahora solo el último es de interés: tareas. Esta es una lista de tareas , se inicializa inmediatamente con una ArrayList vacía y se marca con varias anotaciones. 1. @OneToMany Como dije, esta será nuestra proporción de empleados por tareas. 2. @JoinColumn : la columna mediante la cual se unirán las entidades. En este caso, se creará una columna Employee_id en la tabla Task que apuntará a la identificación de nuestro empleado; nos servirá como ForeighnKey. A pesar de lo aparentemente sagrado del nombre, puedes nombrar la columna como quieras. La situación será un poco más complicada si necesita usar no solo una identificación, sino algún tipo de columna real, tocaremos este tema más adelante. 3. Es posible que también hayas notado una nueva anotación encima del ID: @JsonIgnore. Dado que la identificación es nuestra entidad interna, no necesariamente necesitamos devolvérsela al cliente. 4. @NotBlank es una anotación especial para validación, que dice que el campo no debe ser nulo o una cadena vacía. 5. @Column(unique = true) dice que esta columna debe tener valores únicos. Entonces ya tenemos dos entidades, incluso están conectadas entre sí. Ha llegado el momento de integrarlos en nuestro programa; vayamos a ocuparnos de los servicios y controladores. En primer lugar, eliminemos nuestro código auxiliar del método getAllEmployees() y convirtámoslo en algo que realmente funcione:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Por lo tanto, nuestro repositorio recopilará todo lo que esté disponible en la base de datos y nos lo entregará. Cabe destacar que también seleccionará la lista de tareas. Pero rastrillarlo es ciertamente interesante, pero ¿qué es rastrillarlo si no hay nada ahí? Así es, eso significa que tenemos que descubrir cómo poner algo ahí. Primero que nada, escribamos un nuevo método en nuestro 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();
    }
Esto es @PostMapping, es decir. procesa las solicitudes POST que llegan al punto final de nuestros empleados. En general, pensé que dado que todas las solicitudes a este controlador llegarán a un punto final, simplifiquemos esto un poco. ¿Recuerdas nuestra bonita configuración en application.yml? Arreglemoslos. Deje que la sección de la aplicación ahora se vea así:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
¿Qué nos aporta esto? El hecho de que en el controlador podemos eliminar el mapeo para cada método específico y el punto final se establecerá en el nivel de clase en la anotación @RequestMapping("${application.endpoint.employee}") . Esta es la belleza ahora en nuestro 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();
    }
}
Sin embargo, sigamos adelante. ¿Qué sucede exactamente en el método createOrUpdateEmployee? Obviamente, nuestro servicio de empleado tiene un método de guardar, que debería ser responsable de todo el trabajo de guardar. También es obvio que este método puede generar una excepción con un nombre que se explica por sí mismo. Aquellos. se está llevando a cabo algún tipo de validación. Y la respuesta depende directamente de los resultados de la validación, ya sea 201 Creado o 400 badRequest con una lista de lo que salió mal. De cara al futuro, este es nuestro nuevo servicio de validación, verifica los datos entrantes para detectar la presencia de campos obligatorios (¿recuerdas @NotBlank?) y decide si dicha información es adecuada para nosotros o no. Antes de pasar al método de guardar, validemos e implementemos este servicio. Para hacer esto, propongo crear un paquete de validación separado en el que colocaremos nuestro servicio.
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();
    }
}
La clase resultó ser demasiado grande, pero que no cunda el pánico, lo resolveremos ahora :) Aquí usamos las herramientas de la biblioteca de validación ya preparada javax.validation. Esta biblioteca nos llegó a partir de las nuevas dependencias que agregado a la implementación de build.graddle 'org.springframework.boot:spring-boot-starter -validation' Nuestros viejos amigos Service y RequiredArgsConstructor ya nos cuentan todo lo que necesitamos saber sobre esta clase, también hay un campo de validación final privado. Él hará la magia. Creamos el método isValidEmployee, al cual podemos pasar la entidad Empleado; este método arroja una ValidationException, que escribiremos un poco más adelante. Sí, esta será una excepción personalizada para nuestras necesidades. Usando el método validator.validate(employee), obtendremos una lista de objetos ConstraintViolation: todas esas inconsistencias con las anotaciones de validación que agregamos anteriormente. La lógica adicional es simple: si esta lista no está vacía (es decir, hay infracciones), lanzamos una excepción y creamos una lista de infracciones: el método buildViolationsList. Tenga en cuenta que este es un método genérico, es decir. Puede trabajar con listas de violaciones de diferentes objetos; puede ser útil en el futuro si validamos algo más. ¿Qué hace realmente este método? Utilizando la API de transmisión, revisamos la lista de infracciones. Convertimos cada infracción en el método map en nuestro propio objeto de infracción y recopilamos todos los objetos resultantes en una lista. Lo estamos devolviendo. ¿Qué más es nuestro propio objeto de violación, te preguntas? Aquí hay un registro simple.
public record Violation(String property, String message) {}
Los registros son innovaciones tan especiales en Java, en caso de que necesites un objeto con datos, sin ninguna lógica ni nada más. Aunque yo todavía no he entendido por qué se hizo esto, a veces resulta bastante conveniente. Debe crearse en un archivo separado, como una clase normal. Volviendo a la ValidationException personalizada, se ve así:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Almacena una lista de todas las violaciones, la anotación de Lombok - Getter está adjunta a la lista, y a través de otra anotación de Lombok "implementamos" el constructor requerido :) Vale la pena señalar aquí que no implemento correctamente el comportamiento de isValid ... método, devuelve verdadero o excepción, pero valdría la pena limitarnos al Falso habitual. El enfoque de excepción se realiza porque quiero devolver este error al cliente, lo que significa que necesito devolver algo que no sea verdadero o falso desde el método booleano. En el caso de métodos de validación puramente internos, no es necesario generar una excepción; aquí será necesario iniciar sesión. Sin embargo, volvamos a nuestro Servicio de Empleado, todavía necesitamos comenzar a guardar objetos :) Veamos cómo se ve esta clase ahora:
@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 la nueva propiedad final private ValidationService validationService; El método de guardar en sí está marcado con la anotación @Transactional para que, si se recibe una RuntimeException, los cambios se reviertan. En primer lugar, validamos los datos entrantes utilizando el servicio que acabamos de escribir. Si todo ha ido bien, comprobamos si hay algún empleado en la base de datos (utilizando un número único). Si no guardamos el nuevo, si lo hay actualizamos los campos de la clase. Oh, sí, ¿cómo lo comprobamos realmente? Sí, es muy simple, agregamos un nuevo método al repositorio de Empleados:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
¿Qué es notable? No escribí ninguna consulta lógica o SQL, aunque está disponible aquí. Spring, simplemente leyendo el nombre del método, determina lo que quiero: busca ByUniqueNumber y pasa la cadena correspondiente al método. Volviendo a actualizar los campos, aquí decidí usar el sentido común y actualizar solo el departamento, el salario y las tareas, porque cambiar el nombre, aunque es algo aceptable, todavía no es muy común. Y cambiar la fecha de contratación suele ser un tema controvertido. ¿Qué sería bueno hacer aquí? Combina listas de tareas, pero como aún no tenemos tareas y no sabemos distinguirlas, dejaremos TODO. Intentemos lanzar nuestro Frankenstein. Si no me olvidé de describir nada, debería funcionar, pero primero, aquí está el árbol de clases que obtuvimos: API REST y validación de datos - 1 las clases que han sido modificadas están resaltadas en azul, las nuevas están resaltadas en verde, tales indicaciones se pueden obtener si trabaja con un repositorio git, pero git no es el tema de nuestro artículo, por lo que no nos detendremos en ello. Entonces, por el momento tenemos un punto final que admite dos métodos GET y POST. Por cierto, alguna información interesante sobre el punto final. ¿Por qué, por ejemplo, no asignamos puntos finales separados para GET y POST, como getAllEmployees o createEmployees? Todo es muy sencillo: tener un único punto para todas las solicitudes es mucho más cómodo. El enrutamiento se produce según el método HTTP y es intuitivo, no es necesario recordar todas las variaciones de getAllEmployees, getEmployeeByName, get... update... create... delete... Probemos lo que tenemos. Ya escribí en el artículo anterior que necesitaremos Postman y es hora de instalarlo. En la interfaz del programa, creamos una nueva solicitud POST API REST y validación de datos - 2 e intentamos enviarla. Si todo ha ido bien nos aparecerá el Estado 201 en la parte derecha de la pantalla. Pero, por ejemplo, después de haber enviado lo mismo pero sin un número único (sobre el cual tenemos validación), obtengo una respuesta diferente: API REST y validación de datos - 3 Bueno, veamos cómo funciona nuestra selección completa: creamos un método GET para el mismo punto final y lo enviamos. . API REST y validación de datos - 4 Espero sinceramente que todo te haya salido igual que a mí y nos vemos en el próximo artículo .
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION