تجريد المستودع
تجريد المستودع
إنّ أكبر مكسب في الإنتاجية الذي تجلبه Spring Data JPA هو تجريد المستودع (Repository Abstraction): تُعلن عن واجهة برمجية، وتوسّع إحدى واجهات المستودع المدمجة في Spring Data، فتحصل فورًا على مجموعة كاملة من توابع الوصول إلى البيانات — دون أي كود تطبيقي زائد. إنّ الفهم الدقيق لما تحصل عليه، وكيفية عمل الآلية من الداخل، وأين تختبئ مزالق الأداء — هو ما يُميّز المطوّر الذي يستخدم Spring Data بمهارة عن الذي يكتفي باستخدامها.
هرمية الواجهات الأساسية
تُوفّر Spring Data ثلاث واجهات مستودع ستتعامل معها مباشرة، مُرتّبة في هرمية من القدرات المتصاعدة:
Repository<T, ID>— واجهة العلامة الجذر. لا تحتوي على أي توابع. مفيدة حين تريد الاستفادة من بنية Spring Data (المسح، إنشاء البروكسي) لكنك تُعرّض فقط التوابع التي تُعلنها بنفسك.CrudRepository<T, ID>— تمتدّ منRepositoryوتُضيف 12 عملية CRUD قياسية.JpaRepository<T, ID>— تمتدّ منPagingAndSortingRepository(التي تُضيف الترقيم) وCrudRepository، ثم تُضيف عمليات خاصة بـ JPA: الحفظ الدفعي، والتحكم في الإخراج الفوري (flush)، ومراجع الكيانات.
JpaRepository في معظم مستودعات الإنتاج — ستحصل على كل شيء. انزل إلى CrudRepository حين تريد إخفاء توابع الترقيم والترتيب من المُستدعين لمنع تحميل الجداول الكبيرة بالكامل عن غير قصد.
تعريف أول مستودع لك
افترض أن لديك كيان Order بمفتاح أساسي من نوع Long. تعريف المستودع يتلخّص في واجهة واحدة:
هذا كل شيء. تُحلّل التهيئة التلقائية في Spring Boot حزمك، تعثر على هذه الواجهة، تُولّد بروكسي ديناميكي من نوع JDK يُطبّقها، وتُسجّله كـ Spring bean. تحقنه كأي bean آخر:
ما الذي يمنحك إياه CrudRepository مجانًا
بتوسيع CrudRepository (الذي تتضمّنه JpaRepository)، تحصل فورًا على هذه التوابع دون كتابة سطر واحد من SQL أو JPQL:
save(S entity)— يحفظ كيانًا جديدًا أو يدمج كيانًا موجودًا. يُعيد الكيان المُدار.saveAll(Iterable<S> entities)— حفظ/دمج دفعي.findById(ID id)— يُعيدOptional<T>. لا يُعيد null أبدًا؛ يُمثّل الغياب بشكل صريح.existsById(ID id)— يُنفّذSELECT COUNT— أرخص من تحميل الكيان بالكامل للتحقق من وجوده.findAll()— يحمّل كل صف في الجدول. استخدمه بحذر مع الجداول الكبيرة.findAllById(Iterable<ID> ids)— يحمّل مجموعة محددة من الكيانات بمفاتيحها الأساسية باستخدام جملة SQL من نوعIN.count()— يُعيد العدد الإجمالي للصفوف.deleteById(ID id)— يحذف بالمفتاح الأساسي؛ يرميEmptyResultDataAccessExceptionإذا لم يكن المعرّف موجودًا.delete(T entity)— يحذف الكيان المُعطى؛ يجب أن يكون الكيان مُدارًا أو منفصلًا مع معرّف صحيح.deleteAllById(Iterable<ID> ids)— يحذف بمجموعة معرّفات.deleteAll(Iterable<T> entities)— يحذف الكيانات المُعطاة.deleteAll()— يحذف كل صف. نادرًا ما تحتاجه في الإنتاج.
التوابع الإضافية في JpaRepository
تذهب JpaRepository أبعد من ذلك بعمليات خاصة بـ JPA:
findAll(Sort sort)/findAll(Pageable pageable)— استعلامات مُرتّبة أو مُرقَّمة (تُعالَج في درس الترقيم).saveAndFlush(S entity)— يحفظ ويُخرج فورًا إلى قاعدة البيانات ضمن المعاملة الحالية. مفيد في الاختبارات التي تحتاج التحقق من القيود قبل إتمام المعاملة.saveAllAndFlush(Iterable<S> entities)— حفظ دفعي ثم إخراج فوري.deleteAllInBatch()/deleteAllByIdInBatch(Iterable<ID> ids)— يُنفّذ جملةDELETESQL دفعية واحدة بدلًا من تحميل الكيانات واستدعاءdeleteعلى كل منها. أسرع بكثير للحذف الجماعي، لكنه يتجاوز ردود أفعال دورة حياة Hibernate والتسلسلات.getReferenceById(ID id)— يُعيد بروكسي Hibernate دون الاستعلام عن قاعدة البيانات. مفيد حين تحتاج مرجع كيان فقط لتلبية مفتاح خارجي على كيان آخر على وشك الحفظ.flush()— يُجبر Hibernate على مزامنة سياق الاستمرارية مع قاعدة البيانات فورًا.
getReferenceById لتجنّب SELECT غير الضرورية. إذا كنت تُعيّن حقل customer على Order جديد وتملك معرّف العميل بالفعل، استدعِ customerRepo.getReferenceById(customerId) بدلًا من findById. لن يُنفَّذ أي SELECT — سيُنشئ Hibernate بروكسي ولن يحلّه إلا إذا وصلت فعلًا إلى حقوله.
تابع save() — الكيانات الجديدة مقابل الموجودة
يبدو save(entity) بسيطًا لكن له سلوك مهم يجب فهمه:
خلف الكواليس، تتحقق SimpleJpaRepository في Spring Data ممّا إذا كان الكيان جديدًا باستدعاء EntityInformation.isNew(entity). افتراضيًا يتحقق هذا من كون حقل المعرّف null. عند الجدّة يستدعي EntityManager.persist()؛ وعند الوجود يستدعي EntityManager.merge().
save(entity) على كيان منفصل يُشغّل merge()، الذي ينسخ الحالة إلى نسخة مُدارة جديدة ويُعيدها. اعمل دائمًا مع النسخة المُعادة — التغييرات التي تُجريها على الكائن المنفصل الأصلي بعد الاستدعاء لن تُحفظ.
deleteAllInBatch مقابل deleteAll — تمييز حرج للأداء
هذا من أهم المفاضلات في Spring Data:
deleteAll()— يُحمّل كل كيان منفردًا (استعلام SELECT واحد لكل كيان ما لم يكن جلب الدُفعات مُهيَّأً)، ويُطلق أحداث دورة حياة Hibernate (@PreRemove، والتسلسلات، وحذف الأيتام) لكل منها، ثم يُصدر DELETE واحدًا لكل كيان. آمن وصحيح، لكنه O(n) من رحلات قاعدة البيانات.deleteAllInBatch()— يُنفّذ جملةDELETE FROM ordersواحدة دون أي تحميل. سريع للغاية، لكنه يتجاوز جميع تسلسلات Hibernate وردود أفعال دورة الحياة.
استخدم الحذف الدفعي حين تُنظّف جداول مرحلية أو بيانات اختبار أو مجموعات بدون تبعيات تسلسلية. استخدم deleteAll() حين يُهمّ السلوك الصحيح للتسلسل.
كيف تُولّد Spring Data التطبيق
عند بدء التشغيل تُنشئ Spring Data كائن SimpleJpaRepository<T, ID> مدعومًا بـ EntityManager لكل واجهة مستودع. تُعلَّق هذه الفئة بـ @Transactional(readOnly = true) على مستوى الفئة، مما يعني أن جميع توابع الاستعلام تعمل في معاملة للقراءة فقط افتراضيًا — وهو تحسين أداء حقيقي في Hibernate إذ يُعطّل التحقق من التعديلات (dirty checking) على الكيانات المحمَّلة. أما توابع الكتابة (save، delete) فمُعلَّقة منفردةً بـ @Transactional للقراءة والكتابة، ما يُعيد تعريف الإعداد على مستوى الفئة.
@Transactional على تابع الخدمة فقط حين يجب أن يمتد على عدة استدعاءات مستودع في وحدة عمل واحدة.
مثال عملي — OrderRepository في الاستخدام
الخلاصة
يُلغي تجريد المستودع طبقة DAO المكتوبة يدويًا بالكامل. تُغطّي CrudRepository العمليات الاثنتي عشرة الأساسية؛ وتُضيف JpaRepository الترقيم والتحكم في الإخراج الفوري والعمليات الدفعية المحسَّنة لـ JPA. تُولّد Spring Data بروكسي SimpleJpaRepository عند بدء التشغيل، مع الافتراضي على معاملات للقراءة فقط للاستعلامات ومعاملات للقراءة والكتابة للتعديلات. أتقن الفرق بين دلالات save/merge، والفجوة في الأداء بين deleteAll وdeleteAllInBatch، وحيلة الـ SELECT الصفرية باستخدام getReferenceById — هذه هي التفاصيل التي تُحدث فرقًا في الإنتاج. في الدرس القادم ستوسّع هذا الأساس بتوابع الاستعلام المشتقة التي تتيح لك الاستعلام بأي حقل كياني دون كتابة JPQL.