JavaRush /مدونة جافا /Random-AR /REST API والتحقق من صحة البيانات
Денис
مستوى
Киев

REST API والتحقق من صحة البيانات

نشرت في المجموعة
رابط إلى الجزء الأول: 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": [
  ]
}
لدينا حقول كافية لأول مرة، فلنبدأ في تنفيذها. الحقول الثلاثة الأولى لا تثير أسئلة - هذه خطوط عادية، لكن حقل الراتب موحٍ بالفعل. لماذا الخط الفعلي؟ في العمل الحقيقي، يحدث هذا أيضًا، يأتي إليك عميل ويقول - أريد أن أرسل لك هذه الحمولة، وتقوم أنت بمعالجتها. يمكنك بالطبع أن تهز كتفيك وتفعل ذلك، ويمكنك محاولة التوصل إلى اتفاق وشرح أنه من الأفضل نقل البيانات بالتنسيق المطلوب. لنتخيل أننا صادفنا عميلاً ذكياً واتفقنا على أنه من الأفضل إرسال الأرقام بتنسيق رقمي، وبما أننا نتحدث عن المال فليكن مزدوجاً. ستكون المعلمة التالية لحمولتنا هي تاريخ التوظيف، وسيرسلها العميل إلينا بالتنسيق المتفق عليه: yyyy-mm-dd، حيث يكون y مسؤولاً عن السنوات، وm عن الأيام، وd متوقع عن الأيام - 2022- 08-12. سيكون الحقل الأخير في الوقت الحالي عبارة عن قائمة بالمهام المخصصة للعميل. من الواضح أن المهمة هي كيان آخر في قاعدة بياناتنا، لكننا لا نعرف الكثير عنها بعد، لذلك سنقوم بإنشاء الكيان الأساسي كما فعلنا مع الموظف من قبل. الشيء الوحيد الذي يمكننا أن نفترضه الآن هو أنه يمكن إسناد أكثر من مهمة إلى موظف واحد، لذلك سوف نطبق ما يسمى نهج واحد إلى متعدد، وهي نسبة واحد إلى متعدد. بالحديث بالتفصيل، يمكن أن يتوافق سجل واحد في جدول الموظفين مع عدة سجلات من جدول المهام . وقررت أيضًا إضافة شيء مثل حقل الرقم الفريد، حتى نتمكن من التمييز بوضوح بين موظف وآخر. في الوقت الحالي تبدو فئة الموظفين لدينا كما يلي:
@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<>();
}
تم إنشاء الفئة التالية لكيان المهمة:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
كما قلت، لن نرى أي شيء جديد في المهمة، تم أيضًا إنشاء مستودع جديد لهذه الفئة، وهو نسخة من مستودع الموظف - لن أعطيها، يمكنك إنشائها بنفسك عن طريق القياس. ولكن من المنطقي الحديث عن فئة الموظف. كما قلت، أضفنا العديد من الحقول، ولكن الأخير فقط هو الذي يهم الآن - المهام. هذه قائمة مهام List<Task> ، وتتم تهيئتها على الفور باستخدام قائمة ArrayList فارغة ويتم وضع علامة عليها بعدة تعليقات توضيحية. 1. @OneToMany كما قلت، ستكون هذه هي نسبة الموظفين إلى المهام. 2. @JoinColumn - العمود الذي سيتم من خلاله ضم الكيانات. في هذه الحالة، سيتم إنشاء عمود "معرف_الموظف" في جدول المهام للإشارة إلى معرف الموظف لدينا؛ وسوف يخدمنا كمفتاح فوري. على الرغم من قدسية الاسم الظاهرة، يمكنك تسمية العمود بأي شيء تريده. سيكون الوضع أكثر تعقيدا قليلا إذا كنت بحاجة إلى استخدام ليس مجرد معرف، ولكن نوعا من العمود الحقيقي، سنتطرق إلى هذا الموضوع لاحقا. 3. ربما لاحظت أيضًا تعليقًا توضيحيًا جديدًا أعلى المعرف - @JsonIgnore. نظرًا لأن المعرف هو كياننا الداخلي، فلا نحتاج بالضرورة إلى إعادته إلى العميل. 4. @NotBlank هو تعليق توضيحي خاص للتحقق من الصحة، والذي ينص على أن الحقل يجب ألا يكون فارغًا أو سلسلة فارغة 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.gradle 'org.springframework.boot:spring-boot-starter -validation' خدمة أصدقائنا القدامى وRequiredArgsConstructor أخبرونا بالفعل بكل ما نحتاج إلى معرفته حول هذا الفصل، ويوجد أيضًا حقل مدقق نهائي خاص. سوف يفعل السحر. لقد أنشأنا طريقة isValidEmployee، والتي يمكننا تمرير كيان الموظف إليها؛ وتطرح هذه الطريقة ValidationException، والذي سنكتبه بعد قليل. نعم، سيكون هذا استثناءً مخصصًا لاحتياجاتنا. باستخدام طريقة validator.validate(employee)، سنحصل على قائمة بكائنات ConstraintViolation - كل تلك التناقضات مع التعليقات التوضيحية للتحقق التي أضفناها سابقًا. المنطق الإضافي بسيط، إذا لم تكن هذه القائمة فارغة (أي هناك انتهاكات)، فإننا نطرح استثناءً وننشئ قائمة بالانتهاكات - طريقة buildViolationsList يرجى ملاحظة أن هذه طريقة عامة، أي. يمكن أن تعمل مع قوائم الانتهاكات لكائنات مختلفة - قد يكون من المفيد في المستقبل إذا قمنا بالتحقق من صحة شيء آخر. ماذا تفعل هذه الطريقة في الواقع؟ باستخدام Stream API، نستعرض قائمة الانتهاكات. نحول كل انتهاك في طريقة الخريطة إلى كائن انتهاك خاص بنا، ونجمع كل الكائنات الناتجة في قائمة. نحن نعيده. تسأل، ما هو الشيء الآخر الذي نخالفه؟ وهنا سجل بسيط
public record Violation(String property, String message) {}
السجلات هي ابتكارات خاصة في Java، في حالة احتياجك إلى كائن يحتوي على بيانات، دون أي منطق أو أي شيء آخر. على الرغم من أنني لم أفهم بعد سبب القيام بذلك، إلا أنه في بعض الأحيان يكون الأمر مناسبًا تمامًا. ويجب إنشاؤه في ملف منفصل، مثل الفصل العادي. العودة إلى ValidationException المخصص - يبدو كما يلي:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
يقوم بتخزين قائمة بجميع الانتهاكات، تعليق Lombok التوضيحي - يتم إرفاق Getter بالقائمة، ومن خلال تعليق توضيحي Lombok آخر "نفذنا" المنشئ المطلوب :) تجدر الإشارة هنا إلى أنني لا أنفذ سلوك isValid بشكل صحيح تمامًا ... الطريقة، فإنها تُرجع إما صحيحًا أو استثناءً، ولكن قد يكون من المفيد أن نقتصر على الخطأ المعتاد. يتم إجراء نهج الاستثناء لأنني أريد إعادة هذا الخطأ إلى العميل، مما يعني أنني بحاجة إلى إرجاع شيء آخر غير صحيح أو خطأ من الطريقة المنطقية. في حالة طرق التحقق الداخلية البحتة، ليست هناك حاجة لطرح استثناء، حيث سيكون التسجيل مطلوبًا هنا. ومع ذلك، دعونا نعود إلى خدمة الموظفين لدينا، مازلنا بحاجة إلى البدء في حفظ الكائنات :) دعونا نرى كيف تبدو هذه الفئة الآن:
@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());
    }
}
لاحظ الخاصية النهائية الجديدة الخاصة Final ValidationService validationService؛ يتم وضع علامة على طريقة الحفظ نفسها بالتعليق التوضيحي @Transactional بحيث يتم التراجع عن التغييرات في حالة تلقي RuntimeException. أولاً، نقوم بالتحقق من صحة البيانات الواردة باستخدام الخدمة التي كتبناها للتو. إذا سارت الأمور بسلاسة، فإننا نتحقق مما إذا كان هناك موظف موجود في قاعدة البيانات (باستخدام رقم فريد). إذا لم يكن الأمر كذلك، فإننا نحفظ الحقل الجديد، وإذا كان هناك واحدًا، فإننا نقوم بتحديث الحقول الموجودة في الفصل. أوه نعم، كيف يمكننا التحقق فعلا؟ نعم، الأمر بسيط للغاية، لقد أضفنا طريقة جديدة إلى مستودع الموظفين:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
ما هو ملحوظ؟ لم أكتب أي استعلام منطقي أو SQL، على الرغم من أنه متاح هنا. يحدد Spring ما أريده بمجرد قراءة اسم الطريقة - ابحث عن ByUniqueNumber وقم بتمرير السلسلة المقابلة إلى الطريقة. العودة إلى تحديث الحقول - هنا قررت استخدام الحس السليم وتحديث القسم والراتب والمهام فقط، لأن تغيير الاسم، على الرغم من أنه أمر مقبول، إلا أنه لا يزال غير شائع جدًا. ويعد تغيير تاريخ التوظيف بشكل عام مسألة مثيرة للجدل. ما الذي سيكون من الجيد القيام به هنا؟ قم بدمج قوائم المهام، ولكن بما أنه ليس لدينا مهام بعد ولا نعرف كيفية التمييز بينها، فسوف نترك المهام. دعونا نحاول إطلاق فرانكشتاين الخاص بنا. إذا لم أنس وصف أي شيء، فيجب أن يعمل، ولكن أولاً، هذه هي شجرة الفصل التي حصلنا عليها: يتم REST API والتحقق من صحة البيانات - 1 تمييز الفئات التي تم تعديلها باللون الأزرق، ويتم تمييز الفئات الجديدة باللون الأخضر، ويمكن الحصول على مثل هذه المؤشرات إذا كنت تعمل مع مستودع git، لكن git ليس موضوع مقالتنا، لذلك لن نتناوله بالتفصيل. لذلك، لدينا حاليًا نقطة نهاية واحدة تدعم طريقتين GET وPOST. بالمناسبة، بعض المعلومات المثيرة للاهتمام حول نقطة النهاية. لماذا، على سبيل المثال، لم نخصص نقاط نهاية منفصلة لـ GET وPOST، مثل getAllEmployees أو createEmployees؟ كل شيء بسيط للغاية - الحصول على نقطة واحدة لجميع الطلبات هو أكثر ملاءمة. يتم التوجيه بناءً على طريقة HTTP وهو أمر بديهي، وليست هناك حاجة لتذكر جميع أشكال getAllEmployees، وgetEmployeeByName، وget... تحديث... إنشاء... حذف... دعنا نختبر ما حصلنا عليه. لقد كتبت بالفعل في المقالة السابقة أننا سنحتاج إلى 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