JavaRush /Blog Java /Random-MS /API REST dan Pengesahan Data
Денис
Tahap
Киев

API REST dan Pengesahan Data

Diterbitkan dalam kumpulan
Pautan ke bahagian pertama: REST API dan tugas ujian seterusnya Baiklah, aplikasi kami berfungsi, kami boleh mendapatkan beberapa jenis tindak balas daripadanya, tetapi apakah yang diberikan oleh ini kepada kami? Ia tidak melakukan apa-apa kerja yang berguna. Tidak lama kemudian, mari kita melaksanakan sesuatu yang berguna. Pertama sekali, mari tambahkan beberapa kebergantungan baharu pada build.gradle kami, ia akan berguna kepada kami:
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'
Dan kami akan mulakan dengan data sebenar yang mesti kami proses. Mari kembali ke pakej kegigihan kami dan mula mengisi entiti. Seperti yang anda ingat, kami membiarkannya wujud dengan hanya satu medan, dan kemudian auto dijana melalui `@GeneratedValue(strategy = GenerationType.IDENTITY)` Mari kita ingat spesifikasi teknikal dari bab pertama:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Kami mempunyai medan yang mencukupi untuk kali pertama, jadi mari kita mula melaksanakannya. Tiga bidang pertama tidak menimbulkan persoalan - ini adalah garis biasa, tetapi bidang gaji sudah tidak jelas. Mengapa talian sebenar? Dalam kerja sebenar, ini juga berlaku, pelanggan datang kepada anda dan berkata - Saya mahu menghantar muatan ini kepada anda dan anda memprosesnya. Anda boleh, tentu saja, mengangkat bahu anda dan melakukannya, anda boleh cuba mencapai persetujuan dan menjelaskan bahawa lebih baik untuk menghantar data dalam format yang diperlukan. Mari bayangkan bahawa kami bertemu dengan pelanggan pintar dan bersetuju bahawa adalah lebih baik untuk menghantar nombor dalam format berangka, dan kerana kita bercakap tentang wang, biarkan ia menjadi Berganda. Parameter seterusnya bagi muatan kami ialah tarikh pengambilan pekerja, pelanggan akan menghantarnya kepada kami dalam format yang dipersetujui: yyyy-mm-dd, di mana y bertanggungjawab untuk tahun, m untuk hari dan d dijangka untuk hari - 2022- 08-12. Medan terakhir pada masa ini ialah senarai tugasan yang diberikan kepada pelanggan. Jelas sekali, Task ialah entiti lain dalam pangkalan data kami, tetapi kami masih belum mengetahui tentangnya, jadi kami akan mencipta entiti paling asas seperti yang kami lakukan dengan Pekerja sebelum ini. Satu-satunya perkara yang boleh kita anggap sekarang ialah lebih daripada satu tugas boleh diberikan kepada seorang pekerja, jadi kita akan menggunakan pendekatan yang dipanggil Satu-Ke-Ramai, nisbah satu-ke-banyak. Lebih khusus lagi, satu rekod dalam jadual pekerja boleh sepadan dengan beberapa rekod daripada jadual tugas . Saya juga memutuskan untuk menambah perkara seperti medan uniqueNumber, supaya kami dapat membezakan dengan jelas seorang pekerja daripada yang lain. Pada masa ini kelas Pekerja kami kelihatan seperti ini:
@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<>();
}
Kelas berikut telah dibuat untuk entiti Tugas:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Seperti yang saya katakan, kita tidak akan melihat apa-apa yang baharu dalam Tugasan, repositori baharu juga telah dibuat untuk kelas ini, iaitu salinan repositori untuk Pekerja - Saya tidak akan memberikannya, anda boleh menciptanya sendiri dengan analogi. Tetapi masuk akal untuk bercakap tentang kelas Pekerja. Seperti yang saya katakan, kami menambah beberapa medan, tetapi hanya yang terakhir yang menarik minat sekarang - tugas. Ini ialah List<Task> tasks , ia segera dimulakan dengan ArrayList kosong dan ditandakan dengan beberapa anotasi. 1. @OneToMany Seperti yang saya katakan, ini akan menjadi nisbah pekerja kami kepada tugas. 2. @JoinColumn - lajur yang mana entiti akan disertai. Dalam kes ini, lajur id_pekerja akan dibuat dalam jadual Tugas yang menunjuk kepada id pekerja kami; ia akan berfungsi kepada kami sebagai ForeighnKey Walaupun nama itu kelihatan suci, anda boleh menamakan lajur itu apa sahaja yang anda suka. Keadaan akan menjadi lebih rumit jika anda perlu menggunakan bukan sahaja ID, tetapi beberapa jenis lajur sebenar; kami akan menyentuh topik ini kemudian. 3. Anda juga mungkin perasan anotasi baharu di atas id - @JsonIgnore. Memandangkan id ialah entiti dalaman kami, kami tidak semestinya perlu mengembalikannya kepada pelanggan. 4. @NotBlank ialah anotasi khas untuk pengesahan, yang mengatakan bahawa medan tidak boleh kosong atau rentetan kosong 5. @Column(unique = true) mengatakan bahawa lajur ini mesti mempunyai nilai unik. Jadi, kita sudah mempunyai dua entiti, malah mereka bersambung antara satu sama lain. Masanya telah tiba untuk menyepadukan mereka ke dalam program kami - mari kita berurusan dengan perkhidmatan dan pengawal. Pertama sekali, mari kita keluarkan stub kita daripada kaedah getAllEmployees() dan ubahnya menjadi sesuatu yang benar-benar berfungsi:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Oleh itu, repositori kami akan memperoleh semua yang tersedia daripada pangkalan data dan memberikannya kepada kami. Perlu diperhatikan bahawa ia juga akan mengambil senarai tugas. Tetapi mengautnya sudah tentu menarik, tetapi apa yang mengautnya jika tiada apa-apa di sana? Betul, itu bermakna kita perlu memikirkan bagaimana untuk meletakkan sesuatu di sana. Pertama sekali, mari tulis kaedah baharu dalam pengawal kami.
@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();
    }
Ini ialah @PostMapping, i.e. ia memproses permintaan POST yang datang ke titik akhir pekerja kami. Secara umum, saya fikir kerana semua permintaan kepada pengawal ini akan sampai ke satu titik akhir, mari kita permudahkan ini sedikit. Ingat tetapan bagus kami dalam application.yml? Mari kita betulkan. Biarkan bahagian aplikasi sekarang kelihatan seperti ini:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Apa yang diberikan ini kepada kita? Hakikat bahawa dalam pengawal kita boleh mengalih keluar pemetaan untuk setiap kaedah tertentu, dan titik akhir akan ditetapkan pada peringkat kelas dalam anotasi @RequestMapping("${application.endpoint.employee}") . Inilah keindahannya sekarang dalam Pengawal kami:
@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();
    }
}
Walau bagaimanapun, mari kita teruskan. Apakah sebenarnya yang berlaku dalam kaedah createOrUpdateEmployee? Jelas sekali, employeeService kami mempunyai kaedah simpan, yang sepatutnya bertanggungjawab untuk semua kerja penjimatan. Ia juga jelas bahawa kaedah ini boleh membuang pengecualian dengan nama yang jelas. Itu. beberapa jenis pengesahan sedang dijalankan. Dan jawapannya bergantung terus pada hasil pengesahan, sama ada 201 Created atau 400 badRequest dengan senarai perkara yang salah. Melihat ke hadapan, ini adalah perkhidmatan pengesahan baharu kami, ia menyemak data masuk untuk kehadiran medan yang diperlukan (ingat @NotBlank?) dan memutuskan sama ada maklumat tersebut sesuai untuk kami atau tidak. Sebelum beralih kepada kaedah penjimatan, mari kita sahkan dan laksanakan perkhidmatan ini. Untuk melakukan ini, saya mencadangkan untuk membuat pakej pengesahan yang berasingan di mana kami akan meletakkan perkhidmatan kami.
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();
    }
}
Kelas ternyata terlalu besar, tetapi jangan panik, kami akan memikirkannya sekarang :) Di sini kami menggunakan alat pustaka pengesahan siap pakai javax.validation Pustaka ini datang kepada kami daripada dependensi baharu yang kami ditambahkan pada pelaksanaan build.graddle 'org.springframework.boot:spring-boot-starter -validation' Rakan lama kami Service dan RequiredArgsConstructor Sudah memberitahu kami semua yang kami perlu tahu tentang kelas ini, terdapat juga medan validator akhir peribadi. Dia akan melakukan sihir. Kami mencipta kaedah isValidEmployee, di mana kami boleh lulus entiti Pekerja; kaedah ini mengeluarkan Pengesahan Pengesahan, yang akan kami tulis sedikit kemudian. Ya, ini akan menjadi pengecualian tersuai untuk keperluan kami. Menggunakan kaedah validator.validate(pekerja), kami akan mendapat senarai objek ConstraintViolation - semua ketidakkonsistenan tersebut dengan anotasi pengesahan yang kami tambahkan sebelum ini. Logik selanjutnya adalah mudah, jika senarai ini tidak kosong (iaitu terdapat pelanggaran), kami membuang pengecualian dan membina senarai pelanggaran - kaedah buildViolationsList Sila ambil perhatian bahawa ini ialah kaedah Generik, iaitu. boleh bekerja dengan senarai pelanggaran objek yang berbeza - ia mungkin berguna pada masa hadapan jika kami mengesahkan sesuatu yang lain. Apakah kaedah ini sebenarnya? Menggunakan API strim, kami menyemak senarai pelanggaran. Kami menukar setiap pelanggaran dalam kaedah peta menjadi objek pelanggaran kami sendiri dan mengumpulkan semua objek yang terhasil ke dalam senarai. Kami pulangkan dia. Apa lagi objek pelanggaran kita sendiri, anda bertanya? Berikut adalah rekod mudah
public record Violation(String property, String message) {}
Rekod adalah inovasi istimewa di Jawa, sekiranya anda memerlukan objek dengan data, tanpa sebarang logik atau apa-apa lagi. Walaupun saya sendiri masih belum memahami mengapa ini dilakukan, kadang-kadang ia adalah perkara yang agak mudah. Ia mesti dibuat dalam fail berasingan, seperti kelas biasa. Kembali ke ValidationException tersuai - ia kelihatan seperti ini:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Ia menyimpan senarai semua pelanggaran, anotasi Lombok - Getter dilampirkan pada senarai, dan melalui anotasi Lombok yang lain kami "melaksanakan" pembina yang diperlukan :) Perlu diperhatikan di sini bahawa saya tidak melaksanakan kelakuan isValid dengan betul. ... kaedah, ia mengembalikan sama ada benar atau pengecualian, tetapi ia patut mengehadkan diri kita kepada Palsu yang biasa. Pendekatan pengecualian dibuat kerana saya ingin mengembalikan ralat ini kepada klien, yang bermaksud saya perlu mengembalikan sesuatu selain benar atau palsu daripada kaedah boolean. Dalam kes kaedah pengesahan dalaman semata-mata, tidak perlu membuang pengecualian; pembalakan akan diperlukan di sini. Walau bagaimanapun, mari kembali ke EmployeeService kami, kami masih perlu mula menyimpan objek :) Mari lihat rupa kelas ini sekarang:
@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());
    }
}
Perhatikan ValidationService validationService akhir persendirian harta akhir baharu; Kaedah simpan itu sendiri ditandakan dengan anotasi @Transactional supaya jika RuntimeException diterima, perubahan akan digulung semula. Pertama sekali, kami mengesahkan data masuk menggunakan perkhidmatan yang baru kami tulis. Jika semuanya berjalan lancar, kami menyemak sama ada terdapat pekerja sedia ada dalam pangkalan data (menggunakan nombor unik). Jika tidak, kita simpan yang baru, jika ada, kita kemas kini medan dalam kelas. Oh ya, bagaimana kita sebenarnya menyemak? Ya, ia sangat mudah, kami menambah kaedah baharu pada repositori Pekerja:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Apa yang ketara? Saya tidak menulis sebarang logik atau pertanyaan SQL, walaupun itu tersedia di sini. Spring, hanya dengan membaca nama kaedah, menentukan apa yang saya mahu - cari ByUniqueNumber dan hantar rentetan yang sepadan dengan kaedah. Kembali kepada mengemas kini bidang - di sini saya memutuskan untuk menggunakan akal dan mengemas kini hanya jabatan, gaji dan tugas, kerana menukar nama, walaupun perkara yang boleh diterima, masih tidak begitu biasa. Dan menukar tarikh pengambilan pekerja secara amnya merupakan isu kontroversi. Apa yang bagus untuk dilakukan di sini? Gabungkan senarai tugasan, tetapi kerana kami belum mempunyai tugasan lagi dan tidak tahu cara membezakannya, kami akan meninggalkan TODO. Mari cuba melancarkan Frankenstein kami. Jika saya tidak lupa untuk menerangkan apa-apa, ia sepatutnya berfungsi, tetapi pertama sekali, inilah pokok kelas yang kami dapat: API REST dan Pengesahan Data - 1 Kelas yang telah diubah suai diserlahkan dalam warna biru, yang baharu diserlahkan dalam warna hijau, petunjuk sedemikian boleh diperolehi jika anda bekerja dengan repositori git, tetapi git bukan topik untuk artikel kami, jadi kami tidak akan memikirkannya. Jadi, pada masa ini kami mempunyai satu titik akhir yang menyokong dua kaedah GET dan POST. Dengan cara ini, beberapa maklumat menarik tentang titik akhir. Mengapa, sebagai contoh, kami tidak memperuntukkan titik akhir yang berasingan untuk GET dan POST, seperti getAllEmployees atau createEmployees? Segala-galanya sangat mudah - mempunyai satu titik untuk semua permintaan adalah lebih mudah. Penghalaan berlaku berdasarkan kaedah HTTP dan ia adalah intuitif, tidak perlu mengingati semua variasi getAllEmployees, getEmployeeByName, dapatkan... kemas kini... cipta... padam... Mari kita uji apa yang kita dapat. Saya sudah menulis dalam artikel sebelumnya bahawa kita memerlukan Posman, dan sudah tiba masanya untuk memasangnya. Dalam antara muka program, kami mencipta permintaan POST baharu API REST dan Pengesahan Data - 2 dan cuba menghantarnya. Jika semuanya berjalan lancar, kami akan mendapat Status 201 di sebelah kanan skrin. Tetapi sebagai contoh, setelah menghantar perkara yang sama tetapi tanpa nombor unik (yang mana kami mempunyai pengesahan), saya mendapat jawapan yang berbeza: API REST dan Pengesahan Data - 3 Baiklah, mari semak cara pemilihan penuh kami berfungsi - kami mencipta kaedah GET untuk titik akhir yang sama dan menghantarnya . API REST dan Pengesahan Data - 4 Saya amat berharap bahawa segala-galanya berjaya untuk anda seperti yang berlaku untuk saya, dan jumpa anda dalam artikel seterusnya .
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION