JavaRush /Java Blog /Random-ID /REST API dan Validasi Data
Денис
Level 37
Киев

REST API dan Validasi Data

Dipublikasikan di grup Random-ID
Tautan ke bagian pertama: REST API dan tugas pengujian berikutnya Nah, aplikasi kita berfungsi, kita bisa mendapatkan semacam respons darinya, tapi apa manfaatnya bagi kita? Itu tidak melakukan pekerjaan yang berguna. Tidak lama kemudian, mari kita terapkan sesuatu yang bermanfaat. Pertama-tama, mari tambahkan beberapa dependensi baru ke build.gradle kita, itu akan berguna bagi kita:
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 kita akan mulai dengan data aktual yang harus kita proses. Mari kembali ke paket persistensi dan mulai mengisi entitas. Seperti yang Anda ingat, kami membiarkannya ada hanya dengan satu bidang, dan kemudian otomatis dihasilkan melalui `@GeneratedValue(strategy = GenerationType.IDENTITY)` Mari kita ingat spesifikasi teknis dari bab pertama:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Kami memiliki cukup bidang untuk pertama kalinya, jadi mari mulai menerapkannya. Tiga bidang pertama tidak menimbulkan pertanyaan - ini adalah garis biasa, tetapi bidang gaji sudah sugestif. Mengapa garis sebenarnya? Dalam pekerjaan nyata, ini juga terjadi, seorang pelanggan mendatangi Anda dan berkata - Saya ingin mengirimkan muatan ini kepada Anda, dan Anda memprosesnya. Anda tentu saja dapat mengangkat bahu dan melakukannya, Anda dapat mencoba mencapai kesepakatan dan menjelaskan bahwa lebih baik mengirimkan data dalam format yang diperlukan. Bayangkan kita menemukan klien yang cerdas dan sepakat bahwa lebih baik mengirimkan angka dalam format numerik, dan karena kita berbicara tentang uang, biarlah Ganda. Parameter payload kami berikutnya adalah tanggal perekrutan, klien akan mengirimkannya kepada kami dalam format yang disepakati: yyyy-mm-dd, di mana y bertanggung jawab selama bertahun-tahun, m untuk hari, dan d diharapkan untuk hari - 2022- 08-12. Bidang terakhir saat ini adalah daftar tugas yang diberikan kepada klien. Tentu saja, Task adalah entitas lain dalam database kita, namun kita belum mengetahui banyak tentangnya, jadi kita akan membuat entitas paling dasar seperti yang kita lakukan dengan Employee sebelumnya. Satu-satunya hal yang dapat kita asumsikan sekarang adalah bahwa lebih dari satu tugas dapat diberikan kepada satu karyawan, jadi kita akan menerapkan apa yang disebut pendekatan Satu-Ke-Banyak, yaitu rasio satu-ke-banyak. Lebih khusus lagi, satu catatan dalam tabel karyawan dapat berhubungan dengan beberapa catatan dari tabel tugas . Saya juga memutuskan untuk menambahkan bidang UniqueNumber sehingga kami dapat dengan jelas membedakan satu karyawan dengan karyawan lainnya. Saat ini kelas Karyawan kami terlihat 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 ini dibuat untuk entitas 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 sesuatu yang baru di Task, repositori baru juga dibuat untuk kelas ini, yang merupakan salinan dari repositori untuk Karyawan - saya tidak akan memberikannya, Anda dapat membuatnya sendiri dengan analogi. Tapi masuk akal untuk membicarakan kelas Karyawan. Seperti yang saya katakan, kami menambahkan beberapa bidang, tetapi sekarang hanya bidang terakhir yang menarik - tugas. Ini adalah List<Task> task , yang segera diinisialisasi dengan ArrayList kosong dan ditandai dengan beberapa anotasi. 1. @OneToMany Seperti yang saya katakan, ini akan menjadi rasio karyawan terhadap tugas. 2. @JoinColumn - kolom dimana entitas akan bergabung. Dalam hal ini, kolom Employee_id akan dibuat di tabel Task yang menunjuk ke id karyawan kita; itu akan berfungsi sebagai ForeighnKey Meskipun nama tersebut tampak sakral, Anda dapat memberi nama kolom apa pun yang Anda suka. Situasinya akan menjadi sedikit lebih rumit jika Anda perlu menggunakan bukan hanya ID, tetapi semacam kolom nyata; kita akan membahas topik ini nanti. 3. Anda mungkin juga memperhatikan anotasi baru di atas id - @JsonIgnore. Karena id adalah entitas internal kami, kami tidak perlu mengembalikannya ke klien. 4. @NotBlank adalah anotasi khusus untuk validasi, yang menyatakan bahwa bidang tidak boleh null atau string kosong 5. @Column(unique = true) mengatakan bahwa kolom ini harus memiliki nilai unik. Jadi kita sudah punya dua entitas, bahkan saling terhubung. Waktunya telah tiba untuk mengintegrasikannya ke dalam program kita - mari kita berurusan dengan layanan dan pengontrol. Pertama-tama, mari kita hapus stub dari metode getAllEmployees() dan ubah menjadi sesuatu yang benar-benar berfungsi:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Dengan demikian, repositori kami akan mengambil semua yang tersedia dari database dan memberikannya kepada kami. Perlu dicatat bahwa ia juga akan mengambil daftar tugas. Tapi menyapunya tentu saja menarik, tapi apa jadinya jika tidak ada apa-apa di sana? Itu benar, itu berarti kita perlu memikirkan bagaimana cara meletakkan sesuatu di sana. Pertama-tama, mari kita tulis metode baru di pengontrol kita.
@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 adalah @PostMapping, mis. itu memproses permintaan POST yang datang ke titik akhir karyawan kami. Secara umum, saya pikir karena semua permintaan ke pengontrol ini akan sampai pada satu titik akhir, mari kita sederhanakan ini sedikit. Ingat pengaturan bagus kami di application.yml? Mari kita perbaiki. Biarkan bagian aplikasi sekarang terlihat seperti ini:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Apa manfaatnya bagi kita? Fakta bahwa di pengontrol kita dapat menghapus pemetaan untuk setiap metode tertentu, dan titik akhir akan ditetapkan pada tingkat kelas dalam anotasi @RequestMapping("${application.endpoint.employee}") . Inilah keindahan yang sekarang ada di Pengendali 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();
    }
}
Namun, mari kita lanjutkan. Apa sebenarnya yang terjadi dalam metode createOrUpdateEmployee? Jelas sekali, EmployeeService kami memiliki metode penyimpanan, yang harus bertanggung jawab atas semua pekerjaan penyimpanan. Jelas juga bahwa metode ini dapat memunculkan pengecualian dengan nama yang cukup jelas. Itu. semacam validasi sedang dilakukan. Dan jawabannya tergantung langsung pada hasil validasi, apakah 201 Created atau 400 badRequest dengan daftar kesalahannya. Ke depan, ini adalah layanan validasi baru kami, yang memeriksa data yang masuk untuk mengetahui keberadaan bidang yang wajib diisi (ingat @NotBlank?) dan memutuskan apakah informasi tersebut cocok untuk kami atau tidak. Sebelum beralih ke metode penyimpanan, mari validasi dan implementasi layanan ini. Untuk melakukan ini, saya mengusulkan untuk membuat paket validasi terpisah di mana kami akan menempatkan layanan 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();
    }
}
Kelasnya ternyata terlalu besar, tapi jangan panik, kita akan mencari tahu sekarang :) Di sini kita menggunakan alat dari perpustakaan validasi javax yang sudah jadi.validasi Perpustakaan ini datang kepada kita dari dependensi baru yang kita ditambahkan ke implementasi build.gradle 'org.springframework.boot:spring-boot-starter -validation' Teman lama kami Layanan dan RequiredArgsConstructor Sudah memberi tahu kami semua yang perlu kami ketahui tentang kelas ini, ada juga bidang validator akhir pribadi. Dia akan melakukan keajaiban. Kita membuat metode isValidEmployee, yang ke dalamnya kita dapat meneruskan entitas Karyawan; metode ini memunculkan ValidationException, yang akan kita tulis nanti. Ya, ini akan menjadi pengecualian khusus untuk kebutuhan kita. Dengan menggunakan metode validator.validate(employee), kita akan mendapatkan daftar objek ConstraintViolation - semua ketidakkonsistenan dengan anotasi validasi yang kita tambahkan sebelumnya. Logika selanjutnya sederhana, jika daftar ini tidak kosong (yaitu ada pelanggaran), kami mengeluarkan pengecualian dan membuat daftar pelanggaran - metode buildViolationsList Harap dicatat bahwa ini adalah metode Generik, yaitu. dapat bekerja dengan daftar pelanggaran pada objek yang berbeda - mungkin berguna di masa mendatang jika kita memvalidasi hal lain. Apa sebenarnya fungsi metode ini? Dengan menggunakan API aliran, kami memeriksa daftar pelanggaran. Kami mengubah setiap pelanggaran dalam metode peta menjadi objek pelanggaran kami sendiri, dan mengumpulkan semua objek yang dihasilkan ke dalam daftar. Kami akan mengembalikannya. Anda bertanya, apa lagi yang menjadi objek pelanggaran kami? Berikut catatan sederhananya
public record Violation(String property, String message) {}
Catatan adalah inovasi khusus di Java, jika Anda memerlukan objek dengan data, tanpa logika atau apa pun. Meski saya sendiri belum paham kenapa hal ini dilakukan, namun terkadang hal tersebut cukup memudahkan. Itu harus dibuat dalam file terpisah, seperti kelas biasa. Kembali ke ValidationException khusus - tampilannya seperti ini:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Ini menyimpan daftar semua pelanggaran, anotasi Lombok - Getter dilampirkan ke daftar, dan melalui anotasi Lombok lainnya kami "mengimplementasikan" konstruktor yang diperlukan :) Perlu dicatat di sini bahwa saya tidak mengimplementasikan perilaku isValid dengan benar ... metode, ia mengembalikan nilai benar atau pengecualian, tetapi ada baiknya membatasi diri kita pada False yang biasa. Pendekatan pengecualian dibuat karena saya ingin mengembalikan kesalahan ini ke klien, yang berarti saya perlu mengembalikan sesuatu selain benar atau salah dari metode boolean. Dalam kasus metode validasi internal murni, tidak perlu memberikan pengecualian; logging akan diperlukan di sini. Namun, mari kita kembali ke EmployeeService kita, kita masih perlu mulai menyimpan objek :) Mari kita lihat seperti apa 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 properti final private final ValidationService validationService yang baru; Metode penyimpanan sendiri ditandai dengan anotasi @Transactional sehingga jika RuntimeException diterima, perubahan akan dibatalkan. Pertama-tama, kami memvalidasi data yang masuk menggunakan layanan yang baru saja kami tulis. Jika semuanya berjalan lancar, kami memeriksa apakah ada karyawan yang ada di database (menggunakan nomor unik). Kalau belum kita simpan yang baru, kalau ada kita update field-field yang ada di kelas. Oh iya, sebenarnya bagaimana cara kita mengeceknya? Ya, ini sangat sederhana, kami menambahkan metode baru ke repositori Karyawan:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Apa yang penting? Saya tidak menulis logika atau kueri SQL apa pun, meskipun itu tersedia di sini. Spring, cukup dengan membaca nama metode, menentukan apa yang saya inginkan - temukan ByUniqueNumber dan meneruskan string yang sesuai ke metode tersebut. Kembali memperbarui kolom - di sini saya memutuskan untuk menggunakan akal sehat dan hanya memperbarui departemen, gaji, dan tugas, karena mengubah nama, meskipun merupakan hal yang dapat diterima, masih belum terlalu umum. Dan mengubah tanggal perekrutan umumnya merupakan isu kontroversial. Apa yang baik untuk dilakukan di sini? Gabungkan daftar tugas, tetapi karena kami belum memiliki tugas dan tidak tahu cara membedakannya, kami akan meninggalkan TODO. Mari kita coba meluncurkan Frankenstein kita. Jika saya tidak lupa menjelaskan apa pun, itu seharusnya berhasil, tetapi pertama-tama, inilah pohon kelas yang kita dapatkan: REST API dan Validasi Data - 1 Kelas yang telah dimodifikasi disorot dengan warna biru, yang baru disorot dengan warna hijau, indikasi seperti itu dapat diperoleh jika Anda bekerja dengan repositori git, tetapi git bukanlah topik artikel kami, jadi kami tidak akan membahasnya. Jadi, saat ini kami memiliki satu titik akhir yang mendukung dua metode GET dan POST. Omong-omong, beberapa informasi menarik tentang titik akhir. Mengapa, misalnya, kami tidak mengalokasikan titik akhir terpisah untuk GET dan POST, seperti getAllEmployees atau createEmployees? Semuanya sangat sederhana - memiliki satu titik untuk semua permintaan jauh lebih nyaman. Perutean terjadi berdasarkan metode HTTP dan intuitif, tidak perlu mengingat semua variasi getAllEmployees, getEmployeeByName, get... update... create... delete... Mari kita uji apa yang kita punya. Saya sudah menulis di artikel sebelumnya bahwa kita memerlukan Tukang Pos, dan sekarang saatnya menginstalnya. Di antarmuka program, kami membuat permintaan POST baru REST API dan Validasi Data - 2 dan mencoba mengirimkannya. Jika semuanya berjalan dengan baik kita akan mendapatkan Status 201 di sisi kanan layar. Tetapi misalnya, setelah mengirim hal yang sama tetapi tanpa nomor unik (yang kami validasi), saya mendapatkan jawaban yang berbeda: REST API dan Validasi Data - 3 Baiklah, mari kita periksa cara kerja pilihan lengkap kami - kami membuat metode GET untuk titik akhir yang sama dan mengirimkannya . REST API dan Validasi Data - 4 Saya sangat berharap semuanya berjalan baik untuk Anda seperti yang terjadi pada saya, dan sampai jumpa di artikel berikutnya .
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION