JavaRush /Java Blog /Random EN /REST API and Data Validation
Денис
Level 37
Киев

REST API and Data Validation

Published in the Random EN group
Link to the first part: REST API and the next test task Well, our application is working, we can get some kind of response from it, but what does this give us? It doesn't do any useful work. No sooner said than done, let's implement something useful. First of all, let's add a few new dependencies to our build.gradle, they will be useful to us:
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'
And we will start with the actual data that we must process. Let's return to our persistence package and start filling the entity. As you remember, we left it to exist with only one field, and then the auto is generated via `@GeneratedValue(strategy = GenerationType.IDENTITY)` Let's remember the technical specifications from the first chapter:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
We have enough fields for the first time, so let’s start implementing it. The first three fields do not raise questions - these are ordinary lines, but the salary field is already suggestive. Why the actual line? In real work, this also happens, a customer comes to you and says - I want to send you this payload, and you process it. You can, of course, shrug your shoulders and do it, you can try to come to an agreement and explain that it is better to transmit the data in the required format. Let's imagine that we came across a smart client and agreed that it is better to transmit numbers in numeric format, and since we are talking about money, let it be Double. The next parameter of our payload will be the hiring date, the client will send it to us in the agreed format: yyyy-mm-dd, where y is responsible for years, m for days, and d is expected for days - 2022-08-12. The last field at the moment will be a list of tasks assigned to the client. Obviously, Task is another entity in our database, but we don’t know much about it yet, so we’ll create the most basic entity as we did with Employee before. The only thing we can assume now is that more than one task can be assigned to one employee, so we will apply the so-called One-To-Many approach, a one-to-many ratio. More specifically, one record in the employee table can correspond to several records from the tasks table . I also decided to add such a thing as a uniqueNumber field, so that we could clearly distinguish one employee from another. At the moment our Employee class looks like this:
@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<>();
}
The following class was created for the Task entity:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
As I said, we won’t see anything new in Task, a new repository was also created for this class, which is a copy of the repository for Employee - I won’t give it, you can create it yourself by analogy. But it makes sense to talk about the Employee class. As I said, we added several fields, but only the last one is of interest now - tasks. This is a List<Task> tasks , it is immediately initialized with an empty ArrayList and marked with several annotations. 1. @OneToMany As I said, this will be our ratio of employees to tasks. 2. @JoinColumn - the column by which entities will be joined. In this case, an employee_id column will be created in the Task table pointing to the id of our employee; it will serve us as ForeighnKey Despite the seeming sacredness of the name, you can name the column anything you like. The situation will be a little more complicated if you need to use not just an ID, but some kind of real column; we will touch on this topic later. 3. You may also have noticed a new annotation above the id - @JsonIgnore. Since id is our internal entity, we don’t necessarily need to return it to the client. 4. @NotBlank is a special annotation for validation, which says that the field must not be null or the empty string 5. @Column(unique = true) says that this column must have unique values. So, we already have two entities, they are even connected to each other. The time has come to integrate them into our program - let's go deal with services and controllers. First of all, let’s remove our stub from the getAllEmployees() method and turn it into something that actually works:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Thus, our repository will rake everything that is available from the database and give it to us. It is noteworthy that it will also pick up the task list. But raking it out is certainly interesting, but what is raking it out if there’s nothing there? That's right, that means we need to figure out how to put something there. First of all, let's write a new method in our controller.
@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();
    }
This is @PostMapping, i.e. it processes POST requests coming to our employees endpoint. In general, I thought that since all requests to this controller will come to one endpoint, let’s simplify this a little. Remember our nice settings in application.yml? Let's fix them. Let the application section now look like this:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
What does this give us? The fact that in the controller we can remove the mapping for each specific method, and the endpoint will be set at the class level in the @RequestMapping("${application.endpoint.employee}") annotation . This is the beauty now in our Controller:
@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();
    }
}
However, let's move on. What exactly happens in the createOrUpdateEmployee method? Obviously, our employeeService has a save method, which should be responsible for all the saving work. It is also obvious that this method can throw an exception with a self-explanatory name. Those. some kind of validation is being carried out. And the answer depends directly on the validation results, whether it will be 201 Created or 400 badRequest with a list of what went wrong. Looking ahead, this is our new validation service, it checks incoming data for the presence of required fields (remember @NotBlank?) and decides whether such information is suitable for us or not. Before moving on to the saving method, let's validate and implement this service. To do this, I propose to create a separate validation package in which we will put our service.
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();
    }
}
The class turned out to be too big, but don't panic, we'll figure it out now :) Here we use the tools of the ready-made validation library javax.validation This library came to us from the new dependencies that we added to build.graddle implementation 'org.springframework.boot:spring-boot-starter -validation' Our old friends Service and RequiredArgsConstructor Already tell us everything we need to know about this class, there is also a private final validator field. He will do the magic. We created the isValidEmployee method, into which we can pass the Employee entity; this method throws a ValidationException, which we will write a little later. Yes, this will be a custom exception for our needs. Using the validator.validate(employee) method, we will get a list of ConstraintViolation objects - all those inconsistencies with the validation annotations that we added earlier. The further logic is simple, if this list is not empty (i.e. there are violations), we throw an exception and build a list of violations - the buildViolationsList method Please note that this is a Generic method, i.e. can work with lists of violations of different objects - it may be useful in the future if we validate something else. What does this method actually do? Using the stream API, we go through the list of violations. We turn each violation in the map method into our own violation object, and collect all the resulting objects into a list. We are returning him. What else is our own object of violation, you ask? Here's a simple record
public record Violation(String property, String message) {}
Records are such special innovations in Java, in case you need an object with data, without any logic or anything else. Although I myself haven’t yet understood why this was done, sometimes it’s quite a convenient thing. It must be created in a separate file, like a regular class. Returning to the custom ValidationException - it looks like this:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
It stores a list of all violations, the Lombok annotation - Getter is attached to the list, and through another Lombok annotation we “implemented” the required constructor :) It is worth noting here that I do not quite correctly implement the behavior of the isValid... method, it returns either true or exception, but it would be worth limiting ourselves to the usual False. The exception approach is made because I want to return this error to the client, which means I need to return something other than true or false from the boolean method. In the case of purely internal validation methods, there is no need to throw an exception; logging will be required here. However, let's return to our EmployeeService, we still need to start saving objects :) Let's see what this class looks like now:
@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());
    }
}
Notice the new final property private final ValidationService validationService; The save method itself is marked with the @Transactional annotation so that if a RuntimeException is received, the changes are rolled back. First of all, we validate the incoming data using the service we just wrote. If everything went smoothly, we check whether there is an existing employee in the database (using a unique number). If not, we save the new one, if there is one, we update the fields in the class. Oh yes, how do we actually check? Yes, it’s very simple, we added a new method to the Employee repository:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
What's notable? I didn't write any logic or SQL query, although that is available here. Spring, simply by reading the name of the method, determines what I want - find ByUniqueNumber and pass the corresponding string to the method. Returning to updating the fields - here I decided to use common sense and update only the department, salary and tasks, because changing the name, although an acceptable thing, is still not very common. And changing the hiring date is generally a controversial issue. What would be good to do here? Combine task lists, but since we don’t have tasks yet and don’t know how to distinguish them, we’ll leave TODO. Let's try to launch our Frankenstein. If I didn’t forget to describe anything, it should work, but first, here’s the class tree that we got: REST API and Data Validation - 1 Classes that have been modified are highlighted in blue, new ones are highlighted in green, such indications can be obtained if you work with a git repository, but git is not the topic for ours article, so we won’t dwell on it. So, at the moment we have one endpoint that supports two methods GET and POST. By the way, some interesting information about endpoint. Why, for example, did we not allocate separate endpoints for GET and POST, such as getAllEmployees or createEmployees? Everything is extremely simple - having a single point for all requests is much more convenient. Routing occurs based on the HTTP method and it is intuitive, there is no need to remember all the variations of getAllEmployees, getEmployeeByName, get... update... create... delete... Let's test what we got. I already wrote in the previous article that we will need Postman, and it’s time to install it. In the program interface, we create a new POST request REST API and Data Validation - 2 and try to send it. If everything went well we will get Status 201 on the right side of the screen. But for example, having sent the same thing but without a unique number (on which we have validation), I get a different answer: REST API and Data Validation - 3 Well, let’s check how our full selection works - we create a GET method for the same endpoint and send it. REST API and Data Validation - 4 I sincerely hope that everything worked out for you just like it did for me, and see you in the next article .
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION