JavaRush /Blog Java /Random-VI /Xác thực dữ liệu và API REST
Денис
Mức độ
Киев

Xác thực dữ liệu và API REST

Xuất bản trong nhóm
Liên kết đến phần đầu tiên: API REST và nhiệm vụ kiểm tra tiếp theo Ứng dụng của chúng tôi đang hoạt động, chúng tôi có thể nhận được một số phản hồi từ nó, nhưng điều này mang lại cho chúng tôi điều gì? Nó không làm bất kỳ công việc hữu ích. Nói sớm hơn làm, hãy thực hiện điều gì đó hữu ích. Trước hết, hãy thêm một vài phần phụ thuộc mới vào build.gradle, chúng sẽ hữu ích cho chúng ta:
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'
Và chúng ta sẽ bắt đầu với dữ liệu thực tế mà chúng ta phải xử lý. Hãy quay lại gói kiên trì của chúng tôi và bắt đầu điền vào thực thể. Như bạn nhớ, chúng tôi đã để nó tồn tại chỉ với một trường và sau đó ô tô được tạo thông qua `@GeneratedValue(strategy = GenerationType.IDENTITY)` Hãy nhớ lại các thông số kỹ thuật từ chương đầu tiên:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Lần đầu tiên chúng ta có đủ trường nên hãy bắt đầu triển khai nó. Ba trường đầu tiên không đặt ra câu hỏi - đây là những dòng thông thường, nhưng trường lương đã mang tính gợi ý. Tại sao dòng thực tế? Trong công việc thực tế, điều này cũng xảy ra, một khách hàng đến gặp bạn và nói - Tôi muốn gửi cho bạn tải trọng này và bạn xử lý nó. Tất nhiên, bạn có thể nhún vai và làm điều đó, bạn có thể cố gắng đi đến thống nhất và giải thích rằng tốt hơn là truyền dữ liệu theo định dạng được yêu cầu. Hãy tưởng tượng rằng chúng ta gặp một khách hàng thông minh và đồng ý rằng tốt hơn là truyền các số ở định dạng số và vì chúng ta đang nói về tiền, hãy đặt nó là Double. Tham số tiếp theo của tải trọng của chúng tôi sẽ là ngày tuyển dụng, khách hàng sẽ gửi nó cho chúng tôi theo định dạng đã thỏa thuận: yyyy-mm-dd, trong đó y chịu trách nhiệm theo năm, m tính theo ngày và d dự kiến ​​tính theo ngày - 2022- 08-12. Trường cuối cùng tại thời điểm này sẽ là danh sách các nhiệm vụ được giao cho khách hàng. Rõ ràng, Nhiệm vụ là một thực thể khác trong cơ sở dữ liệu của chúng tôi, nhưng chúng tôi chưa biết nhiều về nó, vì vậy chúng tôi sẽ tạo thực thể cơ bản nhất như chúng tôi đã làm với Nhân viên trước đây. Điều duy nhất chúng ta có thể giả định bây giờ là có thể giao nhiều nhiệm vụ cho một nhân viên, vì vậy chúng ta sẽ áp dụng phương pháp được gọi là Một-Nhiều, tỷ lệ một-nhiều. Cụ thể hơn, một bản ghi trong bảng nhân viên có thể tương ứng với nhiều bản ghi trong bảng nhiệm vụ . Tôi cũng quyết định thêm một thứ như trường Số duy nhất để chúng tôi có thể phân biệt rõ ràng nhân viên này với nhân viên khác. Hiện tại lớp Nhân viên của chúng tôi trông như thế này:
@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<>();
}
Lớp sau đây đã được tạo cho thực thể Nhiệm vụ:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Như tôi đã nói, chúng ta sẽ không thấy gì mới trong Task, một kho lưu trữ mới cũng đã được tạo cho lớp này, đây là bản sao của kho lưu trữ dành cho Nhân viên - Tôi sẽ không cung cấp nó, bạn có thể tự tạo nó bằng cách tương tự. Nhưng thật hợp lý khi nói về lớp Nhân viên. Như tôi đã nói, chúng tôi đã thêm một số trường, nhưng hiện tại chỉ có trường cuối cùng được quan tâm - nhiệm vụ. Đây là một List<Task> task , nó được khởi tạo ngay lập tức với một ArrayList trống và được đánh dấu bằng một số chú thích. 1. @OneToMany Như tôi đã nói, đây sẽ là tỷ lệ nhân viên của chúng tôi so với nhiệm vụ. 2. @JoinColumn - cột mà các thực thể sẽ được tham gia. Trong trường hợp này, một cột member_id sẽ được tạo trong bảng Nhiệm vụ trỏ đến id của nhân viên của chúng ta; nó sẽ phục vụ chúng ta như ForeighnKey. Mặc dù tên có vẻ thiêng liêng nhưng bạn có thể đặt tên cho cột bất cứ tên nào bạn thích. Tình huống sẽ phức tạp hơn một chút nếu bạn không chỉ cần sử dụng ID mà còn cần một loại cột thực nào đó; chúng ta sẽ đề cập đến chủ đề này sau. 3. Bạn cũng có thể nhận thấy chú thích mới phía trên id - @JsonIgnore. Vì id là thực thể nội bộ của chúng tôi nên chúng tôi không nhất thiết phải trả lại nó cho khách hàng. 4. @NotBlank là chú thích đặc biệt để xác thực, chú thích này cho biết trường không được rỗng hoặc chuỗi trống 5. @Column(unique = true) cho biết cột này phải có các giá trị duy nhất. Vì vậy, chúng ta đã có hai thực thể, chúng thậm chí còn được kết nối với nhau. Đã đến lúc tích hợp chúng vào chương trình của chúng ta - hãy bắt tay vào giải quyết các dịch vụ và bộ điều khiển. Trước hết, hãy xóa phần sơ khai của chúng ta khỏi phương thức getAllEmployees() và biến nó thành một thứ thực sự hoạt động:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Do đó, kho lưu trữ của chúng tôi sẽ lấy mọi thứ có sẵn từ cơ sở dữ liệu và cung cấp cho chúng tôi. Đáng chú ý là nó cũng sẽ nhận danh sách nhiệm vụ. Nhưng việc cào nó ra chắc chắn là thú vị, nhưng việc cào nó ra là gì nếu không có gì ở đó? Đúng vậy, điều đó có nghĩa là chúng ta cần tìm ra cách đặt thứ gì đó vào đó. Trước hết, hãy viết một phương thức mới trong bộ điều khiển của chúng ta.
@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();
    }
Đây là @PostMapping, tức là nó xử lý các yêu cầu POST đến điểm cuối nhân viên của chúng tôi. Nói chung, tôi nghĩ rằng vì tất cả các yêu cầu tới bộ điều khiển này sẽ đến một điểm cuối nên hãy đơn giản hóa việc này một chút. Bạn có nhớ các cài đặt thú vị của chúng tôi trong application.yml không? Hãy sửa chúng. Bây giờ hãy để phần ứng dụng trông như thế này:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Điều này mang lại cho chúng ta điều gì? Thực tế là trong bộ điều khiển, chúng ta có thể loại bỏ ánh xạ cho từng phương thức cụ thể và điểm cuối sẽ được đặt ở cấp lớp trong chú thích @RequestMapping("${application.endpoint.employee}") . Đây là vẻ đẹp bây giờ trong Bộ điều khiển của chúng tôi:
@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();
    }
}
Tuy nhiên, hãy tiếp tục. Chính xác thì điều gì xảy ra trong phương thức createOrUpdateEmployee? Rõ ràng, serviceService của chúng tôi có một phương thức lưu, phương thức này sẽ chịu trách nhiệm cho tất cả công việc lưu. Rõ ràng là phương pháp này có thể tạo ra một ngoại lệ với một cái tên dễ hiểu. Những thứ kia. một số loại xác nhận đang được thực hiện. Và câu trả lời phụ thuộc trực tiếp vào kết quả xác thực, cho dù đó là 201 Created hay 400 badRequest với danh sách những gì đã xảy ra. Nhìn về phía trước, đây là dịch vụ xác thực mới của chúng tôi, nó kiểm tra sự hiện diện của các trường bắt buộc trong dữ liệu đến (bạn có nhớ @NotBlank không?) và quyết định xem thông tin đó có phù hợp với chúng tôi hay không. Trước khi chuyển sang phương pháp lưu, hãy xác thực và triển khai dịch vụ này. Để làm điều này, tôi đề xuất tạo một gói xác thực riêng trong đó chúng tôi sẽ đưa dịch vụ của mình và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();
    }
}
Lớp học hóa ra quá lớn, nhưng đừng hoảng sợ, chúng ta sẽ tìm hiểu ngay bây giờ :) Ở đây chúng ta sử dụng các công cụ của thư viện xác thực có sẵn javax.validation. Thư viện này đến với chúng ta từ các phụ thuộc mới mà chúng ta đã thêm vào triển khai build.gradle 'org.springframework.boot:spring-boot-starter -validation' Những người bạn cũ của chúng tôi Dịch vụ và NeedArgsConstructor Đã cho chúng tôi biết mọi thứ chúng tôi cần biết về lớp này, cũng có trường trình xác thực cuối cùng riêng tư. Anh ấy sẽ làm phép thuật. Chúng ta đã tạo phương thức isValidEmployee để có thể chuyển thực thể Nhân viên vào đó; phương thức này đưa ra một ngoại lệ ValidationException mà chúng ta sẽ viết sau. Có, đây sẽ là một ngoại lệ tùy chỉnh cho nhu cầu của chúng tôi. Sử dụng phương thức validator.validate(employee), chúng ta sẽ nhận được danh sách các đối tượng ConstraintViolation - tất cả những điểm không nhất quán với các chú thích xác thực mà chúng ta đã thêm trước đó. Logic xa hơn rất đơn giản, nếu danh sách này không trống (tức là có vi phạm), chúng tôi sẽ đưa ra một ngoại lệ và xây dựng danh sách các vi phạm - phương thức buildViolationsList Xin lưu ý rằng đây là phương thức Chung, tức là. có thể làm việc với danh sách vi phạm của các đối tượng khác nhau - nó có thể hữu ích trong tương lai nếu chúng tôi xác nhận điều gì đó khác. Phương pháp này thực sự làm gì? Bằng cách sử dụng API luồng, chúng tôi xem qua danh sách các vi phạm. Chúng tôi biến từng vi phạm trong phương thức bản đồ thành đối tượng vi phạm của riêng mình và thu thập tất cả các đối tượng kết quả vào một danh sách. Chúng tôi đang trả lại anh ta. Bạn hỏi đối tượng vi phạm của chúng tôi còn là gì nữa? Đây là một bản ghi đơn giản
public record Violation(String property, String message) {}
Bản ghi là những cải tiến đặc biệt trong Java, trong trường hợp bạn cần một đối tượng có dữ liệu mà không cần bất kỳ logic hay bất kỳ thứ gì khác. Mặc dù bản thân tôi cũng chưa hiểu tại sao lại làm như vậy nhưng đôi khi nó lại là một điều khá tiện lợi. Nó phải được tạo trong một tệp riêng biệt, giống như một lớp thông thường. Quay lại ValidationException tùy chỉnh - nó trông như thế này:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Nó lưu trữ một danh sách tất cả các vi phạm, chú thích Lombok - Getter được đính kèm vào danh sách và thông qua một chú thích Lombok khác, chúng tôi đã “triển khai” hàm tạo được yêu cầu :) Điều đáng chú ý ở đây là tôi triển khai không hoàn toàn chính xác hành vi của isValid ..., nó trả về giá trị đúng hoặc ngoại lệ, nhưng sẽ đáng để chúng ta giới hạn ở giá trị Sai thông thường. Cách tiếp cận ngoại lệ được thực hiện vì tôi muốn trả lại lỗi này cho máy khách, điều đó có nghĩa là tôi cần trả về một cái gì đó không phải là đúng hoặc sai từ phương thức boolean. Trong trường hợp các phương pháp xác thực hoàn toàn nội bộ, không cần phải đưa ra ngoại lệ; việc ghi nhật ký sẽ được yêu cầu ở đây. Tuy nhiên, hãy quay lại phần Nhân viên của chúng ta, chúng ta vẫn cần bắt đầu lưu các đối tượng :) Bây giờ hãy xem lớp này trông như thế nào:
@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());
    }
}
Lưu ý thuộc tính cuối cùng mới riêng tư Dịch vụ xác thực dịch vụ xác thực cuối cùng; Bản thân phương thức lưu được đánh dấu bằng chú thích @Transactional để nếu nhận được RuntimeException, các thay đổi sẽ được khôi phục. Trước hết, chúng tôi xác thực dữ liệu đến bằng dịch vụ chúng tôi vừa viết. Nếu mọi việc suôn sẻ, chúng tôi sẽ kiểm tra xem có nhân viên nào hiện có trong cơ sở dữ liệu hay không (sử dụng một số duy nhất). Nếu không, chúng tôi lưu cái mới, nếu có, chúng tôi cập nhật các trường trong lớp. Ồ vâng, làm thế nào để chúng ta thực sự kiểm tra? Vâng, rất đơn giản, chúng tôi đã thêm một phương thức mới vào kho lưu trữ Nhân viên:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Điều gì đáng chú ý? Tôi không viết bất kỳ truy vấn logic hoặc SQL nào, mặc dù điều đó có sẵn ở đây. Spring, chỉ cần đọc tên của phương thức, sẽ xác định điều tôi muốn - tìm ByUniqueNumber và truyền chuỗi tương ứng cho phương thức. Quay lại cập nhật các trường - ở đây tôi quyết định sử dụng thông thường và chỉ cập nhật bộ phận, tiền lương và nhiệm vụ, bởi vì việc thay đổi tên, mặc dù là một điều có thể chấp nhận được, nhưng vẫn không phổ biến lắm. Và việc thay đổi ngày tuyển dụng nói chung là một vấn đề gây tranh cãi. Điều gì sẽ tốt để làm ở đây? Tổng hợp các danh sách nhiệm vụ, nhưng vì chúng ta chưa có nhiệm vụ nào và cũng chưa biết cách phân biệt nên chúng ta sẽ bỏ TODO. Hãy thử khởi động Frankenstein của chúng tôi. Nếu tôi không quên mô tả bất cứ điều gì thì nó sẽ hoạt động, nhưng trước tiên, đây là cây lớp mà chúng ta có: Xác thực dữ liệu và API REST - 1 Các lớp đã được sửa đổi được đánh dấu bằng màu xanh lam, các lớp mới được đánh dấu bằng màu xanh lục, bạn có thể nhận được những dấu hiệu như vậy nếu bạn làm việc với kho lưu trữ git, nhưng git không phải là chủ đề cho bài viết của chúng tôi, vì vậy chúng tôi sẽ không tập trung vào nó. Vì vậy, hiện tại chúng tôi có một điểm cuối hỗ trợ hai phương thức GET và POST. Nhân tiện, có một số thông tin thú vị về điểm cuối. Ví dụ: tại sao chúng tôi không phân bổ các điểm cuối riêng biệt cho GET và POST, chẳng hạn như getAllEmployees hoặc createEmployees? Mọi thứ cực kỳ đơn giản - có một điểm duy nhất cho tất cả các yêu cầu sẽ thuận tiện hơn nhiều. Việc định tuyến diễn ra dựa trên phương thức HTTP và nó trực quan, không cần phải nhớ tất cả các biến thể của getAllEmployees, getEmployeeByName, get... update... create... delete... Hãy kiểm tra những gì chúng ta có. Tôi đã viết trong bài viết trước rằng chúng ta sẽ cần Postman và đã đến lúc cài đặt nó. Trong giao diện chương trình, chúng ta tạo một yêu cầu POST mới API REST và xác thực dữ liệu - 2 và thử gửi nó đi. Nếu mọi việc suôn sẻ, chúng ta sẽ nhận được Trạng thái 201 ở bên phải màn hình. Nhưng ví dụ: sau khi gửi cùng một thứ nhưng không có số duy nhất (mà chúng tôi đã xác thực), tôi nhận được một câu trả lời khác: Xác thực dữ liệu và API REST - 3 Chà, hãy kiểm tra xem lựa chọn đầy đủ của chúng tôi hoạt động như thế nào - chúng tôi tạo một phương thức GET cho cùng một điểm cuối và gửi nó . Xác thực dữ liệu và API REST - 4 Tôi chân thành hy vọng rằng mọi thứ đều diễn ra tốt đẹp với bạn giống như đối với tôi và hẹn gặp lại bạn trong bài viết tiếp theo .
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION