تعيين الحقول والأعمدة
تعيين الحقول والأعمدة
أوضحت الدروس السابقة كيفية تعيين كلاس الكيان واختيار استراتيجية المفتاح الأساسي. غير أن معظم عملك الفعلي يقع في الحقول الواقعة بين تلك المفاتيح الأساسية: التواريخ ورموز الحالة والنصوص الكبيرة والقيم العددية العديدة التي تشكّل كائن النطاق الحقيقي. يعالج Hibernate كل ذلك عبر مجموعة صغيرة من التعليقات التوضيحية — @Temporal و@Enumerated و@Lob — إلى جانب الأساس المتمثّل في @Column. يتناول هذا الدرس كلًّا منها بعمق، ويشرح المفاضلات، ويشير إلى الأخطاء التي تُوقع المطورين في الإنتاج.
@Column — التحكم في العمود الفعلي
بدون @Column يستنتج Hibernate اسم العمود من اسم الحقل باستخدام استراتيجية التسمية الخاصة به (افتراضيًا camelCase إلى snake_case في Spring Boot). يمكنك تجاوز ذلك وإضافة قيود يجب أن يُصدرها مولّد DDL باستخدام @Column:
السمات الرئيسية: name يضبط اسم العمود، nullable/unique يتحكمان في قيود DDL، length يُحدّد الحد الأقصى لـ VARCHAR، وprecision/scale يحكمان أعمدة DECIMAL. تتيح لك columnDefinition تمرير نص نوع SQL خام عندما تحتاج إلى سلوك خاص بقاعدة بيانات معينة.
nullable = false وunique = true فقط على توليد المخطط (spring.jpa.hibernate.ddl-auto=create). لا يُسبّبان قيام Hibernate بالتحقق من القيم في وقت التشغيل. للتطبيق في وقت التشغيل، استخدم Bean Validation (@NotNull، @Email) من jakarta.validation.
@Enumerated — تخزين تعدادات Java
لنفترض أن طلبك يحتوي على تعداد حالة:
بدون أي تعليق توضيحي يخزّن Hibernate الترتيب العددي للتعداد (0، 1، 2 ...) كعدد صحيح INTEGER. وهذا خطأ شبه دائم.
EnumType.STRING اسم الثابت ('PENDING'، 'SHIPPED' ...) الذي يبقى صحيحًا بعد إعادة الترتيب ويجعل صفوف قاعدة البيانات قابلة للقراءة من قِبل الإنسان. استخدم دائمًا EnumType.STRING.
في Hibernate 6 (Spring Boot 3) هناك خيار إضافي: تعيين التعداد إلى نوع ENUM الأصلي لقاعدة البيانات باستخدام @JdbcType أو AttributeConverter مخصص. لمعظم المشاريع، يُعدّ EnumType.STRING مع VARCHAR محدود الطول الاختيار العملي لأنه قابل للنقل عبر قواعد البيانات وقابل للقراءة بالكامل في استعلامات SQL.
@Temporal — حقول التاريخ والوقت
في Hibernate 6 مع Spring Boot 3 يجب عليك استخدام أنواع java.time المُقدَّمة في Java 8. يُعيّنها Hibernate تلقائيًا دون الحاجة إلى أي تعليق توضيحي:
LocalDate إلى DATE، LocalDateTime إلى DATETIME/TIMESTAMP، OffsetDateTime / Instant إلى TIMESTAMP الواعي للمنطقة الزمنية. لا حاجة لأي تعليق توضيحي إضافي.
تُعدّ @Temporal القديمة مُصمَّمة لأنواع java.util.Date وjava.util.Calendar القديمة التي تحمل غموضًا في المنطقة الزمنية وحالة قابلة للتعديل. قد تصادفها في القواعد البرمجية القديمة:
java.time. إنها غير قابلة للتعديل، وواضحة في المنطقة الزمنية، وتُعيَّن إلى أنواع SQL الصحيحة دون أي تعليق توضيحي. استخدم Instant أو OffsetDateTime لأي طابع زمني يجب أن يكون واعيًا للمنطقة الزمنية (مثل حقول المراجعة). استخدم LocalDate للتواريخ التقويمية التي لا تحتوي على مكوّن وقت (أعياد الميلاد وتواريخ الانتهاء).
النمط الشائع لطوابع المراجعة الزمنية يستخدم ردود فعل دورة حياة Spring Data JPA إلى جانب java.time:
@Lob — أعمدة الكائنات الكبيرة
تُشير @Lob إلى Hibernate لاستخدام واجهة JDBC للكائن الكبير (Large Object) للعمود. على حقل String تُعيَّن إلى CLOB (كائن حرفي كبير)؛ وعلى حقل byte[] تُعيَّن إلى BLOB (كائن ثنائي كبير).
SELECT a FROM Article a)، فستبثّ كل صف عمودها الكبير بأكمله من قاعدة البيانات، مما قد ينقل ميغابايتات لصفحة لا تحتاج سوى العناوين. حلّان للتخفيف: (1) استخدم @Basic(fetch = FetchType.LAZY) لتأجيل تحميل LOB حتى يُوصل إلى الحقل فعليًا — وإن كان ذلك يتطلب استخدام أدوات bytecode في وقت البناء. (2) والأكثر موثوقية، انقل الحقل الكبير إلى كيان منفصل مع علاقة @OneToOne كسولة وانضم إليه فقط عند الحاجة. بالنسبة للملفات التي يرفعها المستخدم، يُعدّ تخزين مسار الملف أو عنوان URL لتخزين كائن (S3/GCS) في عمود VARCHAR عادي وتقديم الثنائي من هناك الهندسة الأفضل دائمًا تقريبًا.
في Hibernate 6 قد يتصرف تعيين النوع لـ @Lob على String بشكل مختلف عبر قواعد البيانات. PostgreSQL مثلًا قد يُعيّنه إلى TEXT بدلًا من CLOB. إذا كنت تحتاج فقط إلى تخزين نص كبير على PostgreSQL يمكنك ببساطة التعليق باستخدام @Column(columnDefinition = "TEXT") وإغفال @Lob تمامًا، مما يتجنّب عبء بث CLOB.
تعيينات الأنواع المخصصة باستخدام AttributeConverter
عندما لا تُغطّي أيٌّ من التعليقات التوضيحية المدمجة نوعك — مثل قائمة List<String> من الوسوم المخزنة كعمود مفصول بفواصل، أو كائن قيمة Money — استخدم AttributeConverter:
طبّقه على الحقل:
jsonb، MySQL 5.7+ json)، خزّن البيانات الفرعية المنظَّمة بتنسيق JSON باستخدام @JdbcTypeCode(SqlTypes.JSON) الخاص بـ Hibernate بدلًا من صنع محوّل CSV يدوي. أعمدة JSON قابلة للفهرسة والاستعلام بدوال SQL الأصلية وأكثر مرونة بكثير.
الخلاصة
استخدم @Column للتحكم في اسم العمود الفعلي وقابلية القيم الفارغة والطول. خزّن التعدادات كنصوص باستخدام @Enumerated(EnumType.STRING) ولا تخزّنها أبدًا كأرقام ترتيبية. فضّل أنواع java.time — التي لا تحتاج تعليقات توضيحية — على النهج القديم المتمثّل في @Temporal/java.util.Date. احتفظ بـ @Lob للكائنات الكبيرة الحقيقية وكن واعيًا لتكلفة تحميلها المتحمّس؛ فكّر في تخزين الأصول الثنائية خارج قاعدة البيانات. للأنواع المخصصة، يمنحك AttributeConverter تحكمًا كاملًا مع فصل نظيف بين تمثيلَي Java وSQL.