قيود التحقق المخصصة
قيود التحقق المخصصة
تأتي Jakarta Bean Validation مزوّدة بمجموعة متينة من القيود المدمجة — @NotNull و@Size و@Pattern و@Email وغيرها من عشرات الأخرى. وهي تغطي الحالات الشائعة، غير أن قواعد النطاق في التطبيقات الحقيقية نادرًا ما تكون بهذا العموم. اسم المستخدم يجب ألا يحتوي على مسافات. نسبة الخصم لا يمكن أن تتجاوز السعر الأساسي. رقم الهاتف يجب أن يتطابق مع صيغة خاصة ببلد معين. لا يمكن التعبير عن هذه القواعد بأي تعليق توضيحي مدمج واحد، كما أن تجميع عدة منها معًا يجعل الكود هشًا وصعب القراءة. هنا يأتي دور كتابة قيد مخصص.
يتألف القيد المخصص في Jakarta Validation دائمًا من جزأين: تعليق توضيحي تضعه على الحقول أو المعاملات، وفئة منقّحة (validator) تحتوي على المنطق. يُعلن التعليق التوضيحي عن البيانات الوصفية؛ بينما تؤدي فئة التحقق العمل الفعلي. يربطهما Spring Boot تلقائيًا — لا حاجة لأي إعداد إضافي للـ bean.
تشريح تعليق قيد مخصص
ابدأ بتعريف التعليق التوضيحي نفسه. ثلاثة تعليقات توضيحية وصفية (meta-annotations) من Jakarta API إلزامية:
@Constraint(validatedBy = ...)— يربط التعليق بفئة التحقق الخاصة به.@Target— يتحكم في أماكن وضع التعليق (الحقول، معاملات الدوال، الأنواع الكاملة، إلخ).@Retention(RUNTIME)— يجب أن يكون التعليق متاحًا وقت التشغيل لكي تقرأه محرك التحقق.
يجب أن يُعلن التعليق أيضًا عن ثلاثة سمات يطلبها الإطار: message وgroups وpayload. تمتلك هذه القيم الافتراضية المعيارية؛ فأنت لا تتجاوزها تقريبًا على مستوى تعريف التعليق.
@Documented؟ ليست مطلوبة من الإطار، لكنها تجعل التعليق يظهر في Javadoc المُولَّد. يستحق تضمينها في أي قيد يخص واجهة برمجية عامة حتى يرى مستخدمو مكتبتك القاعدة مباشرةً في التوثيق.
كتابة فئة التحقق
تُنفّذ فئة التحقق الواجهة ConstraintValidator<A, T>، حيث A هو نوع تعليقك التوضيحي وT هو نوع القيمة المُتحقَّق منها. تحتوي الواجهة على دالتين:
initialize(A annotation)— تُستدعى مرة واحدة عند إعداد المنقّح. استخدمها لقراءة سمات التعليق إن كان قيدك قابلًا للضبط (مثل قيمة دنيا/قصوى). اتركها فارغة إن لم يكن هناك ما تقرأه.isValid(T value, ConstraintValidatorContext context)— تُستدعى لكل عملية تحقق. أعِدtrueإن كانت القيمة مقبولة، وfalseلإخفاق التحقق.
null باعتبارها صحيحة في دالة isValid. تُعرّف Jakarta Validation فصلًا واضحًا للمهام: مسؤولية التعامل مع null تعود لـ @NotNull (أو @NotBlank للنصوص). إذا رفض منقّحك المخصص قيم null أيضًا، فأنت تجمع هاتين المسؤوليتين في مكان واحد ولا تستطيع التحكم فيهما بشكل مستقل على حقول مختلفة. أعِد true لـ null ودع @NotNull يتولى الأمر حين تحتاج.
تطبيق القيد
بمجرد وجود التعليق والمنقّح، تستخدم قيدك المخصص تمامًا كأي قيد مدمج. يمكنك تراكمه مع غيره:
وفي الـ controller، يُفعّل @Valid جميع القيود بما فيها قيدك المخصص:
القيود ذات المعاملات
تأتي القوة الحقيقية للقيود المخصصة من جعلها قابلة للتهيئة. لنفترض أنك تحتاج للتحقق من أن قيمة نصية تنتمي إلى مجموعة محددة من القيم المسموح بها — قيد "القيم المسموح بها". أضف سمات لتعليقك التوضيحي واقرأها في initialize:
الاستخدام على حقل مع رسالة مفيدة واضحة:
القيود على مستوى الفئة
في بعض الأحيان يستلزم التحقق مقارنة حقول متعددة — المثال الكلاسيكي هو "تاريخ الانتهاء يجب أن يكون بعد تاريخ البداية". لا يمكنك التعبير عن هذا في حقل واحد. بدلًا من ذلك، استهدف الفئة بأكملها بضبط @Target(ElementType.TYPE) والتحقق من الكائن:
isValid باستخدام context.buildConstraintViolationWithTemplate(...).addPropertyNode("endDate").addConstraintViolation(). هذا مهم لواجهات REST التي تُترجم المخالفات إلى مسارات حقول JSON، وهو ما يُغطّيه الدرس التالي.
حقن Spring Beans داخل المنقّحات
نظرًا لأن Spring Boot يسجّل LocalValidatorFactoryBean باعتباره المنقّح الافتراضي، فكل تنفيذ لـ ConstraintValidator هو bean مُدار بـ Spring. هذا يعني أنك تستطيع حقن الخدمات بـ @Autowired أو حقن المُنشئ — مفيد حين يتطلب القيد استعلامًا من قاعدة البيانات، كالتحقق من أن اسم المستخدم لم يُستخدم بالفعل:
isValid قد ينفّذ استعلام SQL. لنقاط نهاية التسجيل هذا مقبول. أما للعمليات الكثيرة أو التسلسلات الهرمية العميقة للكائنات، فكّر فيما إذا كانت هذه القاعدة تنتمي للمنقّح أم لطبقة الخدمة حيث تملك سيطرة أكبر على تجميع الاستعلامات.
الخلاصة
بناء قيد مخصص أمر بسيط بمجرد أن تعرف الوصفة المكونة من ثلاثة أجزاء: تعليق توضيحي مزيّن بـ @Constraint و@Target و@Retention(RUNTIME)؛ وفئة منقّح تُنفّذ ConstraintValidator<A, T>؛ وreturn true لقيم null. تضيف القيود ذات المعاملات سمات قابلة للتهيئة تُقرأ في initialize. تتحقق القيود على مستوى الفئة من حالة الكائن عبر حقول متعددة. يجعل Spring Boot المنقّحات beans كاملة فتستطيع حقن المستودعات أو الخدمات حين تتطلب القاعدة ذلك. في الدرس التالي سترى كيف تُترجم مخالفات القيود المدمجة والمخصصة على حد سواء إلى استجابات HTTP خطأ منظّمة.