JavaRush /Java Blogu /Random-AZ /REST API və Data Validation
Денис
Səviyyə
Киев

REST API və Data Validation

Qrupda dərc edilmişdir
Birinci hissəyə keçid: REST API və növbəti test tapşırığı Yaxşı, proqramımız işləyir, ondan bir növ cavab ala bilərik, lakin bu bizə nə verir? Heç bir faydalı iş görmür. Dediyimiz kimi, gəlin faydalı bir şey həyata keçirək. Əvvəlcə build.gradle-a bir neçə yeni asılılıq əlavə edək, onlar bizim üçün faydalı olacaq:
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ə biz emal etməli olduğumuz faktiki məlumatlardan başlayacağıq. Gəlin əzmkarlıq paketimizə qayıdaq və obyekti doldurmağa başlayaq. Yadınızdadırsa, biz onu yalnız bir sahə ilə mövcud olmaq üçün buraxmışdıq və sonra avtomatik `@GeneratedValue(strategy = GenerationType.IDENTITY)` vasitəsilə yaradılır. Gəlin birinci fəsildəki texniki xüsusiyyətləri xatırlayaq:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
İlk dəfədir ki, kifayət qədər sahəmiz var, ona görə də onu həyata keçirməyə başlayaq. İlk üç sahə suallar doğurmur - bunlar adi xətlərdir, lakin maaş sahəsi artıq təklif edir. Niyə faktiki xətt? Real işdə bu da olur, müştəri gəlir sizə deyir - mən sizə bu yükü göndərmək istəyirəm, siz onu emal edirsiniz. Siz, əlbəttə ki, çiyinlərinizi çəkə və bunu edə bilərsiniz, razılığa gəlməyə cəhd edə və lazımi formatda məlumatları ötürməyin daha yaxşı olduğunu izah edə bilərsiniz. Təsəvvür edək ki, ağıllı müştəri ilə rastlaşdıq və razılaşdıq ki, nömrələri rəqəmsal formatda ötürmək daha yaxşıdır və söhbət puldan getdiyi üçün Qoy Double olsun. Faydalı yükümüzün növbəti parametri işə qəbul tarixi olacaq, müştəri onu razılaşdırılmış formatda bizə göndərəcək: yyyy-aa-gg, burada y illər üçün, m günlər üçün, d isə günlər üçün gözlənilir - 2022- 08-12. Hazırda son sahə müştəriyə tapşırılan tapşırıqların siyahısı olacaq. Aydındır ki, Tapşırıq verilənlər bazamızdakı başqa bir obyektdir, lakin biz bu barədə hələ çox şey bilmirik, ona görə də əvvəllər İşçi ilə etdiyimiz kimi ən əsas obyekti yaradacağıq. İndi fərz edə biləcəyimiz yeganə şey, bir işçiyə birdən çox tapşırıq verilə biləcəyidir, buna görə də biz bir-çox nisbəti adlanan Bir-Çox yanaşmasını tətbiq edəcəyik. Ətraflı danışarkən, işçi cədvəlindəki bir qeyd tapşırıqlar cədvəlindəki bir neçə qeydə uyğun ola bilər . Mən də unikalNumber sahəsi kimi bir şey əlavə etmək qərarına gəldim ki, bir işçini digərindən aydın şəkildə fərqləndirə bilək. Hal-hazırda İşçi sinifimiz belə görünür:
@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<>();
}
Tapşırıq obyekti üçün aşağıdakı sinif yaradılmışdır:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Dediyim kimi, Tapşırıqda yeni bir şey görməyəcəyik, bu sinif üçün yeni bir repozitoriya da yaradıldı, bu da İşçi üçün deponun surətidir - onu verməyəcəyəm, bənzətmə ilə özünüz yarada bilərsiniz. Amma İşçi sinfi haqqında danışmağın mənası var. Dediyim kimi, biz bir neçə sahə əlavə etdik, lakin indi yalnız sonuncusu maraq doğurur - tapşırıqlar. Bu, List<Task> tapşırıqlarıdır , dərhal boş ArrayList ilə işə salınır və bir neçə annotasiya ilə qeyd olunur. 1. @OneToMany Dediyim kimi, bu bizim işçilərin tapşırıqlara nisbəti olacaq. 2. @JoinColumn - qurumların birləşdiriləcəyi sütun. Bu halda, Tapşırıq cədvəlində işçimizin id-sini göstərən bir işçi_id sütunu yaradılacaq; o, bizə ForeighnKey kimi xidmət edəcək Adın görünən müqəddəsliyinə baxmayaraq, sütunu istədiyiniz hər hansı bir ad verə bilərsiniz. Yalnız şəxsiyyət vəsiqəsindən deyil, bir növ real sütundan istifadə etməlisinizsə, vəziyyət bir az daha mürəkkəb olacaq, bu mövzuya daha sonra toxunacağıq. 3. Siz həmçinin id-nin üstündə yeni annotasiyaya diqqət yetirmiş ola bilərsiniz - @JsonIgnore. İd bizim daxili qurumumuz olduğundan, onu mütləq müştəriyə qaytarmağa ehtiyacımız yoxdur. 4. @NotBlank yoxlama üçün xüsusi annotasiyadır, bu sahənin null və ya boş sətir olmamalı olduğunu bildirir 5. @Column(unique = true) deyir ki, bu sütunun unikal dəyərləri olmalıdır. Deməli, bizim artıq iki subyektimiz var, onlar hətta bir-birinə bağlıdırlar. Onları proqramımıza inteqrasiya etməyin vaxtı gəldi - gedək xidmətlər və nəzarətçilər ilə məşğul olaq. Əvvəlcə getAllEmployees() metodundan stubumuzu çıxaraq və onu həqiqətən işləyən bir şeyə çevirək:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Beləliklə, depomuz verilənlər bazasında mövcud olan hər şeyi toplayıb bizə verəcəkdir. Maraqlıdır ki, o, tapşırıq siyahısını da götürəcəkdir. Ancaq onu çıxarmaq, əlbəttə ki, maraqlıdır, amma orada heç bir şey yoxdursa, onu çıxarmaq nədir? Düzdü, bu o deməkdir ki, biz ora nəyisə necə yerləşdirəcəyimizi anlamalıyıq. Əvvəlcə kontrollerimizdə yeni metod yazaq.
@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();
    }
