تعرف على: "جحيم المُنشئ الفارغ" في Kotlin + JPA
\n\nإذا سبق لك أن حاولت التوفيق بين Kotlin وJPA (Java Persistence API)، فمن المحتمل أنك واجهت موقفًا يرمي فيه Hibernate استثناءً مثل:
\n\norg.hibernate.InstantiationException: No default constructor for entity: com.example.User\n\n\nمألوف؟ بالتأكيد. هذه هي المشكلة التي جمعت آلاف المشاهدات على Stack Overflow وأُطلق عليها الاسم الكئيب "Kotlin with JPA: default constructor hell".
\n\nفي هذه المقالة، سنشرح بهدوء، خطوة بخطوة:
\n- \n
- لماذا يحدث هذا الخطأ أساسًا
- لماذا Kotlin "لا يريد" إنشاء مُنشئ فارغ
- ثلاث طرق مجربة لحل المشكلة (من البسيط إلى الاحترافي)
- أي طريقة تختار لمشروعك
هيا بنا!
\n\nلماذا يتطلب Hibernate مُنشئًا بدون معاملات؟
\n\nلنبدأ مع Java. في Java التقليدية، اعتدنا على كتابة:
\n\npublic class User {\n private Long id;\n private String name;\n\n // مُنشئ فارغ إلزامي\n public User() {}\n\n // دوال الوصول (Getters) والتعديل (Setters)...\n}\n\n\nيستخدم Hibernate (وأي تطبيق JPA) الانعكاس (reflection) لإنشاء الكائنات. عندما يقرأ سطرًا من قاعدة البيانات، يحتاج إلى إنشاء مثيل للكلاس User، ثم ملء الحقول عبر الانعكاس. للقيام بذلك، يحتاج إلى مُنشئ بدون معاملات (default constructor).
في Java، يقوم المترجم تلقائيًا بإنشاء مُنشئ فارغ إذا لم تحدد أي مُنشئ آخر. لذلك، لا تظهر المشكلة عادةً.
\n\nوما هي خصوصية Kotlin؟
\n\nKotlin هي لغة تركز بشدة على الثبات (immutability) والأمان. انظر إلى كلاس Kotlin النموذجي:
\n\ndata class User(\n val id: Long = 0,\n val name: String = ""\n)\n\n\nهنا يحدث أمران رئيسيان:
\n- \n
- جميع الخصائص مُعلنة في المُنشئ الأساسي (primary constructor)
- الخصائص مُعلنة كـ
val(غير قابلة للتغيير) ولها قيم افتراضية
Kotlin لا يُنشئ مُنشئًا فارغًا منفصلاً إذا كان لديك مُنشئ أساسي بمعاملات. حتى لو تم تعيين قيم افتراضية لجميع المعاملات - فهذا لا يُعتبر "مُنشئًا حقيقيًا" بدون معاملات على مستوى bytecode لـ JVM.
\n\nعندما يحاول Hibernate استدعاء User.class.newInstance() عبر الانعكاس، فإنه لا يجد المُنشئ الفارغ - ويتعطل مع خطأ.
الطريقة 1: الأسهل - إضافة مُنشئ ثانوي بشكل صريح
\n\nأبسط طريق هو إضافة مُنشئ ثانٍ فارغ يدويًا:
\n\ndata class User(\n val id: Long = 0,\n val name: String = ""\n) {\n // مُنشئ ثانوي بدون معاملات\n constructor() : this()\n}\n\n\nهنا constructor() : this() يعني: "استدعِ المُنشئ الأساسي بالقيم الافتراضية".
الإيجابيات:
\n- \n
- بسيط جدًا وواضح
- لا يتطلب أي إضافات (plugins) أو إعدادات إضافية
السلبيات:
\n- \n
- تحتاج إلى كتابة هذا المُنشئ لكل كيان
- إذا كان لديك 10-15 كيانًا - يصبح الأمر مملًا
- من السهل نسيان إضافته
الطريقة 2: استخدام open class وجعل جميع الخصائص var
\n\nتقترح العديد من الأمثلة على الإنترنت هذا الحل:
\n\nopen class User {\n var id: Long = 0\n var name: String = ""\n}\n\n\nهنا نجعل الكلاس open (ليتمكن Hibernate من إنشاء proxy)، والخصائص var (قابلة للتغيير). وبدون مُنشئ أساسي - وبالتالي، سينشئ Kotlin مُنشئًا فارغًا افتراضيًا.
الإيجابيات:
\n- \n