JavaRush /Java Blog /Random-TL /REST API at Pagpapatunay ng Data
Денис
Antas
Киев

REST API at Pagpapatunay ng Data

Nai-publish sa grupo
Mag-link sa unang bahagi: REST API at ang susunod na gawain sa pagsubok Buweno, gumagana ang aming aplikasyon, makakakuha tayo ng ilang uri ng tugon mula rito, ngunit ano ang ibinibigay nito sa atin? Hindi ito gumagawa ng anumang kapaki-pakinabang na gawain. Hindi pa nasabi kaysa tapos na, ipatupad natin ang isang bagay na kapaki-pakinabang. Una sa lahat, magdagdag tayo ng ilang bagong dependency sa ating build.gradle, magiging kapaki-pakinabang ang mga ito sa atin:
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'
At magsisimula tayo sa aktwal na data na dapat nating iproseso. Bumalik tayo sa aming persistence package at simulan ang pagpuno sa entity. Gaya ng naaalala mo, hinayaan namin itong umiral nang may isang field lang, at pagkatapos ay nabuo ang auto sa pamamagitan ng `@GeneratedValue(strategy = GenerationType.IDENTITY)` Tandaan natin ang mga teknikal na detalye mula sa unang kabanata:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Mayroon tayong sapat na mga field sa unang pagkakataon, kaya simulan natin itong ipatupad. Ang unang tatlong larangan ay hindi nagtataas ng mga katanungan - ito ay mga ordinaryong linya, ngunit ang patlang ng suweldo ay nagpapahiwatig na. Bakit ang aktwal na linya? Sa totoong trabaho, nangyayari rin ito, may customer na lumapit sa iyo at nagsasabing - Gusto kong ipadala sa iyo ang payload na ito, at iproseso mo ito. Maaari mong, siyempre, ipakibit ang iyong mga balikat at gawin ito, maaari mong subukan na magkaroon ng isang kasunduan at ipaliwanag na mas mahusay na ipadala ang data sa kinakailangang format. Isipin natin na nakatagpo kami ng isang matalinong kliyente at nagkasundo na mas mainam na magpadala ng mga numero sa numeric na format, at dahil pera ang pinag-uusapan, hayaan itong maging Doble. Ang susunod na parameter ng aming payload ay ang petsa ng pag-hire, ipapadala ito sa amin ng kliyente sa napagkasunduang format: yyyy-mm-dd, kung saan ang y ang may pananagutan sa mga taon, m para sa mga araw, at ang d ay inaasahan para sa mga araw - 2022- 08-12. Ang huling field sa ngayon ay isang listahan ng mga gawain na itinalaga sa kliyente. Malinaw, ang Task ay isa pang entity sa aming database, ngunit wala pa kaming masyadong alam tungkol dito, kaya gagawa kami ng pinakapangunahing entity tulad ng ginawa namin sa Empleyado dati. Ang tanging bagay na maaari nating ipagpalagay ngayon ay higit sa isang gawain ang maaaring italaga sa isang empleyado, kaya ilalapat natin ang tinatawag na One-To-Many na diskarte, isang one-to-many ratio. Higit na partikular, ang isang tala sa talahanayan ng empleyado ay maaaring tumugma sa ilang mga tala mula sa talahanayan ng mga gawain . Nagpasya din akong magdagdag ng isang bagay bilang isang uniqueNumber field, upang malinaw naming makilala ang isang empleyado mula sa isa pa. Sa ngayon, ganito ang hitsura ng aming Employee class:
@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<>();
}
Ang sumusunod na klase ay nilikha para sa Task entity:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Gaya ng sinabi ko, wala tayong makikitang bago sa Task, isang bagong repositoryo ang ginawa din para sa klase na ito, na isang kopya ng repository para sa Empleyado - Hindi ko ito ibibigay, maaari mo itong likhain sa pamamagitan ng pagkakatulad. Ngunit makatuwirang pag-usapan ang tungkol sa klase ng Empleyado. Tulad ng sinabi ko, nagdagdag kami ng ilang mga patlang, ngunit ang huli lamang ang interesado ngayon - mga gawain. Isa itong List<Task> tasks , agad itong sinisimulan gamit ang isang walang laman na ArrayList at minarkahan ng ilang anotasyon. 1. @OneToMany Gaya ng sinabi ko, ito ang magiging ratio natin ng mga empleyado sa mga gawain. 2. @JoinColumn - ang column kung saan sasali ang mga entity. Sa kasong ito, gagawa ng column na employee_id sa talahanayan ng Task na tumuturo sa id ng aming empleyado; ito ay magsisilbi sa amin bilang ForeighnKey Sa kabila ng tila kasagrado ng pangalan, maaari mong pangalanan ang column kahit anong gusto mo. Ang sitwasyon ay magiging mas kumplikado kung kailangan mong gumamit ng hindi lamang isang ID, ngunit isang uri ng totoong column; tatalakayin namin ang paksang ito sa ibang pagkakataon. 3. Maaaring may napansin ka ring bagong anotasyon sa itaas ng id - @JsonIgnore. Dahil ang id ay ang aming panloob na entity, hindi namin kinakailangang ibalik ito sa kliyente. 4. Ang @NotBlank ay isang espesyal na anotasyon para sa pagpapatunay, na nagsasabing hindi dapat null ang field o ang walang laman na string 5. Sinasabi ng @Column(unique = true) na ang column na ito ay dapat may mga natatanging value. So, dalawa na ang entity namin, konektado pa nga sila sa isa't isa. Dumating na ang oras upang isama ang mga ito sa aming programa - harapin natin ang mga serbisyo at controller. Una sa lahat, alisin natin ang ating stub mula sa getAllEmployees() na paraan at gawin itong isang bagay na talagang gumagana:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Kaya, kukunin ng aming repositoryo ang lahat ng magagamit mula sa database at ibibigay ito sa amin. Kapansin-pansin na kukunin din nito ang listahan ng gawain. Ngunit ang pag-rake nito ay tiyak na kawili-wili, ngunit ano ang pag-rake nito kung wala doon? Tama, ibig sabihin, kailangan nating malaman kung paano maglagay ng isang bagay doon. Una sa lahat, magsulat tayo ng bagong paraan sa ating 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();
    }