Bu @PostMapping, yəni. o, əməkdaşlarımızın son nöqtəsinə gələn POST sorğularını emal edir. Ümumiyyətlə, düşündüm ki, bu nəzarətçiyə edilən bütün sorğular bir son nöqtəyə çatacaq, gəlin bunu bir az sadələşdirək. application.yml-dəki gözəl parametrlərimizi xatırlayırsınız? Gəlin onları düzəldək. Tətbiq bölməsi indi belə görünsün:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Bu bizə nə verir? Nəzarətçidə hər bir xüsusi metod üçün xəritələşdirməni silə biləcəyimiz və son nöqtənin @RequestMapping("${application.endpoint.employee}") annotasiyasında sinif səviyyəsində təyin olunması faktı budur ki, indi bizim gözəllik budur. Nəzarə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();
    }
}
Bununla belə, davam edək. CreateOrUpdateEmployee metodunda tam olaraq nə baş verir? Aydındır ki, bizim staffService-də bütün qənaət işlərinə cavabdeh olan bir qənaət metodu var. Bu metodun özünü izah edən bir adla bir istisna ata biləcəyi də açıqdır. Bunlar. bir növ doğrulama aparılır. Cavab birbaşa yoxlama nəticələrindən asılıdır, bunun 201 Created və ya səhv olanların siyahısı ilə 400 badRequest olması. İrəliyə baxsaq, bu, bizim yeni doğrulama xidmətimizdir, o, daxil olan məlumatları tələb olunan sahələrin olub-olmadığını yoxlayır (@NotBlank yadda saxla?) və belə məlumatların bizim üçün uyğun olub-olmamasına qərar verir. Saxlama metoduna keçməzdən əvvəl gəlin bu xidməti təsdiq edək və həyata keçirək. Bunun üçün xidmətimizi yerləşdirəcəyimiz ayrıca yoxlama paketi yaratmağı təklif edirəm.
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();
    }
}
Sinif çox böyük oldu, amma panik etməyin, indi anlayacağıq :) Burada biz hazır validasiya kitabxanasının alətlərindən istifadə edirik javax.validation Bu kitabxana bizə yeni asılılıqlardan gəldi. build.graddle tətbiqinə əlavə edildi 'org.springframework.boot:spring-boot-starter -validation' Köhnə dostlarımız Xidmət və RequiredArgsConstructor Artıq bu sinif haqqında bilmək lazım olan hər şeyi bizə deyirlər, həmçinin şəxsi yekun validator sahəsi var. O, sehr edəcək. Biz isValidEmployee metodunu yaratdıq, onun içinə Employee obyektini keçə bilərik; bu üsul bir az sonra yazacağımız ValidationException atır. Bəli, bu, ehtiyaclarımız üçün xüsusi bir istisna olacaq. validator.validate(işçi) metodundan istifadə edərək biz ConstraintViolation obyektlərinin siyahısını əldə edəcəyik - əvvəllər əlavə etdiyimiz yoxlama annotasiyaları ilə bütün uyğunsuzluqlar. Sonrakı məntiq sadədir, əgər bu siyahı boş deyilsə (yəni pozuntular var), biz bir istisna atırıq və pozuntuların siyahısını tərtib edirik - buildViolationsList metodu Xahiş edirik unutmayın ki, bu Ümumi metoddur, yəni. müxtəlif obyektlərin pozuntularının siyahıları ilə işləyə bilər - başqa bir şeyi təsdiqləsək, gələcəkdə faydalı ola bilər. Bu üsul əslində nə edir? Axın API-dən istifadə edərək, pozuntuların siyahısından keçirik. Xəritə metodunda hər bir pozuntunu öz pozuntu obyektimizə çeviririk və nəticədə bütün obyektləri siyahıya yığırıq. Biz onu geri qaytarırıq. Soruşursunuz ki, bizim öz pozulma obyektimiz daha nədir? Budur sadə bir rekord
public record Violation(String property, String message) {}
Rekordlar Java-da belə xüsusi yeniliklərdir, əgər sizə heç bir məntiq və ya başqa bir şey olmadan verilənləri olan bir obyekt lazımdırsa. Mən özüm bunun niyə edildiyini hələ başa düşməsəm də, bəzən bu olduqca əlverişli bir şeydir. Adi sinif kimi ayrıca faylda yaradılmalıdır. Fərdi ValidationException-a qayıdırıq - bu belə görünür:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
O, bütün pozuntuların siyahısını saxlayır, Lombok annotasiyası - Getter siyahıya əlavə olunur və başqa bir Lombok annotasiyası vasitəsilə biz tələb olunan konstruktoru "həyata keçirdik" :) Burada qeyd etmək lazımdır ki, isValid-in davranışını tamamilə düzgün yerinə yetirmirəm. ... metodu, ya doğru, ya da istisna qaytarır, lakin özümüzü adi False ilə məhdudlaşdırmağa dəyər. İstisna yanaşması ona görə edilir ki, mən bu xətanı müştəriyə qaytarmaq istəyirəm, yəni mən boolean metodundan doğru və ya yalandan başqa bir şey qaytarmalıyam. Sırf daxili yoxlama metodları vəziyyətində, istisna atmağa ehtiyac yoxdur, burada giriş tələb olunacaq. Bununla belə, EmployeeService-ə qayıdaq, biz hələ də obyektləri saxlamağa başlamalıyıq :) Gəlin görək bu sinif indi necə görünür:
@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());
    }
}
Yeni son mülkiyyət özəl yekun ValidationService validationService-ə diqqət yetirin; Saxlama metodunun özü @Transactional annotasiyası ilə qeyd olunur ki, RuntimeException qəbul edilərsə, dəyişikliklər geri qaytarılır. İlk növbədə, biz yeni yazdığımız xidmətdən istifadə edərək daxil olan məlumatları təsdiqləyirik. Hər şey qaydasındadırsa, verilənlər bazasında (unikal nömrədən istifadə etməklə) mövcud işçinin olub olmadığını yoxlayırıq. Yoxdursa, yenisini saxlayırıq, varsa, sinifdəki sahələri yeniləyirik. Bəli, əslində necə yoxlayaq? Bəli, çox sadədir, biz İşçilərin anbarına yeni üsul əlavə etdik:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Nə diqqətəlayiqdir? Mən heç bir məntiq və ya SQL sorğusu yazmadım, baxmayaraq ki, bu burada mövcuddur. Spring, sadəcə olaraq metodun adını oxumaqla, nə istədiyimi müəyyənləşdirir - ByUniqueNumber tapın və müvafiq sətri metoda ötürün. Sahələrin yenilənməsinə qayıtmaq - burada mən sağlam düşüncədən istifadə etmək və yalnız şöbəni, əmək haqqı və vəzifələri yeniləmək qərarına gəldim, çünki adın dəyişdirilməsi məqbul bir şey olsa da, hələ də çox yaygın deyil. Və işə qəbul tarixinin dəyişdirilməsi ümumiyyətlə mübahisəli məsələdir. Burada nə etmək yaxşı olardı? Tapşırıq siyahılarını birləşdirin, lakin hələ tapşırıqlarımız olmadığından və onları necə ayırd edəcəyimizi bilmədiyimiz üçün TODO-dan ayrılacağıq. Gəlin Frankenşteynimizi işə salmağa çalışaq. Bir şeyi təsvir etməyi unutmamışamsa, işləməlidir, amma əvvəlcə əldə etdiyimiz sinif ağacı budur: REST API və Data Validation - 1 Dəyişdirilmiş siniflər mavi rənglə, yeniləri yaşıl rənglə vurğulanır, işləsəniz belə göstəricilər əldə edilə bilər. git repozitoriyası ilə, lakin git məqaləmizin mövzusu deyil, ona görə də onun üzərində dayanmayacağıq. Beləliklə, hazırda iki GET və POST metodunu dəstəkləyən bir son nöqtəmiz var. Yeri gəlmişkən, son nöqtə haqqında bəzi maraqlı məlumatlar. Niyə, məsələn, getAllEmployees və ya createEmployees kimi GET və POST üçün ayrıca son nöqtələr ayırmadıq? Hər şey olduqca sadədir - bütün sorğular üçün bir nöqtənin olması daha rahatdır. Marşrutlaşdırma HTTP metodu əsasında baş verir və o, intuitivdir, getAllEmployees, getEmployeeByName-in bütün variasiyalarını yadda saxlamağa ehtiyac yoxdur, alın... yeniləyin... yaradın... silin... Əldə etdiklərimizi yoxlayaq. Əvvəlki məqalədə artıq yazmışdım ki, bizə Postman lazımdır və onu quraşdırmağın vaxtı gəldi. Proqram interfeysində biz yeni POST sorğusu yaradırıq REST API və Data Validation - 2 və onu göndərməyə çalışırıq. Hər şey yaxşı olarsa, ekranın sağ tərəfində Status 201 alacağıq. Ancaq məsələn, eyni şeyi göndərib, lakin unikal nömrə olmadan (təsdiqimiz var) fərqli cavab alıram: Gəlin REST API və Data Validation - 3 tam seçimimizin necə işlədiyini yoxlayaq - eyni son nöqtə üçün GET metodu yaradırıq və onu göndəririk. . REST API və Data Validation - 4 Ümid edirəm ki, hər şey mənim üçün olduğu kimi sizin üçün də uğurlu oldu və növbəti məqalədə görüşərik .
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION