التتالي وحذف الأيتام
التتالي وحذف الأيتام
عندما تُثبّت كيانًا أو تُحدّثه أو تحذفه، غالبًا ما تحتاج إلى انتشار نفس العملية تلقائيًا عبر الارتباطات المرتبطة به. تمنحك آلية التتالي (cascade) في JPA وميزة حذف الأيتام (orphan removal) في Hibernate تحكمًا دقيقًا في كيفية انتشار عمليات دورة الحياة عبر رسم الكائنات المترابطة. إذا استُخدمت بشكل صحيح، فإنها تقلل من الكود المكرر بشكل كبير. أما إذا استُخدمت باستهتار، فقد تُفضي إلى عمليات حذف جماعية مفاجئة أو تمنع إعادة استخدام الكيانات المشتركة.
ما هو التتالي؟
كل تعليق توضيحي @OneToOne أو @OneToMany أو @ManyToOne أو @ManyToMany يقبل خاصية cascade تأخذ قيمة واحدة أو أكثر من CascadeType. عند تنفيذ عملية دورة حياة على الكيان المالك، تُطبّق JPA تلقائيًا نفس العملية على كل كيان مرتبط مُهيَّأت علاقته بنوع التتالي المطابق.
القائمة الكاملة لقيم CascadeType:
PERSIST— عند استدعاءem.persist(parent)، يتمّ تثبيت الأبناء أيضًا.MERGE— عند استدعاءem.merge(parent)، يتمّ دمج الأبناء أيضًا.REMOVE— عند استدعاءem.remove(parent)، يتمّ حذف الأبناء أيضًا.REFRESH— عند استدعاءem.refresh(parent)، يتمّ تحديث الأبناء من قاعدة البيانات أيضًا.DETACH— عند فصل الأب عن سياق المثابرة، يُفصَل الأبناء أيضًا.ALL— ثابت مختصر يعادل تحديد الأنواع الخمسة جميعها.
CascadeType.PERSIST عمليًا
أكثر نقطة بداية شيوعًا هي PERSIST. تخيّل كيان Order يمتلك مجموعة من كيانات LineItem. بدون التتالي ستضطر إلى تثبيت كل بند بشكل فردي قبل تثبيت الطلب، لأن Hibernate سيشكو من وجود نسخة عابرة في المجموعة.
الآن استدعاء em.persist(order) واحد (أو في Spring Data: orderRepository.save(order)) يتدفق إلى الأسفل ويُدرج كل LineItem في القائمة:
CascadeType.REMOVE ومخاطره
يُعدّ REMOVE قويًا لكنه خطير. عند حذف الأب، يُحذف كل ابن في المجموعة أيضًا. هذا صحيح لعلاقات التأليف (composition) — حيث لا يستطيع الابن الوجود بدون الأب — لكنه خاطئ لعلاقات الارتباط (association) حيث يكون الابن مشتركًا بين آباء متعددين.
CascadeType.REMOVE (أو ALL) أبدًا على @ManyToMany أو أي علاقة يُشار فيها إلى الكيان الهدف من آباء متعددة. حذف أحد الآباء سيتتالى إلى الابن المشترك ويُفسد جميع الآباء الأخرى التي تشير إليه. احتفظ بـ REMOVE لعلاقات التأليف الصارمة بين الأب والابن فقط.
CascadeType.ALL — اختصار مع تحفّظ
CascadeType.ALL اختصار للأنواع الخمسة جميعها. وهو مناسب عندما يكون الابن مملوكًا حصريًا للأب وينبغي أن تتبع دورة حياته الأب بالكامل. مثال كلاسيكي هو Address مُضمَّنة كيانًا منفصلًا داخل Customer:
CascadeType.ALL مقترنًا بـ orphanRemoval = true هو عادةً الاختيار الصحيح. أما إذا كان الابن مستقلًا أو يُشار إليه من مكان آخر، فتتالَ فقط PERSIST وMERGE.
حذف الأيتام
orphanRemoval = true ميزة في Hibernate/JPA تتجاوز CascadeType.REMOVE خطوة إضافية. بينما يُطلَق REMOVE فقط عند استدعاء remove() صراحةً على الأب، يُطلَق orphanRemoval أيضًا عند فصل الابن عن المجموعة — أي عند إزالة مرجع الابن من قائمة الأب دون حذف الأب نفسه.
عند الالتزام بالمعاملة، يكتشف Hibernate أن LineItem لم يعد مُشارًا إليه من أي مجموعة Order ويُصدر جملة DELETE تلقائيًا. هذا يُبقي كود الخدمة نظيفًا ويُلغي الحاجة إلى حقن مستودع الابن لمجرد حذف ابن.
التتالي PERSIST + MERGE (الإعداد الافتراضي الآمن لمعظم الحالات)
لمعظم علاقات الإنتاج يُوصى بتهيئة التتالي على {CascadeType.PERSIST, CascadeType.MERGE} بدون REMOVE وبدون orphanRemoval. هذا يتيح حفظ رسم الكائنات وتحديثها بسهولة دون إطلاق عمليات حذف جماعية غير متوقعة. أضف orphanRemoval = true و/أو CascadeType.REMOVE فقط بعد أن تقرر بوعي أن العلاقة هي تأليف صارم.
التتالي والعلاقات ثنائية الاتجاه
يُهيَّأ التتالي دائمًا على الكيان المالك في العلاقة — الجانب الذي يحمل المفتاح الخارجي، أو في زوج @OneToMany / @ManyToOne، عادةً على جانب @OneToMany الذي يمثل المجموعة. غير أن التتالي يعمل من خلال رسم الكائنات في الذاكرة وليس قاعدة البيانات. وهذا يعني أنك يجب أن تُبقي كلا جانبي علاقة ثنائية الاتجاه متزامنَين؛ وإلا قد لا يتتالى Hibernate إلى كيانات لا يستطيع رؤيتها في المجموعة.
اعتبارات الأداء
يُصدر تتالي REMOVE عبر JPA جمل DELETE فردية لكل كيان ابن — جملة SQL واحدة لكل ابن. إذا كان للأب آلاف الأبناء، فهذا أبطأ بشكل درامي من جملة DELETE FROM line_items WHERE order_id = ? واحدة على مستوى قاعدة البيانات. لسيناريوهات الحذف الجماعي، يُفضَّل استخدام استعلام أصلي أو تعليق @Query بدلًا من الاعتماد على تتالي الحذف.
DELETE JPQL أو أصليًا موجَّهًا بدلًا من ذلك وتجنّب تتالي REMOVE.
الخلاصة
يتحكم CascadeType في عمليات دورة حياة JPA التي تنتشر تلقائيًا من كيان أب إلى أبنائه المرتبطين. يُعدّ PERSIST وMERGE آمنَين للاستخدام الواسع. أما REMOVE وCascadeType.ALL فمناسبان فقط لعلاقات التأليف الصارمة حيث لا يستطيع الابن تجاوز الأب. يمتد orphanRemoval = true ليشمل حالة إزالة الابن من مجموعة أبيه في الذاكرة. معًا تُتيح لك هذه الميزات إدارة رسم كائنات غني دون كود مكرر — شريطة تطبيقها بعناية مع مراقبة SQL الذي تُولّده.