Ito ay @PostMapping, ibig sabihin. pinoproseso nito ang mga kahilingan ng POST na dumarating sa endpoint ng aming mga empleyado. Sa pangkalahatan, naisip ko na dahil ang lahat ng mga kahilingan sa controller na ito ay darating sa isang endpoint, pasimplehin natin ito nang kaunti. Tandaan ang aming magagandang setting sa application.yml? Ayusin natin sila. Hayaang magmukhang ganito ngayon ang seksyon ng application:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Ano ang ibinibigay nito sa atin? Ang katotohanan na sa controller ay maaari nating alisin ang pagmamapa para sa bawat partikular na paraan, at ang endpoint ay itatakda sa antas ng klase sa @RequestMapping("${application.endpoint.employee}") annotation . Ito ang kagandahan ngayon sa aming 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();
    }
}
Gayunpaman, magpatuloy tayo. Ano ang eksaktong nangyayari sa paraan ng createOrUpdateEmployee? Malinaw, ang aming employeeService ay may paraan ng pag-save, na dapat na responsable para sa lahat ng gawaing pag-save. Malinaw din na ang pamamaraang ito ay maaaring magtapon ng isang pagbubukod na may sariling paliwanag na pangalan. Yung. ang ilang uri ng pagpapatunay ay isinasagawa. At ang sagot ay direktang nakasalalay sa mga resulta ng pagpapatunay, kung ito ay 201 Created o 400 badRequest na may listahan ng kung ano ang naging mali. Sa hinaharap, ito ang aming bagong serbisyo sa pagpapatunay, sinusuri nito ang papasok na data para sa pagkakaroon ng mga kinakailangang field (tandaan ang @NotBlank?) at nagpapasya kung ang naturang impormasyon ay angkop para sa amin o hindi. Bago lumipat sa paraan ng pag-save, patunayan natin at ipatupad ang serbisyong ito. Upang gawin ito, iminumungkahi kong lumikha ng isang hiwalay na pakete ng pagpapatunay kung saan ilalagay namin ang aming serbisyo.
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();
    }
}
Masyadong malaki ang klase, ngunit huwag mag-panic, aalamin natin ito ngayon :) Dito ginagamit namin ang mga tool ng ready-made validation library na javax.validation Ang library na ito ay dumating sa amin mula sa mga bagong dependency na aming idinagdag sa pagpapatupad ng build.graddle 'org.springframework.boot:spring-boot-starter -validation' Sinabi na sa amin ng aming mga dating kaibigan na Serbisyo at RequiredArgsConstructor ang lahat ng kailangan naming malaman tungkol sa klase na ito, mayroon ding pribadong final validator field. Gagawin niya ang magic. Ginawa namin ang isValidEmployee na paraan, kung saan maaari naming ipasa ang Employee entity; ang pamamaraang ito ay nagtatapon ng ValidationException, na isusulat namin sa ibang pagkakataon. Oo, magiging custom na exception ito para sa aming mga pangangailangan. Gamit ang pamamaraang validator.validate(employee), makakakuha tayo ng listahan ng mga object ng ConstraintViolation - lahat ng hindi pagkakatugma sa mga anotasyon ng validation na idinagdag namin kanina. Ang karagdagang lohika ay simple, kung ang listahang ito ay walang laman (i.e. may mga paglabag), nagtatapon kami ng eksepsiyon at bumuo ng listahan ng mga paglabag - ang paraan ng buildViolationsList Pakitandaan na ito ay isang Generic na paraan, ibig sabihin. maaaring gumana sa mga listahan ng mga paglabag sa iba't ibang bagay - maaari itong maging kapaki-pakinabang sa hinaharap kung patunayan namin ang ibang bagay. Ano ba talaga ang ginagawa ng pamamaraang ito? Gamit ang stream API, dumaan kami sa listahan ng mga paglabag. Ginagawa namin ang bawat paglabag sa paraan ng mapa sa sarili naming object ng paglabag, at kinokolekta namin ang lahat ng resultang object sa isang listahan. Ibinabalik namin siya. Ano pa ang sarili nating object of violation, tanong mo? Narito ang isang simpleng tala
public record Violation(String property, String message) {}
Ang mga rekord ay tulad ng mga espesyal na inobasyon sa Java, kung sakaling kailangan mo ng isang bagay na may data, nang walang anumang lohika o anumang bagay. Kahit na ako mismo ay hindi pa naiintindihan kung bakit ito ginawa, kung minsan ito ay medyo isang maginhawang bagay. Dapat itong gawin sa isang hiwalay na file, tulad ng isang regular na klase. Pagbabalik sa pasadyang ValidationException - ganito ang hitsura:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Nag-iimbak ito ng isang listahan ng lahat ng mga paglabag, ang Lombok annotation - Getter ay naka-attach sa listahan, at sa pamamagitan ng isa pang Lombok annotation ay "ipinatupad" namin ang kinakailangang constructor :) Ito ay nagkakahalaga na tandaan dito na hindi ko masyadong naipapatupad ang pag-uugali ng isValid ... paraan, ibinabalik nito ang alinman sa totoo o exception, ngunit sulit na limitahan ang ating sarili sa karaniwang Mali. Ginawa ang exception approach dahil gusto kong ibalik ang error na ito sa client, na nangangahulugang kailangan kong ibalik ang isang bagay maliban sa true o false mula sa boolean method. Sa kaso ng mga panloob na pamamaraan ng pagpapatunay, hindi na kailangang maghagis ng eksepsiyon; kakailanganin ang pag-log dito. Gayunpaman, bumalik tayo sa ating EmployeeService, kailangan pa rin nating simulan ang pag-save ng mga bagay :) Tingnan natin kung ano ang hitsura ng klase na ito ngayon:
@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());
    }
}
Pansinin ang bagong final property private final ValidationService validationService; Ang mismong paraan ng pag-save ay minarkahan ng @Transactional na anotasyon upang kung ang isang RuntimeException ay natanggap, ang mga pagbabago ay ibabalik. Una sa lahat, pinapatunayan namin ang papasok na data gamit ang serbisyong isinulat namin. Kung ang lahat ay naging maayos, sinusuri namin kung mayroong isang umiiral na empleyado sa database (gamit ang isang natatanging numero). Kung hindi, i-save namin ang bago, kung mayroon man, ina-update namin ang mga patlang sa klase. Oh oo, paano natin talaga susuriin? Oo, napakasimple nito, nagdagdag kami ng bagong paraan sa repositoryo ng Empleyado:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Ano ang kapansin-pansin? Hindi ako sumulat ng anumang logic o SQL query, kahit na magagamit iyon dito. Ang Spring, sa pamamagitan lamang ng pagbabasa ng pangalan ng pamamaraan, ay tinutukoy kung ano ang gusto ko - hanapin ang ByUniqueNumber at ipasa ang kaukulang string sa pamamaraan. Pagbabalik sa pag-update ng mga patlang - dito ako nagpasya na gumamit ng sentido komun at i-update lamang ang departamento, suweldo at mga gawain, dahil ang pagpapalit ng pangalan, bagaman isang katanggap-tanggap na bagay, ay hindi pa rin karaniwan. At ang pagbabago ng petsa ng pag-hire ay karaniwang isang kontrobersyal na isyu. Ano ang magandang gawin dito? Pagsamahin ang mga listahan ng gawain, ngunit dahil wala pa kaming mga gawain at hindi namin alam kung paano makilala ang mga ito, aalis kami sa TODO. Subukan nating ilunsad ang ating Frankenstein. Kung hindi ko nakalimutang ilarawan ang anumang bagay, dapat itong gumana, ngunit una, narito ang puno ng klase na nakuha namin: Ang REST API at Pagpapatunay ng Data - 1 mga klase na binago ay naka-highlight sa asul, ang mga bago ay naka-highlight sa berde, ang mga naturang indikasyon ay maaaring makuha kung nagtatrabaho ka na may git repository, ngunit hindi git ang paksa para sa aming artikulo, kaya hindi na namin ito pag-uusapan. Kaya, sa ngayon mayroon kaming isang endpoint na sumusuporta sa dalawang pamamaraan na GET at POST. Sa pamamagitan ng paraan, ilang kawili-wiling impormasyon tungkol sa endpoint. Bakit, halimbawa, hindi kami naglaan ng hiwalay na mga endpoint para sa GET at POST, gaya ng getAllEmployees o createEmployees? Ang lahat ay napaka-simple - ang pagkakaroon ng isang punto para sa lahat ng mga kahilingan ay mas maginhawa. Nangyayari ang pagruruta batay sa pamamaraan ng HTTP at ito ay intuitive, hindi na kailangang tandaan ang lahat ng variation ng getAllEmployees, getEmployeeByName, get... update... create... delete... Subukan natin kung ano ang nakuha natin. Naisulat ko na sa nakaraang artikulo na kakailanganin natin ang Postman, at oras na para i-install ito. Sa interface ng programa, lumikha kami ng bagong kahilingan sa POST REST API at Pagpapatunay ng Data - 2 at subukang ipadala ito. Kung naging maayos ang lahat, makakakuha tayo ng Status 201 sa kanang bahagi ng screen. Ngunit halimbawa, nang maipadala ang parehong bagay ngunit walang natatanging numero (kung saan mayroon kaming pagpapatunay), nakakakuha ako ng ibang sagot: REST API at Pagpapatunay ng Data - 3 Well, tingnan natin kung paano gumagana ang aming buong pagpili - lumikha kami ng GET na paraan para sa parehong endpoint at ipadala ito . REST API at Pagpapatunay ng Data - 4 Taos-puso akong umaasa na ang lahat ay gumana para sa iyo tulad ng ginawa nito para sa akin, at makita ka sa susunod na artikulo .
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION