JavaRush /จาวาบล็อก /Random-TH /REST API และการตรวจสอบข้อมูล
Денис
ระดับ
Киев

REST API และการตรวจสอบข้อมูล

เผยแพร่ในกลุ่ม
ลิงก์ไปยังส่วนแรก: REST API และงานทดสอบถัดไป แอปพลิเคชันของเราใช้งานได้ เราจะได้รับการตอบสนองบางอย่างจากมัน แต่สิ่งนี้ให้อะไรเราบ้าง มันไม่มีประโยชน์อะไรเลย ไม่ช้าก็เร็ว เรามาประยุกต์ใช้สิ่งที่มีประโยชน์กันดีกว่า ก่อนอื่น เรามาเพิ่มการขึ้นต่อกันใหม่สองสามรายการให้กับ build.gradle ของเรา ซึ่งจะเป็นประโยชน์สำหรับเรา:
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'
และเราจะเริ่มต้นด้วยข้อมูลจริงที่เราต้องประมวลผล กลับไปที่แพ็คเกจการคงอยู่ของเราและเริ่มกรอกเอนทิตี ดังที่คุณจำได้ เราปล่อยให้มันมีอยู่เพียงฟิลด์เดียว จากนั้นรถยนต์จะถูกสร้างขึ้นผ่าน `@GeneratedValue(strategy = GenerationType.IDENTITY)` มาจำข้อกำหนดทางเทคนิคจากบทแรกกัน:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
เรามีช่องข้อมูลเพียงพอในครั้งแรก ดังนั้นมาเริ่มใช้งานกันดีกว่า สามฟิลด์แรกไม่ก่อให้เกิดคำถาม - นี่เป็นบรรทัดธรรมดา แต่ฟิลด์เงินเดือนมีการชี้นำอยู่แล้ว ทำไมต้องเป็นเส้นจริง? ในการทำงานจริง สิ่งนี้ก็เกิดขึ้นเช่นกัน ลูกค้ามาหาคุณแล้วพูดว่า - ฉันต้องการส่งเพย์โหลดนี้ไปให้คุณ และคุณก็ดำเนินการ แน่นอนคุณสามารถยักไหล่และทำได้คุณสามารถลองตกลงและอธิบายว่าเป็นการดีกว่าที่จะส่งข้อมูลในรูปแบบที่ต้องการ ลองนึกภาพว่าเราเจอไคลเอนต์ที่ชาญฉลาดและตกลงกันว่าควรส่งตัวเลขในรูปแบบตัวเลขดีกว่าและเนื่องจากเรากำลังพูดถึงเรื่องเงิน ปล่อยให้มันเป็นสองเท่า พารามิเตอร์ถัดไปของเพย์โหลดของเราคือวันที่จ้างงาน ลูกค้าจะส่งมาให้เราในรูปแบบที่ตกลงกัน: ปปปป-ดด-วว โดยที่ y รับผิดชอบเป็นปี m คือวัน และ d คือวันที่คาดหวัง - 2022- 08-12. ฟิลด์สุดท้ายในขณะนี้จะเป็นรายการงานที่ได้รับมอบหมายให้กับลูกค้า แน่นอนว่า Task เป็นอีกเอนทิตีหนึ่งในฐานข้อมูลของเรา แต่เรายังไม่รู้อะไรมากนัก ดังนั้น เราจะสร้างเอนทิตีพื้นฐานที่สุดเหมือนกับที่เราเคยทำกับ Employee มาก่อน สิ่งเดียวที่เราสามารถสรุปได้ในตอนนี้คือสามารถมอบหมายงานให้กับพนักงานได้มากกว่าหนึ่งงาน ดังนั้นเราจะใช้สิ่งที่เรียกว่าวิธีแบบหนึ่งต่อกลุ่ม ซึ่งเป็นอัตราส่วนแบบหนึ่งต่อกลุ่ม โดยเฉพาะอย่างยิ่ง หนึ่งระเบียนในตารางพนักงานสามารถสอดคล้องกับหลายระเบียนจาก ตาราง งาน ฉันยังตัดสินใจเพิ่มสิ่งนี้เป็นฟิลด์ UniqueNumber เพื่อให้เราสามารถแยกแยะพนักงานคนหนึ่งจากอีกคนหนึ่งได้อย่างชัดเจน ในขณะนี้คลาส Employee ของเรามีลักษณะดังนี้:
@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<>();
}
คลาสต่อไปนี้ถูกสร้างขึ้นสำหรับเอนทิตีงาน:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
อย่างที่ฉันบอกไปเราจะไม่เห็นสิ่งใหม่ใน Task นอกจากนี้ยังมีการสร้างพื้นที่เก็บข้อมูลใหม่สำหรับคลาสนี้ซึ่งเป็นสำเนาของพื้นที่เก็บข้อมูลสำหรับพนักงาน - ฉันจะไม่ให้มันคุณสามารถสร้างมันขึ้นมาเองโดยการเปรียบเทียบ แต่มันสมเหตุสมผลแล้วที่จะพูดถึงคลาส Employee อย่างที่ฉันบอกไป เราได้เพิ่มหลายฟิลด์ แต่ตอนนี้มีเพียงอันสุดท้ายเท่านั้นที่น่าสนใจนั่นคืองาน นี่คืองาน List<Task>ซึ่งจะเริ่มต้นทันทีด้วย ArrayList ว่างและทำเครื่องหมายด้วยคำอธิบายประกอบหลายรายการ 1. @OneToManyอย่างที่ผมบอกไปแล้ว นี่จะเป็นอัตราส่วนของพนักงานต่องานของเรา 2. @JoinColumn - คอลัมน์ที่จะรวมเอนทิตี ในกรณีนี้ คอลัมน์ Employee_id จะถูกสร้างขึ้นในตาราง Task โดยชี้ไปที่ id ของพนักงานของเรา ซึ่งจะทำหน้าที่เป็น ForeighnKey แม้ว่าชื่อจะดูศักดิ์สิทธิ์ แต่คุณก็สามารถตั้งชื่อคอลัมน์ได้ตามต้องการ สถานการณ์จะซับซ้อนขึ้นเล็กน้อยหากคุณจำเป็นต้องใช้ไม่เพียงแค่ ID แต่เป็นคอลัมน์จริงบางประเภท เราจะพูดถึงหัวข้อนี้ในภายหลัง 3. คุณอาจสังเกตเห็นคำอธิบายประกอบใหม่เหนือ id - @JsonIgnore เนื่องจาก id เป็นหน่วยงานภายในของเรา เราจึงไม่จำเป็นต้องส่งคืนให้แก่ลูกค้า 4. @NotBlank เป็นคำอธิบายประกอบพิเศษสำหรับการตรวจสอบ ซึ่งระบุว่าฟิลด์ต้องไม่เป็นค่าว่างหรือสตริงว่าง 5. @Column(unique = true) บอกว่าคอลัมน์นี้ต้องมีค่าไม่ซ้ำกัน ดังนั้นเราจึงมีสองเอนทิตีอยู่แล้ว พวกมันเชื่อมโยงถึงกันด้วยซ้ำ ถึงเวลาแล้วที่จะรวมสิ่งเหล่านี้เข้ากับโปรแกรมของเรา - ไปจัดการกับบริการและตัวควบคุมกันดีกว่า ก่อนอื่น เรามาลบ stub ของเราออกจากเมธอด getAllEmployees() แล้วเปลี่ยนให้เป็นสิ่งที่ใช้งานได้จริง:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
ดังนั้นพื้นที่เก็บข้อมูลของเราจะรวบรวมทุกสิ่งที่มีอยู่ในฐานข้อมูลและมอบให้กับเรา เป็นที่น่าสังเกตว่ามันจะรับรายการงานด้วย แต่การกวาดล้างมันน่าสนใจอย่างแน่นอน แต่จะเกิดอะไรขึ้นถ้าไม่มีอะไรอยู่ตรงนั้น? ถูกต้อง นั่นหมายความว่าเราต้องหาวิธีใส่อะไรลงไปตรงนั้น ก่อนอื่น เรามาเขียนวิธีการใหม่ในคอนโทรลเลอร์ของเรากันก่อน
@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();
    }
นี่คือ @PostMapping เช่น มันประมวลผลคำขอ POST ที่มาถึงจุดสิ้นสุดของพนักงานของเรา โดยทั่วไป ฉันคิดว่าเนื่องจากคำขอทั้งหมดไปยังคอนโทรลเลอร์นี้จะมาที่จุดสิ้นสุดจุดเดียว มาทำให้เรื่องนี้ง่ายขึ้นสักหน่อย จำการตั้งค่าที่ดีของเราใน application.yml ได้ไหม? มาแก้ไขกันเถอะ ให้ส่วนของแอปพลิเคชันมีลักษณะดังนี้:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
สิ่งนี้ให้อะไรเราบ้าง? ความจริงที่ว่าในคอนโทรลเลอร์เราสามารถลบการแมปสำหรับแต่ละวิธีเฉพาะได้และจุดสิ้นสุดจะถูกตั้งค่าที่ระดับคลาสในคำ อธิบายประกอบ @RequestMapping("${application.endpoint.employee}") นี่คือความสวยงามในตอนนี้ ผู้ควบคุมของเรา:
@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();
    }
}
อย่างไรก็ตาม เรามาต่อกันดีกว่า จะเกิดอะไรขึ้นในเมธอด createOrUpdateEmployee แน่นอนว่า EmployeeService ของเรามีวิธีบันทึกซึ่งควรรับผิดชอบงานบันทึกทั้งหมด เห็นได้ชัดว่าวิธีนี้สามารถส่งข้อยกเว้นด้วยชื่อที่อธิบายตนเองได้ เหล่านั้น. กำลังดำเนินการตรวจสอบความถูกต้องบางอย่าง และคำตอบนั้นขึ้นอยู่กับผลการตรวจสอบโดยตรง ไม่ว่าจะเป็น 201 Create หรือ 400 badRequest พร้อมรายการสิ่งที่ผิดพลาด เมื่อมองไปข้างหน้า นี่คือบริการตรวจสอบใหม่ของเรา โดยจะตรวจสอบข้อมูลขาเข้าว่ามีฟิลด์ที่จำเป็นหรือไม่ (จำ @NotBlank ได้ไหม) และตัดสินใจว่าข้อมูลดังกล่าวเหมาะสำหรับเราหรือไม่ ก่อนที่จะไปยังวิธีการบันทึก เรามาตรวจสอบและใช้บริการนี้กันก่อน ในการทำเช่นนี้ ฉันเสนอให้สร้างแพ็คเกจการตรวจสอบแยกต่างหากซึ่งเราจะให้บริการของเรา
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();
    }
}
ชั้นเรียนกลายเป็นเรื่องใหญ่เกินไป แต่อย่าตกใจ เราจะคิดออกตอนนี้ :) ที่นี่เราใช้เครื่องมือของไลบรารีการตรวจสอบความถูกต้องสำเร็จรูป javax.validation ไลบรารี นี้มาหาเราจากการขึ้นต่อกันใหม่ที่เรา เพิ่มไปยัง การใช้งาน build.graddle 'org.springframework.boot:spring-boot-starter -validation' บริการเพื่อนเก่าของเราและ RequiredArgsConstructor บอกเราทุกสิ่งที่เราจำเป็นต้องรู้เกี่ยวกับคลาสนี้แล้ว นอกจากนี้ยังมีฟิลด์ตรวจสอบขั้นสุดท้ายส่วนตัวอีกด้วย เขาจะทำเวทย์มนตร์ เราได้สร้างเมธอด isValidEmployee ซึ่งเราสามารถส่งผ่านเอนทิตี Employee ได้ เมธอดนี้จะส่ง ValidationException ซึ่งเราจะเขียนในภายหลังเล็กน้อย ใช่ นี่จะเป็นข้อยกเว้นที่กำหนดเองสำหรับความต้องการของเรา การใช้เมธอด validator.validate(employee) เราจะได้รับรายการของออบเจ็กต์ ConstraintViolation - ทั้งหมดที่ไม่สอดคล้องกับคำอธิบายประกอบการตรวจสอบที่เราเพิ่มไว้ก่อนหน้านี้ ตรรกะเพิ่มเติมนั้นง่ายมาก หากรายการนี้ไม่ว่างเปล่า (เช่น มีการละเมิด) เราจะสร้างข้อยกเว้นและสร้างรายการการละเมิด - วิธี buildViolationsList โปรดทราบว่านี่เป็นวิธีทั่วไป เช่น สามารถทำงานร่วมกับรายการการละเมิดของออบเจ็กต์ต่างๆ ได้ - มันอาจจะมีประโยชน์ในอนาคตหากเราตรวจสอบสิ่งอื่น วิธีการนี้ทำอะไรได้จริง? เมื่อใช้สตรีม API เราจะผ่านรายการการละเมิด เราเปลี่ยนการละเมิดแต่ละครั้งในวิธีการแผนที่ให้เป็นวัตถุการละเมิดของเราเอง และรวบรวมวัตถุผลลัพธ์ทั้งหมดไว้ในรายการ เรากำลังส่งคืนเขา คุณถามอะไรอีกบ้างที่เป็นการละเมิดของเรา? นี่เป็นบันทึก ง่ายๆ
public record Violation(String property, String message) {}
บันทึกเป็นนวัตกรรมพิเศษใน Java ในกรณีที่คุณต้องการวัตถุที่มีข้อมูล โดยไม่มีตรรกะหรือสิ่งอื่นใด แม้ว่าตัวฉันเองยังไม่เข้าใจว่าเหตุใดจึงทำเช่นนี้ แต่บางครั้งมันก็ค่อนข้างสะดวก จะต้องสร้างเป็นไฟล์แยกต่างหากเหมือนคลาสทั่วไป กลับไปที่ ValidationException แบบกำหนดเอง - มีลักษณะดังนี้:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
มันเก็บรายการการละเมิดทั้งหมด คำอธิบายประกอบลอมบอก - Getter ถูกแนบไปกับรายการ และผ่านคำอธิบายประกอบลอมบอกอื่น เราได้ "นำไปใช้" ตัวสร้างที่จำเป็น :) เป็นที่น่าสังเกตว่าที่นี่ฉันไม่ได้ใช้พฤติกรรมของ isValid อย่างถูกต้องนัก ... วิธีการจะส่งกลับค่าจริงหรือข้อยกเว้น แต่ก็คุ้มค่าที่จะจำกัดตัวเองให้เป็นค่าเท็จตามปกติ วิธีการยกเว้นเกิดขึ้นเนื่องจากฉันต้องการส่งคืนข้อผิดพลาดนี้ไปยังไคลเอนต์ ซึ่งหมายความว่าฉันต้องส่งคืนอย่างอื่นที่ไม่ใช่จริงหรือเท็จจากวิธีบูลีน ในกรณีของวิธีการตรวจสอบภายในเพียงอย่างเดียว ไม่จำเป็นต้องทิ้งข้อยกเว้น โดยจะต้องมีการบันทึกที่นี่ อย่างไรก็ตาม กลับมาที่ EmployeeService ของเราอีกครั้ง เรายังคงต้องเริ่มบันทึก Object กัน :) มาดูกันว่า Class นี้มีลักษณะอย่างไร:
@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());
    }
}
แจ้งให้ทราบคุณสมบัติสุดท้ายใหม่ส่วนตัวสุดท้าย ValidationService validationService; วิธีการบันทึกนั้นถูกทำเครื่องหมายด้วยคำอธิบายประกอบ @Transactional ดังนั้นหากได้รับ RuntimeException การเปลี่ยนแปลงจะถูกย้อนกลับ ก่อนอื่น เราตรวจสอบข้อมูลขาเข้าโดยใช้บริการที่เราเพิ่งเขียน หากทุกอย่างเป็นไปอย่างราบรื่น เราจะตรวจสอบว่ามีพนักงานอยู่ในฐานข้อมูลหรือไม่ (โดยใช้หมายเลขเฉพาะ) ถ้าไม่ เราจะบันทึกอันใหม่ ถ้ามี เราจะอัปเดตฟิลด์ในชั้นเรียน โอ้ใช่แล้วเราจะตรวจสอบได้อย่างไร? ใช่ มันง่ายมาก เราได้เพิ่มวิธีการใหม่ไปยังพื้นที่เก็บข้อมูลของพนักงาน:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
มีอะไรน่าสังเกต? ฉันไม่ได้เขียนตรรกะหรือการสืบค้น SQL ใด ๆ แม้ว่าจะมีให้ที่นี่ก็ตาม Spring เพียงแค่อ่านชื่อของวิธีการ ก็กำหนดสิ่งที่ฉันต้องการ - ค้นหา ByUniqueNumber และส่งสตริงที่เกี่ยวข้องไปยังวิธีการ กลับไปที่การอัปเดตฟิลด์ - ที่นี่ฉันตัดสินใจใช้สามัญสำนึกและอัปเดตเฉพาะแผนกเงินเดือนและงานเนื่องจากการเปลี่ยนชื่อแม้ว่าจะเป็นสิ่งที่ยอมรับได้ แต่ก็ยังไม่ธรรมดานัก และการเปลี่ยนวันจ้างงานโดยทั่วไปถือเป็นประเด็นที่ถกเถียงกัน มาทำอะไรที่นี่ดี? รวมรายการงานเข้าด้วยกัน แต่เนื่องจากเรายังไม่มีงานและไม่ทราบวิธีแยกแยะงานเหล่านั้น เราจึงละทิ้ง TODO มาลองเปิดตัวแฟรงเกนสไตน์ของเรากัน ถ้าฉันไม่ลืมอธิบายอะไรมันก็ควรจะได้ผล แต่ก่อนอื่น นี่คือแผนผังคลาสที่เราได้รับ: REST API และการตรวจสอบข้อมูล - 1 คลาสที่ได้รับการแก้ไขจะถูกเน้นด้วยสีน้ำเงิน คลาสใหม่จะถูกเน้นด้วยสีเขียว คุณสามารถรับสิ่งบ่งชี้ดังกล่าวได้หากคุณทำงาน มีที่เก็บ git แต่ git ไม่ใช่หัวข้อสำหรับบทความของเรา ดังนั้นเราจะไม่พูดถึงมันอีกต่อไป ดังนั้น ในขณะนี้ เรามีจุดสิ้นสุดหนึ่งจุดที่รองรับสองวิธี GET และ POST อย่างไรก็ตาม มีข้อมูลที่น่าสนใจเกี่ยวกับจุดสิ้นสุด เหตุใดเราจึงไม่จัดสรรตำแหน่งข้อมูลแยกกันสำหรับ GET และ POST เช่น getAllEmployees หรือ createEmployees ทุกอย่างง่ายมาก - การมีจุดเดียวสำหรับคำขอทั้งหมดจะสะดวกกว่ามาก การกำหนดเส้นทางเกิดขึ้นตามวิธี HTTP และใช้งานง่าย ไม่จำเป็นต้องจำรูปแบบทั้งหมดของ getAllEmployees, getEmployeeByName, รับ... อัปเดต... สร้าง... ลบ... มาทดสอบสิ่งที่เราได้รับกันดีกว่า ฉันได้เขียนไปแล้วในบทความก่อนหน้านี้ว่าเราจะต้องมีบุรุษไปรษณีย์และได้เวลาติดตั้งแล้ว ในอินเทอร์เฟซของโปรแกรม เราสร้างคำขอ POST ใหม่ REST API และการตรวจสอบข้อมูล - 2 และพยายามส่ง หากทุกอย่างเป็นไปด้วยดี เราจะเห็นสถานะ 201 ทางด้านขวาของหน้าจอ แต่ตัวอย่างเช่น เมื่อส่งสิ่งเดียวกัน แต่ไม่มีหมายเลขเฉพาะ (ซึ่งเรามีการตรวจสอบ) ฉันได้รับคำตอบที่ต่างออกไป: เรามาตรวจสอบว่า REST API และการตรวจสอบข้อมูล - 3 การเลือกทั้งหมดของเราทำงานอย่างไร - เราสร้างวิธี GET สำหรับจุดสิ้นสุดเดียวกันแล้วส่งไป . ฉันหวังเป็นอย่าง ยิ่ง REST API และการตรวจสอบข้อมูล - 4 ว่าทุกอย่างจะได้ผลสำหรับคุณเช่นเดียวกับที่ทำเพื่อฉัน แล้วพบกันใหม่บทความหน้า
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION