ضبط الأداء والتحليل
ضبط الأداء والتحليل
كتابة كود Hibernate صحيح لا تمثّل إلا نصف العمل. في بيئة الإنتاج، تطبيق يُصدر مئات الاستعلامات في كل طلب، أو يحمّل كائنات لا تُقرأ أبدًا، أو يُدرج الصفوف واحدًا تلو الآخر، سيُعيق تطبيقك في صمت قبل أن تبلغ أي حد للأجهزة. يُزوّدك هذا الدرس بمجموعة أدوات عملية للكشف عن هذه المشكلات وإصلاحها: محرّك إحصاءات Hibernate المدمج، والجلب الدفعي للارتباطات، والإدراج والتحديث المجمّع على مستوى JDBC، وأكثر الأنماط المضادة شيوعًا.
تفعيل إحصاءات Hibernate
يتضمّن Hibernate نظامًا غنيًا للإحصاءات معطّلًا افتراضيًا. تفعيله يضيف عبئًا ضئيلًا في بيئات التطوير والاختبار، وهو لا غنى عنه لتحديد نقاط الازدحام قبل وصولها إلى الإنتاج.
في ملف application.properties:
بمجرد تفعيلها، يصبح كائن الإحصاءات متاحًا برمجيًا عبر SessionFactory:
EntityManagerFactory لا SessionFactory في كود JPA المعياري. افكّه عند بدء التشغيل باستخدام emf.unwrap(SessionFactory.class). يُهيئ Spring Boot تلقائيًا EntityManagerFactory من خصائص JPA، لذا يكون دائمًا متاحًا كحبّة (bean).
مشكلة N+1 في الاستعلامات
تُعدّ مشكلة N+1 أكثر ثغرات الأداء شيوعًا في Hibernate. تحدث عندما تُحمّل مجموعة من N كيان أب، ثم لكل منها تُشغّل ارتباطًا يُحمَّل كسولًا — ما يُنتج استعلامًا واحدًا للآباء ثم N استعلامًا للأبناء.
الحل هو إخبار Hibernate بالضم الجشع (join fetch) للارتباط في الاستعلام الأوّلي، ما يُلغي رحلات الذهاب والإياب الإضافية:
LIMIT/OFFSET إلى استعلام يستخدم JOIN FETCH على مجموعة، لا يستطيع Hibernate تطبيق الحد في SQL (لأن صف الأب الواحد يمتد على صفوف نتائج متعددة). يجلب كلّ الصفوف في الذاكرة ويُصفّحها هناك — وتظهر في السجل كـ: "HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory!". الحل: صفّح على معرّف الأب ثم اجلب المجموعة في استعلام ثانٍ، أو استخدم تلميح @BatchSize.
الجلب الدفعي مع @BatchSize
تُمثّل @BatchSize استراتيجيةً وسطى بين JOIN FETCH الكامل والتحميل الكسول البحت. بدلًا من إصدار SQL واحد لكل وصول إلى ارتباط، يجمّع Hibernate معرّفات الوكلاء (proxies) غير المُهيَّأة في دفعات ويجلبها بعبارة IN (...) واحدة. لا يستلزم ذلك أي تعديل على الكود المُستدعي.
يمكنك أيضًا تطبيق قيمة افتراضية عالمية في application.properties لتشمل كل الارتباطات الكسولة دفعةً واحدة:
اختيار الحجم المناسب ينطوي على مفاضلة: صغير جدًا وستظل تُصدر استعلامات كثيرة؛ كبير جدًا وستُحمّل بيانات لا تحتاجها. القيمة بين 16 و50 نقطة بداية شائعة — قِس أولًا ثم اضبط.
الإدراج والتحديث المجمّع عبر JDBC
عند حفظ أو دمج مئات الكيانات داخل معاملة واحدة، يُرسل Hibernate افتراضيًا كل INSERT وUPDATE إلى قاعدة البيانات بشكل منفرد. يجمّع تجميع JDBC تلك العبارات ويُرسلها في رحلة شبكة واحدة، مما قد يُقلّص زمن الاستجابة بمقدار عشرة أضعاف.
فعّله في application.properties:
GenerationType.IDENTITY، تُعيّن قاعدة البيانات المفتاح الأساسي عند الإدراج وتُعيده فورًا. يجب على Hibernate مسح كل صف منفردًا لاسترداد معرّفه — وهذا يُعطّل التجميع بصمت. انتقل إلى GenerationType.SEQUENCE (مع ضبط allocationSize ليطابق حجم دفعتك) لاستعادة التجميع.
مثال على حلقة إدراج مجمّع تستفيد من تجميع JDBC:
زوج flush() وclear() بالغ الأهمية. بدون clear()، تنمو الذاكرة المؤقتة الأولى بلا حدود — كل كيان تحفظه يبقى في الذاكرة طوال عمر الجلسة، وستتسبّب عملية استيراد مجمّع لـ 100,000 صف في OutOfMemoryError في نهاية المطاف.
الأنماط المضادة الشائعة في الأداء
- تحميل الكيان بالكامل عند الحاجة لأعمدة قليلة. استخدم الإسقاطات (interface-based أو DTO-based) أو JPQL مع
SELECT new MyDto(e.id, e.name)لتجنب نقل أعمدة غير مستخدمة. - استدعاء
findAll()على جدول كبير. استخدم التصفح الصفحي دائمًا:repository.findAll(PageRequest.of(page, size)). جدول بمليون صف سيُرسل كل صفوفه. - مزج القراءات والكتابات في حلقة واحدة. كل كتابة تُشغّل فحص الاتساق على كل الكيانات في الجلسة. افصل الاستعلامات للقراءة فقط (مع
@Transactional(readOnly = true)) عن عمليات الكتابة. - غياب فهارس قاعدة البيانات. يُصدر Hibernate SQL صحيحًا؛ لكن بدون فهرس على عمود المرشّح أو الربط، تُجري قاعدة البيانات مسحًا كاملًا للجدول. استخدم
@Indexعلى الكيان أو أدر الفهارس في نصوص الترحيل. - النمط المضاد Open Session in View. يُفعّل Spring Boot هذا افتراضيًا (
spring.jpa.open-in-view=true). يُبقي OSIV جلسة Hibernate مفتوحة طوال طلب HTTP بما يُتيح التحميل الكسول في طبقة العرض — لكنه يربط اتصال قاعدة بيانات طوال مدة الطلب بما فيها وقت التصيير. عطّله في الخدمات عالية الإنتاجية وحمّل الارتباطات جشعًا في طبقة الخدمة بدلًا من ذلك.
التحليل مع P6Spy وDatasource-Proxy
لتحليل أعمق في بيئة التطوير، تعترض أدوات كـ P6Spy أو datasource-proxy كل استدعاء JDBC وتُبلّغ عن SQL الفعلي مع المعاملات المرتبطة ووقت التنفيذ وعدد الاستدعاءات. أضف P6Spy إلى مسار الفئات في التطوير:
غيّر بادئة عنوان URL لمصدر البيانات إلى jdbc:p6spy:mysql://... وأضف ملف spy.properties. ستظهر كل جملة SQL في السجل مع استبدال المعاملات كاملًا — أكثر قراءةً بكثير من مخرجات Hibernate التي تستخدم ? بدلًا من القيم الفعلية.
الخلاصة
ضبط الأداء في Hibernate منهجي لا عشوائي. فعّل الإحصاءات للقياس أولًا، ثم أصلح أكبر المشكلات: حُلّ مشكلة N+1 باستخدام JOIN FETCH أو @BatchSize، فعّل تجميع JDBC لعمليات الكتابة المجمّعة (وانتقل إلى SEQUENCE حتى يعمل)، تجنّب تحميل بيانات أكثر مما تحتاج، وعطّل OSIV في الخدمات التي تهتم بضغط تجمّع الاتصالات. قِس في بيئة واقعية قبل كل تغيير وبعده لتأكيد التحسّن.