JavaRush /وبلاگ جاوا /Random-FA /REST API و Data Validation
Денис
مرحله
Киев

REST API و Data Validation

در گروه منتشر شد
پیوند به قسمت اول: 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": [
  ]
}
ما برای اولین بار فیلدهای کافی داریم، بنابراین اجازه دهید شروع به اجرای آن کنیم. سه زمینه اول سؤالی ایجاد نمی کند - اینها خطوط معمولی هستند، اما زمینه حقوق و دستمزد از قبل پیشنهاد می کند. چرا خط واقعی؟ در کار واقعی هم این اتفاق می افتد، یک مشتری به شما می آید و می گوید - من می خواهم این بار را برای شما ارسال کنم و شما آن را پردازش کنید. البته می‌توانید شانه‌های خود را بالا انداخته و این کار را انجام دهید، می‌توانید سعی کنید به توافق برسید و توضیح دهید که بهتر است داده‌ها را در قالب مورد نیاز انتقال دهید. بیایید تصور کنیم که با یک کلاینت هوشمند روبرو شدیم و توافق کردیم که بهتر است اعداد را به صورت عددی ارسال کنیم و چون در مورد پول صحبت می کنیم، بگذارید Double باشد. پارامتر بعدی بار ما تاریخ استخدام خواهد بود، مشتری آن را در قالب توافق شده برای ما ارسال می کند: yyyy-mm-dd، که در آن y برای سال ها مسئول است، m برای روزها، و d برای روزها انتظار می رود - 2022- 08-12. آخرین فیلد در حال حاضر لیستی از وظایف محول شده به مشتری خواهد بود. بدیهی است که Task موجودیت دیگری در پایگاه داده ما است، اما ما هنوز چیز زیادی در مورد آن نمی دانیم، بنابراین ابتدایی ترین موجودیت را همانطور که قبلا با Employee انجام دادیم ایجاد خواهیم کرد. تنها چیزی که اکنون می توانیم فرض کنیم این است که می توان بیش از یک کار را به یک کارمند واگذار کرد، بنابراین ما رویکرد به اصطلاح یک به چند، نسبت یک به چند را اعمال خواهیم کرد. اگر با جزئیات صحبت کنیم، یک رکورد در جدول کارمند می تواند با چندین رکورد از جدول وظایف مطابقت داشته باشد . من همچنین تصمیم گرفتم چیزی به عنوان یک فیلد منحصر به فرد Number اضافه کنم تا بتوانیم به وضوح یک کارمند را از دیگری تشخیص دهیم. در حال حاضر کلاس 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<>();
}
کلاس زیر برای موجودیت Task ایجاد شد:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
همانطور که گفتم، ما هیچ چیز جدیدی در Task نخواهیم دید، یک مخزن جدید نیز برای این کلاس ایجاد شده است که یک کپی از مخزن برای Employee است - من آن را نمی دهم، شما می توانید خودتان آن را با قیاس ایجاد کنید. اما منطقی است که در مورد کلاس Employee صحبت کنیم. همانطور که گفتم، ما چندین فیلد اضافه کردیم، اما فقط آخرین مورد در حال حاضر مورد علاقه است - وظایف. این یک List<Task> وظایف است ، بلافاصله با یک ArrayList خالی مقدار دهی اولیه می شود و با چندین حاشیه مشخص می شود. 1. @OneToMany همانطور که گفتم، این نسبت کارمندان به وظایف ما خواهد بود. 2. @JoinColumn - ستونی که توسط آن نهادها به یکدیگر ملحق می شوند. در این حالت، یک ستون working_id در جدول Task ایجاد می‌شود که به شناسه کارمند ما اشاره می‌کند؛ این ستون به عنوان ForeighnKey به ما عمل می‌کند. اگر لازم باشد نه فقط از یک شناسه، بلکه از نوعی ستون واقعی استفاده کنید، وضعیت کمی پیچیده تر خواهد شد؛ ما بعداً به این موضوع خواهیم پرداخت. 3. همچنین ممکن است متوجه یک حاشیه نویسی جدید در بالای شناسه شده باشید - @JsonIgnore. از آنجایی که id موجودیت داخلی ما است، لزوماً نیازی به برگرداندن آن به مشتری نداریم. 4. @NotBlank یک حاشیه نویسی ویژه برای اعتبار سنجی است که می گوید فیلد نباید null یا رشته خالی 5 باشد. @Column(unique = true) می گوید که این ستون باید مقادیر منحصر به فرد داشته باشد. بنابراین، ما در حال حاضر دو نهاد داریم، آنها حتی به یکدیگر متصل هستند. زمان آن فرا رسیده است که آنها را در برنامه خود ادغام کنیم - بیایید با خدمات و کنترلرها سروکار داشته باشیم. اول از همه، بیایید خرد خود را از متد 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 چه اتفاقی می افتد؟ بدیهی است که کارکنان سرویس ما یک روش ذخیره دارد که باید مسئولیت تمام کارهای ذخیره سازی را بر عهده بگیرد. همچنین بدیهی است که این روش می تواند یک استثنا با نام خود توضیحی ایجاد کند. آن ها نوعی اعتبارسنجی در حال انجام است. و پاسخ مستقیماً به نتایج اعتبارسنجی بستگی دارد، آیا 201 Created یا 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' اضافه کردیم . دوستان قدیمی ما Service and RequiredArgsConstructor در حال حاضر همه چیزهایی را که باید در مورد این کلاس بدانیم به ما گفته اند، همچنین یک فیلد اعتبارسنجی نهایی خصوصی نیز وجود دارد. او جادو را انجام خواهد داد. ما متد isValidEmployee را ایجاد کردیم که می‌توانیم موجودیت Employee را به آن منتقل کنیم؛ این متد یک ValidationException می‌اندازد که کمی بعد می‌نویسیم. بله، این یک استثنای سفارشی برای نیازهای ما خواهد بود. با استفاده از روش validator.validate(employee)، لیستی از اشیاء ConstraintViolation را دریافت می کنیم - همه آن ناهماهنگی ها با حاشیه نویسی های اعتبارسنجی که قبلا اضافه کردیم. منطق بعدی ساده است، اگر این لیست خالی نباشد (یعنی موارد نقض وجود داشته باشد)، یک استثنا می اندازیم و لیستی از نقض ها را ایجاد می کنیم - روش buildViolationsList لطفا توجه داشته باشید که این یک روش عمومی است، یعنی. می تواند با لیست هایی از نقض اشیاء مختلف کار کند - اگر چیز دیگری را تأیید کنیم ممکن است در آینده مفید باشد. این روش در واقع چه کاری انجام می دهد؟ با استفاده از استریم API، لیست تخلفات را مرور می کنیم. ما هر نقض در متد نقشه را به شی نقض خودمان تبدیل می کنیم و تمام اشیاء به دست آمده را در یک لیست جمع می کنیم. ما او را برمی گردانیم. شما می‌پرسید که مورد نقض خود ما چیست؟ در اینجا یک رکورد ساده است
public record Violation(String property, String message) {}
رکوردها چنین نوآوری های خاصی در جاوا هستند، در صورتی که به یک شی با داده نیاز دارید، بدون هیچ منطق یا هر چیز دیگری. اگرچه من خودم هنوز متوجه نشده ام که چرا این کار انجام شد، گاهی اوقات این یک چیز کاملاً راحت است. باید در یک فایل جداگانه مانند یک کلاس معمولی ایجاد شود. بازگشت به ValidationException سفارشی - به نظر می رسد:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
این فهرستی از تمام نقض ها را ذخیره می کند، حاشیه نویسی Lombok - Getter به لیست پیوست شده است، و از طریق حاشیه نویسی Lombok دیگری، سازنده مورد نیاز را "پیاده سازی" کردیم :) شایان ذکر است که من رفتار isValid را کاملاً درست اجرا نمی کنم. ... روش، درست یا استثنا را برمی گرداند، اما ارزش آن را دارد که خود را به False معمول محدود کنیم. رویکرد استثنا به این دلیل ساخته شده است که من می خواهم این خطا را به مشتری برگردانم، به این معنی که باید چیزی غیر از true یا false را از روش بولی برگردانم. در مورد روش‌های اعتبارسنجی صرفاً داخلی، نیازی به ایجاد استثنا نیست؛ ورود به سیستم در اینجا لازم است. با این حال، بیایید به EmployeeService خود برگردیم، ما هنوز باید ذخیره اشیاء را شروع کنیم :) بیایید ببینیم این کلاس اکنون چگونه به نظر می رسد:
@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، تغییرات به عقب برگردند. اول از همه، ما داده های دریافتی را با استفاده از سرویسی که نوشتیم تأیید می کنیم. اگر همه چیز به خوبی پیش رفت، بررسی می کنیم که آیا کارمند موجود در پایگاه داده (با استفاده از یک شماره منحصر به فرد) وجود دارد یا خیر. اگر نه، جدید را ذخیره می کنیم، اگر وجود داشت، فیلدهای کلاس را به روز می کنیم. اوه بله، واقعاً چگونه بررسی کنیم؟ بله، بسیار ساده است، ما یک روش جدید به مخزن Employee اضافه کردیم:
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، دریافت... به روز رسانی... ایجاد... حذف... بیایید آنچه را که به دست آوردیم را آزمایش کنیم. قبلاً در مقاله قبلی نوشتم که به Postman نیاز داریم و زمان نصب آن فرا رسیده است. در رابط برنامه، یک درخواست 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