2. الكيانات JPA
2.1. مثال 1 - تمثيل كائن لجدول واحد
2.1.1. الجدول [personne]
لنفترض أن لدينا قاعدة بيانات تحتوي على جدول واحد [personne] مهمته تخزين بعض المعلومات عن الأفراد:
![]() |
المفتاح الأساسي للجدول | |
إصدار السطر في الجدول. في كل مرة يتم تعديل الشخص، يتم زيادة رقم إصداره. | |
اسم الشخص | |
اسمه الأول | |
تاريخ ميلاده | |
رقم صحيح 0 (غير متزوج) أو 1 (متزوج) | |
عدد أطفال الشخص |
2.1.2. الكيان [Personne]
نضع أنفسنا في بيئة التنفيذ التالية:
![]() |
يجب أن تشكل الطبقة JPA [5] جسراً بين العالم العلائقي لقاعدة البيانات [7] والعالم الكائني [4] الذي تعامله برامج Java [3]. يتم إنشاء هذا الجسر عن طريق التكوين، وهناك طريقتان للقيام بذلك:
- باستخدام ملفات XML. كانت هذه هي الطريقة الوحيدة تقريبًا للقيام بذلك حتى ظهور JDK 1.5
- باستخدام تعليقات Java منذ الإصدار JDK 1.5
في هذا المستند، سنستخدم الطريقة الثانية بشكل حصري تقريبًا.
قد يكون كائن [Personne] الذي يمثل الجدول [personne] المقدم سابقًا كما يلي:
...
@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// المنشئون
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
setNom(nom);
setPrenom(prenom);
setDatenaissance(datenaissance);
setMarie(marie);
setNbenfants(nbenfants);
}
// toString
public String toString() {
...
}
// مُستردات ومُعيّنات
...
}
يتم التكوين باستخدام تعليقات Java @Annotation. يتم استخدام تعليقات Java إما بواسطة المُجمِّع أو بواسطة أدوات متخصصة عند التنفيذ. باستثناء التعليق في السطر 3 المخصص للمُجمِّع، فإن جميع التعليقات هنا مخصصة للتنفيذ JPA المستخدم، Hibernate أو Toplink. وبالتالي سيتم استخدامها عند التنفيذ. في حالة عدم وجود أدوات قادرة على تفسيرها، يتم تجاهل هذه التعليقات التوضيحية. وبالتالي، يمكن استخدام الفئة [Personne] المذكورة أعلاه في سياق خارج JPA.
يجب التمييز بين حالتين لاستخدام التعليقات التوضيحية JPA في فئة C مرتبطة بجدول T:
- الجدول T موجود بالفعل: يجب أن تعكس التعليقات التوضيحية JPA ما هو موجود بالفعل (اسم الأعمدة وتعريفها، قيود التكامل، المفاتيح الخارجية، المفاتيح الأساسية، ...)
- الجدول T غير موجود وسيتم إنشاؤه بناءً على التعليقات التوضيحية الموجودة في الفئة C.
الحالة 2 هي الأسهل في التعامل معها. باستخدام التعليقات التوضيحية JPA، نحدد بنية الجدول T التي نريدها. غالبًا ما تكون الحالة 1 أكثر تعقيدًا. ربما تم إنشاء الجدول T منذ فترة طويلة، خارج أي سياق JPA. قد تكون بنيتها إذن غير ملائمة للجسر العلائقي / الكائني لـ JPA. للتبسيط، سنضع أنفسنا في الحالة 2 حيث سيتم إنشاء الجدول T المرتبط بالفئة C وفقًا للتعليقات التوضيحية JPA الخاصة بالفئة C.
دعونا نعلق على تعليقات JPA للفئة [Personne]:
- السطر 4: التعليق التوضيحي @Entity هو أول تعليق توضيحي ضروري. يتم وضعه قبل السطر الذي يعلن عن الفئة ويشير إلى أن الفئة المعنية يجب أن تدار بواسطة طبقة الاستمرارية JPA. في حالة عدم وجود هذا التعليق التوضيحي، سيتم تجاهل جميع التعليقات التوضيحية الأخرى JPA.
- السطر 5: تشير التعليقة التوضيحية @Table إلى جدول قاعدة البيانات الذي تمثل الفئة نسخة منه. حجتها الرئيسية هي name التي تشير إلى اسم الجدول. في حالة عدم وجود هذه الحجة، سيحمل الجدول اسم الفئة، وهو هنا [Personne]. في مثالنا، تكون التعليقة التوضيحية @Table غير ضرورية.
- السطر 8: تُستخدم العلامة @Id لتحديد الحقل في الفئة الذي يمثل المفتاح الأساسي للجدول. هذه العلامة إلزامية. وهي تشير هنا إلى أن الحقل id في السطر 11 يمثل المفتاح الأساسي للجدول.
- السطر 9: تُستخدم العلامة @Column لربط حقل في الفئة بالعمود في الجدول الذي يمثله الحقل. يشير السمة name إلى اسم العمود في الجدول. في حالة عدم وجود هذه السمة، يحمل العمود نفس اسم الحقل. في مثالنا، لم يكن الحجة name إلزامية. تشير الحجة nullable=false إلى أن العمود المرتبط بالحقل لا يمكن أن يكون له القيمة NULL، وبالتالي يجب أن يكون للحقل قيمة بالضرورة.
- السطر 10: تشير التعليقات التوضيحية @GeneratedValue إلى كيفية إنشاء المفتاح الأساسي عندما يتم إنشاؤه تلقائيًا بواسطة SGBD. وسيكون هذا هو الحال في جميع أمثلةنا. هذا ليس إلزاميًا. وبالتالي، قد يكون لشخصنا رقم طالب يستخدم كمفتاح أساسي ولا يتم إنشاؤه بواسطة SGBD بل يتم تحديده بواسطة التطبيق. في هذه الحالة، سيكون التعليق التوضيحي @GeneratedValue غائبًا. تشير الحجة strategy إلى كيفية إنشاء المفتاح الأساسي عندما يتم إنشاؤه بواسطة SGBD. لا تستخدم جميع SGBD نفس تقنية إنشاء قيم المفتاح الأساسي. على سبيل المثال:
يستخدم مولد قيم يتم استدعاؤه قبل كل عملية إدراج | |
يتم تعريف الحقل الرئيسي على أنه من النوع Identity. نحصل على نتيجة مشابهة لمولد القيم في Firebird، باستثناء أن قيمة المفتاح لا تُعرف إلا بعد إدراج السطر. | |
يستخدم كائنًا يسمى SEQUENCE والذي يلعب دور مولد القيم مرة أخرى |
يجب أن تقوم الطبقة JPA بتوليد أوامر SQL مختلفة وفقًا لـ SGBD لإنشاء مولد القيم. يتم تحديد نوع SGBD الذي يجب أن تديره من خلال التكوين. وبالتالي، يمكنها معرفة الاستراتيجية المعتادة لتوليد قيم المفتاح الأساسي لهذا SGBD. تشير الحجة strategy = GenerationType.AUTO إلى الطبقة JPA بأن عليها استخدام هذه الاستراتيجية المعتادة. نجحت هذه التقنية في جميع أمثلة هذا المستند بالنسبة لـ SGBD السبعة المستخدمة.
- السطر 14: تشير التعليقات التوضيحية @Version إلى الحقل المستخدم لإدارة الوصول المتزامن إلى نفس السطر في الجدول.
لفهم مشكلة الوصول المتزامن إلى نفس السطر في الجدول [personne]، لنفترض أن تطبيق ويب يسمح بتحديث شخص ما ولندرس الحالة التالية:
في الوقت T1، يدخل مستخدم U1 لتعديل شخص P. في هذه اللحظة، يكون عدد الأبناء 0. يقوم بتغيير هذا العدد إلى 1 ولكن قبل أن يثبت تعديله، يدخل مستخدم U2 لتعديل نفس الشخص P. وبما أن U1 لم يثبت تعديله بعد، يرى U2 على شاشته أن عدد الأطفال هو 0. يقوم U2 بتغيير اسم الشخص P إلى أحرف كبيرة. ثم يقوم U1 و U2 بتأكيد تعديلاتهما بهذا الترتيب. التعديل الذي أجراه U2 هو الذي سيسود: في قاعدة البيانات، سيتحول الاسم إلى أحرف كبيرة وسيبقى عدد الأطفال صفرًا، حتى وإن كان U1 يعتقد أنه قد غيّره إلى 1.
يساعدنا مفهوم إصدار الشخص في حل هذه المشكلة. لنأخذ نفس حالة الاستخدام:
في الوقت T1، يدخل مستخدم U1 لتعديل شخص P. في هذه اللحظة، يكون عدد الأبناء 0 والإصدار V1. يقوم بتغيير عدد الأبناء إلى 1 ولكن قبل أن يثبت تعديله، يدخل مستخدم U2 لتعديل نفس الشخص P. وبما أن U1 لم يثبت تعديله بعد، يرى U2 أن عدد الأطفال هو 0 والإصدار هو V1. يقوم U2 بتغيير اسم الشخص P إلى أحرف كبيرة. ثم يقوم كل من U1 و U2 بالتصديق على تعديلاتهما بهذا الترتيب. قبل التصديق على أي تعديل، يتم التحقق من أن الشخص الذي يقوم بتعديل الشخص P يمتلك نفس الإصدار الذي يمتلكه الشخص P المسجل حاليًا. سيكون هذا هو الحال بالنسبة للمستخدم U1. وبالتالي يتم قبول تعديله، ثم يتم تغيير إصدار الشخص المعدل من V1 إلى V2 للإشارة إلى أن الشخص قد خضع لتغيير. عند المصادقة على تعديل U2، سنلاحظ أن U2 يحتوي على إصدار V1 للشخص P، في حين أن إصداره الحالي هو V2. سنتمكن عندئذٍ من إخبار المستخدم U2 بأن شخصًا ما سبقه وأن عليه البدء من الإصدار الجديد للشخص P. وسيقوم بذلك، وسيحصل على شخص P من الإصدار V2 الذي لديه الآن طفل، وسيحول الاسم إلى أحرف كبيرة، ثم يوافق. سيتم قبول تعديله إذا كان الشخص P المسجل لا يزال يحمل الإصدار V2. في النهاية، سيتم أخذ التعديلات التي أجراها U1 و U2 في الاعتبار، بينما في حالة الاستخدام بدون إصدار، كان أحد التعديلات قد ضاع.
يمكن للطبقة [dao] في تطبيق العميل إدارة إصدار الفئة [Personne] بنفسها. في كل مرة يتم فيها تعديل كائن P، سيتم زيادة إصدار هذا الكائن بمقدار 1 في الجدول. تسمح التعليقات التوضيحية @Version بنقل هذه الإدارة إلى الطبقة JPA. لا يلزم أن يُسمى الحقل المعني version كما في المثال. يمكن أن يحمل أي اسم.
الحقول المطابقة للتعليقات التوضيحية @Id و@Version هي حقول موجودة بسبب الاستمرارية. لن تكون هناك حاجة إليها إذا لم تكن الفئة [Personne] بحاجة إلى الاستمرارية. نرى إذن أن الكائن لا يتم تمثيله بنفس الطريقة حسب ما إذا كان بحاجة إلى الاستمرارية أم لا.
- السطر 17: مرة أخرى التعليق التوضيحي @Column لتقديم معلومات عن عمود الجدول [personne] المرتبط بالحقل nom في الفئة Personne. نجد هنا حجتين جديدتين:
- unique=true تشير إلى أن اسم الشخص يجب أن يكون فريدًا. سيترجم ذلك في قاعدة البيانات بإضافة قيد فريد على العمود NOM في الجدول [personne].
- length=30 يحدد عدد أحرف العمود NOM بـ 30. وهذا يعني أن نوع هذا العمود سيكون VARCHAR(30).
- السطر 24: تُستخدم التعليقات التوضيحية @Temporal للإشارة إلى نوع SQL الذي يجب إعطاؤه لعمود/حقل من نوع التاريخ/الوقت. يشير النوع TemporalType.DATE إلى تاريخ فقط دون وقت مرتبط به. الأنواع الأخرى الممكنة هي TemporalType.TIME لترميز الساعة و TemporalType.TIMESTAMP لترميز التاريخ مع الساعة.
لنعلق الآن على بقية كود الفئة [Personne]:
- السطر 6: تنفذ الفئة واجهة Serializable. تتكون sérialisation لكائن ما من تحويله إلى سلسلة من البتات. désérialisation هي العملية العكسية. يُستخدم التسلسل/إلغاء التسلسل بشكل خاص في تطبيقات العميل/الخادم حيث يتم تبادل الكائنات عبر الشبكة. لا تعلم تطبيقات العميل أو الخادم بهذه العملية التي تتم بشكل شفاف بواسطة JVM. ولكن لكي تكون هذه العملية ممكنة، يجب أن تكون فئات الكائنات المتبادلة "موسومة" بالكلمة الرئيسية Serializable.
- السطر 37: منشئ للفئة. تجدر الإشارة إلى أن الحقلين id و version ليسا جزءًا من المعلمات. في الواقع، يتم إدارة هذين الحقلين بواسطة الطبقة JPA وليس بواسطة التطبيق.
- السطر 51 وما بعده: طرق get و set لكل حقل من حقول الفئة. تجدر الإشارة إلى أنه يمكن وضع التعليقات التوضيحية JPA على طرق get الخاصة بالحقول بدلاً من وضعها على الحقول نفسها. يحدد مكان التعليقات التوضيحية الوضع الذي يجب أن تستخدمه JPA للوصول إلى الحقول:
- إذا تم وضع التعليقات التوضيحية على مستوى الحقل، فسيصل JPA مباشرة إلى الحقول لقراءتها أو كتابتها
- إذا تم وضع التعليقات التوضيحية على مستوى get، فسيصل JPA إلى الحقول عبر طرق get / set لقراءتها أو كتابتها
إن موضع التعليق التوضيحي @Id هو الذي يحدد موضع التعليقات التوضيحية JPA لفئة ما. عند وضعها على مستوى الحقل، فإنها تشير إلى وصول مباشر إلى الحقول، وعند وضعها على مستوى get، فإنها تشير إلى الوصول إلى الحقول عبر get و set. يجب عندئذٍ وضع التعليقات التوضيحية الأخرى بنفس طريقة وضع التعليق التوضيحي @Id.
2.1.3. مشروع Eclipse للاختبارات
سنقوم بإجراء تجاربنا الأولى باستخدام الكيان [Personne] السابق. وسنجريها باستخدام البنية التالية:
![]() |
- في [7]: قاعدة البيانات التي سيتم إنشاؤها من تعليقات الكيان [Personne] بالإضافة إلى التكوينات الإضافية التي تم إجراؤها في ملف يسمى [persistence.xml]
- إلى [5, 6]: طبقة JPA تم تنفيذها بواسطة Hibernate
- في [4]: الكيان [Personne]
- إلى [3]: برنامج اختبار من نوع وحدة التحكم
سنقوم بإجراء تجارب متنوعة:
- إنشاء مخطط BD من نصوص Ant وأداة Hibernate Tools
- إنشاء BD وتهيئته ببعض البيانات
- استخدام BD وتنفيذ العمليات الأساسية الأربع على الجدول [personne] (الإدراج، التحديث، الحذف، الاستعلام)
الأدوات اللازمة هي التالية:
- Eclipse ومكوناته الإضافية الموضحة في الفقرة 5.2.
- مشروع [hibernate-personnes-entites] الموجود في المجلد <exemples>/hibernate/direct/personnes-entites
- مختلف ملفات SGBD الموضحة في الملاحق (الفقرة 5 وما بعدها).
مشروع Eclipse هو التالي:
![]() |
- في [1]: مجلد مشروع Eclipse
- في [2]: المشروع المستورد إلى Eclipse (ملف / استيراد)
- في [3]: الكيان [Personne] موضوع الاختبارات
- في [4]: برامج الاختبار
- في [5]: [persistence.xml] هو ملف تكوين الطبقة JPA
- في [6]: المكتبات المستخدمة. وقد تم وصفها في الفقرة 1.5.
- في [8]: نصوص برمجية ant ستُستخدم لإنشاء الجدول المرتبط بالكيان [Personne]
- في [9]: ملفات [persistence.xml] لكل من SGBD المستخدمة
- في [10]: مخططات قاعدة البيانات التي تم إنشاؤها لكل ملف من ملفات SGBD المستخدمة
سنقوم بوصف هذه العناصر واحدًا تلو الآخر.
2.1.4. الكيان [Personne] (2)
نقوم بإجراء تعديل طفيف على الوصف السابق للكيان [Personne] بالإضافة إلى معلومات إضافية:
package entites;
...
@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{
@Id
@Column(name = "ID", nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name = "VERSION", nullable = false)
@Version
private int version;
@Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
@Column(name = "PRENOM", length = 30, nullable = false)
private String prenom;
@Column(name = "DATENAISSANCE", nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(name = "MARIE", nullable = false)
private boolean marie;
@Column(name = "NBENFANTS", nullable = false)
private int nbenfants;
// المنشئات
public Personne() {
}
public Personne(String nom, String prenom, Date datenaissance, boolean marie,
int nbenfants) {
....
}
// toString
public String toString() {
return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
.format(getDatenaissance()), isMarie(), getNbenfants());
}
// الوصول والضبط
...
}
- السطر 7: نسمي الجدول المرتبط بالكيان [Personne] باسم [jpa01_personne]. في المستند، سيتم إنشاء جداول متنوعة في مخطط يُسمى دائمًا jpa. في نهاية هذا البرنامج التعليمي، سيحتوي المخطط jpa على العديد من الجداول. حتى يتمكن القارئ من التعرف عليها، ستحمل الجداول المرتبطة ببعضها البعض نفس البادئة jpaxx_.
- السطر 45: طريقة [toString] لعرض كائن [Personne] على وحدة التحكم.
2.1.5. تكوين طبقة الوصول إلى البيانات
في مشروع Eclipse أعلاه، يتم تكوين الطبقة JPA بواسطة الملف [META-INF/persistence.xml]:
![]() |
عند التنفيذ، يتم البحث عن الملف [META-INF/persistence.xml] في classpath الخاص بالتطبيق. في مشروع Eclipse الخاص بنا، يتم نسخ كل ما يوجد في المجلد [/src] [1] إلى مجلد [/bin] [2]. وهذا المجلد جزء من classpath الخاص بالمشروع. ولهذا السبب سيتم العثور على [META-INF/persistence.xml] عند تكوين الطبقة JPA.
بشكل افتراضي، لا يضع Eclipse أكواد المصدر في مجلد [/src] الخاص بالمشروع بل يضعها مباشرةً تحت المجلد نفسه. سيتم تكوين جميع مشاريع Eclipse الخاصة بنا بحيث تكون المصادر في [/src] والفئات المُجمَّعة في [/bin] كما هو موضح في الفقرة 5.2.1.
دعونا نلقي نظرة على تكوين الطبقة JPA الذي تم إجراؤه في الملف [persistence.xml] لمشروعنا:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- الفئات الدائمة -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- السجلات SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- الاتصال JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
<property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- إنشاء المخطط تلقائيًا -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- اللهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- خصائص DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
لفهم هذا التكوين، علينا العودة إلى بنية الوصول إلى البيانات في تطبيقنا:
![]() |
- سيقوم الملف [persistence.xml] بتكوين الطبقات [4, 5, 6]
- [4]: تنفيذ Hibernate لـ JPA
- [5]: يصل Hibernate إلى قاعدة البيانات عبر مجموعة اتصالات. مجموعة الاتصالات هي احتياطي من الاتصالات المفتوحة مع SGBD. يتم الوصول إلى SGBD من قبل العديد من المستخدمين، في حين أنه لأسباب تتعلق بالأداء، لا يمكن أن يتجاوز عددًا محددًا N من الاتصالات المفتوحة في وقت واحد. يفتح الكود المكتوب بشكل جيد اتصالاً مع SGBD لأقل وقت ممكن: فهو يرسل أوامر SQL ويغلق الاتصال. وسيقوم بذلك بشكل متكرر، في كل مرة يحتاج فيها إلى العمل مع قاعدة البيانات. تكلفة فتح/إغلاق اتصال ليست بالهينة، وهنا يأتي دور مجموعة الاتصالات. ستقوم هذه المجموعة عند بدء تشغيل التطبيق بفتح N1 اتصالات مع SGBD. وستطلب التطبيق منه اتصالاً مفتوحاً عندما تحتاج إليه. وسيتم إعادة هذا الاتصال إلى المجمع بمجرد أن لا يعود التطبيق بحاجة إليه، ويفضل أن يكون ذلك في أسرع وقت ممكن. لا يتم إغلاق الاتصال ويبقى متاحاً للمستخدم التالي. وبالتالي، فإن مجمع الاتصالات هو نظام لمشاركة الاتصالات المفتوحة.
- [6]: برنامج التشغيل JDBC المستخدم في SGBD
الآن لنرى كيف يقوم الملف [persistence.xml] بتكوين الطبقات [4, 5, 6] المذكورة أعلاه:
- السطر 2: العلامة الجذرية لملف XML هي <persistence>.
- السطر 3: <persistence-unit> تُستخدم لتعريف وحدة ثبات. يمكن أن يكون هناك عدة وحدات ثبات. لكل منها اسم (السمة name) ونوع معاملات (السمة transaction-type). سيتمكن التطبيق من الوصول إلى وحدة الثبات عبر اسمها، وهو jpa هنا. يشير نوع المعاملة RESOURCE_LOCAL إلى أن التطبيق يدير المعاملات بنفسه باستخدام SGBD. سيكون هذا هو الحال هنا. عندما يتم تشغيل التطبيق في حاوية EJB3، يمكنه استخدام خدمة المعاملات الخاصة بها. في هذه الحالة، سنضع transaction-type=JTA (Java Transaction Api). JTA هي القيمة الافتراضية عند غياب السمة transaction-type.
- السطر 5: تُستخدم العلامة <provider> لتعريف فئة تُنفذ واجهة [javax.persistence.spi.PersistenceProvider]، وهي واجهة تسمح للتطبيق بتهيئة طبقة الاستمرارية. نظرًا لاستخدامنا لتنفيذ JPA / Hibernate، فإن الفئة المستخدمة هنا هي فئة من Hibernate.
- السطر 6: تدرج العلامة <properties> خصائص خاصة بـ provider المحدد. وبالتالي، حسب ما إذا تم اختيار Hibernate أو Toplink أو Kodo...، فستكون هناك خصائص مختلفة. الخصائص التالية خاصة بـ Hibernate.
- السطر 8: يطلب من Hibernate استكشاف classpath الخاص بالمشروع للعثور على الفئات التي تحتوي على التعليق التوضيحي @Entity من أجل إدارتها. يمكن أيضًا إعلان فئات @Entity بواسطة علامات <class>nom_de_la_classe</class>، مباشرةً تحت علامة <persistence-unit>. وهذا ما سنفعله مع provider JPA / Toplink.
- تقوم الأسطر 10-12، الموضوعة هنا في تعليقات، بتكوين سجلات وحدة التحكم في Hibernate:
- السطر 10: لتحديد ما إذا كان سيتم عرض أوات أم لا الأوامر التي يصدرها Hibernate على SGBD. وهذا مفيد جدًا خلال مرحلة التعلم. بسبب الجسر العلائقي / الكائني، تعمل التطبيق على كائنات ثابتة تطبق عليها عمليات من نوع [persist, merge, remove]. من المثير للاهتمام معرفة الأوامر SQL التي يتم إصدارها فعليًا على هذه العمليات. من خلال دراستها، نتمكن تدريجيًا من تخمين الأوامر SQL التي سيقوم Hibernate بتوليدها عند إجراء مثل هذه العملية على الكائنات الدائمة، ويبدأ الجسر العلائقي/الكائني في التبلور في الذهن.
- السطر 11: يمكن تنسيق الأوامر SQL المعروضة على وحدة التحكم بشكل جميل لتسهيل قراءتها
- السطر 12: سيتم أيضًا إضافة تعليقات على الأوامر SQL المعروضة
- تحدد الأسطر 15-19 الطبقة JDBC (الطبقة [6] في البنية):
- السطر 15: فئة برنامج التشغيل JDBC لـ SGBD، هنا MySQL5
- السطر 16: عنوان URL لقاعدة البيانات المستخدمة
- السطران 17 و18: مستخدم الاتصال وكلمة المرور الخاصة به
- نستخدم هنا العناصر الموضحة في الملاحق في الفقرة 5.5. يُرجى من القارئ قراءة هذا القسم حول MySQL5.
- السطر 22: يحتاج Hibernate إلى معرفة SGBD الذي أمامه. في الواقع، تحتوي جميع SGBD على امتدادات SQL خاصة، وهي طريقة فريدة لإدارة التوليد التلقائي لقيم المفتاح الأساسي، ... مما يجعل Hibernate بحاجة إلى معرفة SGBD الذي يعمل معه من أجل إرسال الأوامر SQL التي سيفهمها هذا الأخير. يشير [MySQL5InnoDBDialect] إلى SGBD MySQL5 مع جداول من النوع InnoDB التي تدعم المعاملات.
- تقوم الأسطر 24-28 بتكوين مجموعة الاتصالات c3p0 (الطبقة [5] في البنية):
- السطران 24 و25: الحد الأدنى (الافتراضي 3) والحد الأقصى لعدد الاتصالات (الافتراضي 15) في المجموعة. العدد الافتراضي الأولي للاتصالات هو 3.
- السطر 26: المدة القصوى بالمللي ثانية لانتظار طلب اتصال من العميل. بعد انقضاء هذه المدة، سيرسل c3p0 استثناءً إليه.
- السطر 27: للوصول إلى BD، يستخدم Hibernate أوامر SQL المعدة مسبقًا (PreparedStatement) التي يمكن لـ c3p0 تخزينها في ذاكرة التخزين المؤقت. وهذا يعني أنه إذا طلب التطبيق مرة ثانية أمر SQL مُعد مسبقًا وموجود بالفعل في ذاكرة التخزين المؤقت، فلن تكون هناك حاجة لإعداده (إعداد أمر SQL له تكلفة) وسيتم استخدام الأمر الموجود في ذاكرة التخزين المؤقت. هنا، يتم تحديد العدد الأقصى لأوامر SQL المعدة التي يمكن أن تحتوي عليها ذاكرة التخزين المؤقت، لجميع الاتصالات مجتمعة (تنتمي أمر SQL المعد إلى اتصال واحد).
- السطر 28: تكرار التحقق من صلاحية الاتصالات بالمللي ثانية. قد تصبح إحدى اتصالات المجموعة غير صالحة لأسباب متنوعة (يقوم برنامج التشغيل JDBC بإبطال صلاحية الاتصال لأنه طويل جدًا، أو يعاني برنامج التشغيل JDBC من "أخطاء"، ...).
- السطر 20: يُطلب هنا، عند تهيئة وحدة الاستمرارية، إنشاء قاعدة بيانات صورة الكائنات @Entity. يمتلك Hibernate الآن جميع الأدوات اللازمة لإصدار أوامر SQL لإنشاء جداول قاعدة البيانات:
- تسمح له تكوين كائنات @Entity بمعرفة الجداول التي يجب إنشاؤها
- تسمح له الأسطر 15-18 و24-28 بالحصول على اتصال مع SGBD
- تسمح له السطر 22 بمعرفة اللهجة SQL التي يجب استخدامها لإنشاء الجداول
وبالتالي، فإن الملف [persistence.xml] المستخدم هنا يعيد إنشاء قاعدة بيانات جديدة عند كل تشغيل جديد للتطبيق. يتم إعادة إنشاء الجداول (create table) بعد إزالتها (drop table) في حال وجودها. تجدر الإشارة إلى أنه من الواضح أنه لا ينبغي القيام بذلك مع قاعدة بيانات قيد التشغيل...
أظهرت الاختبارات أن مرحلة حذف/إنشاء الجداول قد تفشل. وقد حدث ذلك على وجه الخصوص عند الانتقال، في نفس الاختبار، من طبقة JPA/Hibernate إلى طبقة JPA/Toplink أو العكس. انطلاقًا من نفس الكائنات @Entity، لا تولد كلتا العمليتين نفس الجداول والمولدات والتسلسلات... بالضبط، وقد حدث أحيانًا أن تفشل مرحلة الحذف/الإنشاء ونضطر إلى حذف الجداول يدويًا. يصف قسم "الملاحق"، الفقرة 5 وما بعدها، التطبيقات التي يمكن استخدامها للقيام بهذا العمل يدويًا. تجدر الإشارة إلى أن تطبيق JPA/Hibernate أثبت أنه الأكثر فعالية في هذه المرحلة من الإنشاء الأولي لمحتوى قاعدة البيانات: فقد كانت حالات التعطل نادرة.
توجد الأدوات المستخدمة من قبل طبقة JPA / Hibernate في مكتبة [jpa-hibernate]، المعروضة في الفقرة 1.5، الصفحة 8. توجد برامج التشغيل JDBC اللازمة للوصول إلى SGBD في مكتبة [jpa-divers]. تم وضع هاتين المكتبتين في classpath للمشروع المدروس هنا. ونذكر أدناه محتواهما:
![]() |
2.1.6. إنشاء قاعدة البيانات باستخدام برنامج نصي Ant
كما رأينا للتو، يوفر Hibernate أدوات لإنشاء قاعدة بيانات صورة لكائنات @Entity في التطبيق. يمكن لـ Hibernate:
- إنشاء ملف نصي للأوامر SQL التي تنشئ قاعدة البيانات. يتم عندئذ استخدام اللهجة الموجودة في [persistence.xml] فقط.
- إنشاء الجداول التي تمثل كائنات @Entity في قاعدة البيانات المستهدفة المحددة في [persistence.xml]. عندئذ يتم استخدام ملف [persistence.xml] بالكامل.
سنقدم نصوص Ant قادرة على إنشاء مخطط قاعدة البيانات، صورة كائنات @Entity. هذه النصوص ليست من تأليفي: فهي تستند إلى نصوص مماثلة من [ref1]. Ant (Another Neat Tool) هي أداة دفعية لمهام Java. ليست نصوص Ant سهلة الفهم للمبتدئين. لن نستخدم سوى واحدة منها، وهي التي نعلق عليها الآن:
![]() |
- في [1]: شجرة أمثلة هذا البرنامج التعليمي.
- في [2]: المجلد [personnes-entites] لمشروع Eclipse الذي ندرسه حاليًا
- في [3]: المجلد <lib> الذي يحتوي على المكتبات الخمس من ملفات jars المحددة في الفقرة 1.5.
- في [4]: الأرشيف [hibernate-tools.jar] الضروري لإحدى مهام البرنامج النصي [ant-hibernate.xml] الذي سنقوم بدراسته.
![]() |
- في [5]: مشروع Eclipse والنص البرمجي [ant-hibernate.xml]
- في [6]: مجلد [src] الخاص بالمشروع
سيستخدم البرنامج النصي [ant-hibernate.xml] [5] أرشيفات jars الموجودة في المجلد <lib> [3]، لا سيما الأرشيف [hibernate-tools.jar] [4] من المجلد [lib/hibernate]. لقد قمنا بإعادة إنتاج شجرة المجلدات حتى يرى القارئ أنه للعثور على المجلد [lib] من المجلد [personnes-entites] [2] في البرنامج النصي [ant-hibernate.xml]، يجب اتباع المسار: ../../../lib.
دعونا نلقي نظرة على البرنامج النصي [ant-hibernate.xml]:
<project name="jpa-hibernate" default="compile" basedir=".">
<!-- اسم المشروع والإصدار -->
<property name="proj.name" value="jpa-hibernate" />
<property name="proj.shortname" value="jpa-hibernate" />
<property name="version" value="1.0" />
<!-- الخصائص العامة -->
<property name="src.java.dir" value="src" />
<property name="lib.dir" value="../../../lib" />
<property name="build.dir" value="bin" />
<!-- مسار فئة المشروع -->
<path id="project.classpath">
<fileset dir="${lib.dir}">
<include name="**/*.jar" />
</fileset>
</path>
<!-- ملفات التكوين التي يجب أن تكون في مسار الفئات-->
<patternset id="conf">
<include name="**/*.xml" />
<include name="**/*.properties" />
</patternset>
<!-- تنظيف المشروع -->
<target name="clean" description="Nettoyer le projet">
<delete dir="${build.dir}" />
<mkdir dir="${build.dir}" />
</target>
<!-- تجميع المشروع -->
<target name="compile" depends="clean">
<javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
</target>
<!-- نسخ ملفات التكوين إلى مسار الفئات -->
<target name="copyconf">
<mkdir dir="${build.dir}" />
<copy todir="${build.dir}">
<fileset dir="${src.java.dir}">
<patternset refid="conf" />
</fileset>
</copy>
</target>
<!-- أدوات Hibernate -->
<taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />
<!-- إنشاء DDL من قاعدة البيانات -->
<target name="DDL" depends="compile, copyconf" description="Génération DDL base">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- استخدام META-INF/persistence.xml -->
<jpaconfiguration />
<!-- تصدير -->
<hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
<!-- إنشاء قاعدة البيانات -->
<target name="BD" depends="compile, copyconf" description="Génération BD">
<hibernatetool destdir="${basedir}">
<classpath path="${build.dir}" />
<!-- استخدام META-INF/persistence.xml -->
<jpaconfiguration />
<!-- تصدير -->
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
</hibernatetool>
</target>
</project>
- السطر 1: يُسمى المشروع [ant] "jpa-hibernate". وهو يجمع مجموعة من المهام، إحداها هي المهمة الافتراضية: وهي هنا المهمة المسماة "compile". يتم استدعاء البرنامج النصي ant لتنفيذ مهمة T. إذا لم يتم تحديد هذه المهمة، يتم تنفيذ المهمة الافتراضية. يشير basedir="." إلى أن نقطة البداية لجميع المسارات النسبية الموجودة في البرنامج النصي هي المجلد الذي يوجد فيه البرنامج النصي ant، وهو هنا المجلد <exemples>/hibernate/direct/personnes-entites.
- الأسطر 3-11: تحدد متغيرات البرنامج النصي باستخدام العلامة <property name="nomVariable" value="valeurVariable"/>. يمكن بعد ذلك استخدام المتغير في البرنامج النصي باستخدام الترميز ${nomVariable}. يمكن أن تكون الأسماء أي شيء. لنركز على المتغيرات المحددة في الأسطر 9-11:
- السطر 9: يحدد متغيرًا باسم "src.java.dir" (الاسم حر) والذي سيشير، في بقية البرنامج النصي، إلى المجلد الذي يحتوي على أكواد مصدر Java. قيمته هي "src"، وهو مسار نسبي للمجلد المحدد بواسطة السمة basedir (السطر 1). إذن، يتعلق الأمر بالمسار "./src" حيث تشير النقطة (.) هنا إلى المجلد <exemples>/hibernate/direct/personnes-entites. وتوجد أكواد Java المصدر بالفعل في المجلد <personnes-entites>/src (انظر [6] أعلاه).
- السطر 10: يحدد متغيرًا باسم "lib.dir" والذي سيشير، في بقية البرنامج النصي، إلى المجلد الذي يحتوي على أرشيفات jars التي تحتاجها مهام Java في البرنامج النصي. قيمته ../../../lib تشير إلى المجلد <exemples>/lib (انظر [3] أعلاه).
- السطر 11: يحدد متغيرًا باسم "build.dir" والذي سيشير، في بقية البرنامج النصي، إلى المجلد الذي يجب إنشاء ملفات .class فيه الناتجة عن ترجمة ملفات .java المصدرية. تشير قيمته "bin" إلى المجلد <personnes-entites>/bin. لقد أوضحنا سابقًا أن المجلد <bin> في مشروع Eclipse الذي درسناه هو المجلد الذي تم فيه إنشاء ملفات .class. وسيقوم Ant بنفس الشيء.
- الأسطر 14-18: تُستخدم علامة <path> لتحديد عناصر ملف classpath التي ستستخدمها مهام ant. هنا، يجمع المسار "project.classpath" (الاسم حر) جميع أرشيفات .jar الموجودة في شجرة المجلد <exemples>/lib.
- السطور 21-24: تُستخدم العلامة <patternset> لتعيين مجموعة من الملفات من خلال أنماط الأسماء. هنا، يشير patternset المسمى conf إلى جميع الملفات التي تحمل اللاحقة .xml أو .properties. سيُستخدم patternset للإشارة إلى ملفات .xml و.properties في المجلد <src> (persistence.xml، log4j.properties) (انظر [6]) وهي ملفات تكوين التطبيق. عند تنفيذ بعض المهام، يجب نسخ هذه الملفات إلى المجلد <bin> حتى تكون ضمن classpath للمشروع. عندئذٍ سنستخدم patternset conf للإشارة إليها.
- الأسطر 27-30: تشير العلامة <target> إلى مهمة في البرنامج النصي. هذه هي الأولى التي نواجهها. كل ما سبق يتعلق بتكوين بيئة تشغيل البرنامج النصي ant. تسمى المهمة clean. يتم تنفيذها على مرحلتين: يتم حذف المجلد <bin> (السطر 28) ليتم إعادة إنشاؤه بعد ذلك (السطر 29).
- الأسطر 33-35: المهمة compile التي هي المهمة الافتراضية للبرنامج النصي (السطر 1). وهي تعتمد (السمة depends) على المهمة clean. وهذا يعني أنه قبل تنفيذ المهمة compile، يجب على ant تنفيذ المهمة clean، c.a.d. لتنظيف المجلد <bin>. والغرض من المهمة compile هنا هو ترجمة مصادر Java من المجلد <src>.
- السطر 34: استدعاء مترجم Java بثلاثة معلمات:
- srcdir: المجلد الذي يحتوي على مصادر Java، وهو هنا المجلد <src>
- destdir: المجلد الذي يجب تخزين ملفات .class التي تم إنشاؤها فيه، وهو هنا المجلد <bin>
- classpathref: مسار الفئات (classpath) الذي سيتم استخدامه للتجميع، وهو هنا جميع ملفات jar الموجودة في شجرة المجلد <lib>
- (تابع)
- الأسطر 38-45: المهمة copyconf التي تهدف إلى نسخ جميع ملفات .xml و.properties من الملف <src> إلى المجلد <bin>.
- السطر 48: تعريف مهمة باستخدام العلامة <taskdef>. تهدف هذه المهمة إلى إعادة استخدامها في أماكن أخرى من البرنامج النصي. وهي وسيلة لتسهيل البرمجة. ونظرًا لأن المهمة تُستخدم في أماكن مختلفة من البرنامج النصي، يتم تعريفها مرة واحدة باستخدام العلامة <taskdef> ثم إعادة استخدامها عبر اسمها عند الحاجة.
- تسمى المهمة hibernatetool (السمة name).
- يتم تعريف فئتها بواسطة السمة classname. هنا، سيتم العثور على الفئة المحددة في الأرشيف [hibernate-tools.jar] الذي تحدثنا عنه سابقًا.
- تشير السمة classpathref إلى ant لمكان البحث عن الفئة السابقة
- (تابع)
- تتعلق الأسطر 51-60 بالمهمة التي تهمنا هنا، وهي إنشاء مخطط قاعدة بيانات صورة كائنات @Entity لمشروع Eclipse الخاص بنا.
- السطر 51: تسمى المهمة DDL (مثل Data Definition Language، وهي SQL المرتبطة بإنشاء كائنات قاعدة البيانات). وهي تعتمد على مهمتي compile و copyconf بهذا الترتيب. وبالتالي، ستؤدي المهمة DDL إلى تنفيذ المهام clean و compile و copyconf بالترتيب. عندما تبدأ المهمة DDL، يحتوي المجلد <bin> على ملفات .class لمصادر .java، ولا سيما كائنات @Entity، بالإضافة إلى الملف [META-INF/persistence.xml] الذي يقوم بتكوين الطبقة JPA / Hibernate.
- الأسطر 53-59: يتم استدعاء المهمة [hibernatetool] المحددة في السطر 48. يتم تمرير العديد من المعلمات إليها، بالإضافة إلى تلك المحددة بالفعل في السطر 48:
- السطر 53: سيكون مجلد الإخراج للنتائج التي تنتجها المهمة هو المجلد الحالي.
- السطر 54: سيكون مجلد <bin> هو مجلد المهمة classpath
- السطر 56: يوضح للمهمة [hibernatetool] كيف يمكنها معرفة بيئة التنفيذ الخاصة بها: تشير العلامة <jpaconfiguration/> إلى أنها في بيئة JPA وبالتالي يجب عليها استخدام الملف [META-INF/persistence.xml] الذي ستجده هنا في classpath.
- يحدد السطر 58 شروط إنشاء قاعدة البيانات: drop=true تشير إلى أنه يجب إصدار أوامر SQL drop table قبل إنشاء الجداول، و create=true تشير إلى أنه يجب إنشاء ملف نصي لأوامر SQL لإنشاء قاعدة البيانات، و outputfilename تشير إلى اسم هذا الملف SQL - هنا schema.sql في مجلد <ddl> لمشروع Eclipse، ويشير export=false إلى أنه لا يجب تشغيل الأوامر SQL التي تم إنشاؤها في اتصال بـ SGBD. هذه النقطة مهمة: فهي تعني أنه لتنفيذ المهمة، لا يلزم تشغيل الهدف SGBD. يحدد delimiter الحرف الذي يفصل بين أمرين SQL في المخطط الذي تم إنشاؤه، بينما يطلب format=true إجراء تنسيق أساسي للنص الذي تم إنشاؤه.
- تتعلق الأسطر 51-60 بالمهمة التي تهمنا هنا، وهي إنشاء مخطط قاعدة بيانات صورة كائنات @Entity لمشروع Eclipse الخاص بنا.
- (تابع)
- تحدد الأسطر 63-72 المهمة المسماة BD. وهي مطابقة للمهمة DDL السابقة، إلا أنها هذه المرة تقوم بإنشاء قاعدة البيانات (export="true" في السطر 70). تفتح المهمة اتصالاً على SGBD باستخدام المعلومات الموجودة في [persistence.xml]، لتشغيل المخطط SQL وإنشاء قاعدة البيانات. لتنفيذ المهمة BD، يجب إذن تشغيل SGBD.
2.1.7. تنفيذ المهمة قبل DDL
لتنفيذ البرنامج النصي [ant-hibernate.xml]، يتعين علينا أولاً إجراء بعض الإعدادات داخل Eclipse.
![]() |
- في [1]: حدد [External Tools]
- في [2]: قم بإنشاء إعداد جديد ant
![]() |
- إلى [3]: تسمية التكوين ant
- إلى [5]: تعيين البرنامج النصي ant باستخدام الزر [4]
- في [6]: تطبيق التعديلات
- في [7]: تم إنشاء التكوين ant DDL
![]() |
![]() |
- في [8]: في علامة التبويب JRE، يتم تحديد JRE المراد استخدامه. عادةً ما يتم ملء الحقل [10] مسبقًا بـ JRE المستخدم من قبل Eclipse. لذلك، لا يوجد عادةً ما يجب القيام به في هذه اللوحة. ومع ذلك، واجهت حالة لم يتمكن فيها البرنامج النصي ant من العثور على المُجمِّع <javac>. لا يوجد هذا المُجمِّع في JRE (Java Runtime Environment) بل في JDK (Java Development Kit). تجد أداة ant في Eclipse هذا المُجمِّع عبر متغير البيئة JAVA_HOME (ابدأ / لوحة التحكم / الأداء والصيانة / النظام / علامة التبويب خيارات متقدمة / زر متغيرات البيئة) [A]. إذا لم يتم تعريف هذه المتغير، يمكن السماح لـ ant بالعثور على المُجمِّع <javac> عن طريق وضع JDK في [10]، وليس JRE. وهذا متاح في نفس المجلد الذي يوجد فيه JRE [B]. سنستخدم الزر [9] للإعلان عن JDK ضمن JRE المتاح [C] حتى نتمكن بعد ذلك من تحديده في [10].
- في [12]: في علامة التبويب [Targets]، يتم تحديد المهمة DDL. وبالتالي فإن التكوين ant الذي أطلقنا عليه اسم DDL [7] سيتوافق مع تنفيذ المهمة المسماة DDL [12] التي، كما نعلم، تولد مخطط DDL لقاعدة بيانات كائنات @Entity في التطبيق.
![]() |
- في [13]: يتم التحقق من صحة التكوين
- في [14]: يتم تنفيذها
نحصل في عرض [console] على سجلات تنفيذ المهمة ant DDL:
Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
[delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
[mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
[javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
[copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
- نتذكر أن المهمة DDL اسمها [hibernatetool] (السطر 10) وأنها تعتمد على المهام clean (السطر 2)، compile (السطر 5) و copyconf (السطر 7).
- السطر 10: تستخدم المهمة [hibernatetool] الملف [persistence.xml] من تكوين JPA
- السطر 11: ستقوم المهمة [hbm2ddl] بإنشاء مخطط DDL لقاعدة البيانات
- الأسطر 12-22: مخطط قاعدة البيانات DDL
نتذكر أننا طلبنا من المهمة [hbm2ddl] إنشاء المخطط DDL في مكان محدد:
<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
- السطر 74: يجب إنشاء المخطط في الملف ddl/schema.sql. دعونا نتحقق من ذلك:
![]() |
- في [1]: الملف ddl/schema.sql موجود بالفعل (قم بتنفيذ F5 لتحديث شجرة الملفات)
- إلى [2]: محتواه. هذا هو مخطط قاعدة بيانات MySQL5. كان ملف [persistence.xml] الخاص بتكوين الطبقة JPA يحدد بالفعل ملف SGBD MySQL5 (السطر 8 أدناه):
<!-- تسجيل الدخول JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
<!-- إنشاء المخطط تلقائيًا -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- اللهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
<!-- الخصائص DataSource c3p0 -->
...
دعونا ندرس الجسر بين الكائن والعلاقة الذي تم إنشاؤه هنا من خلال فحص تكوين الكائن @Entity Personne والمخطط DDL الذي تم إنشاؤه:
![]() |
![]() |
تجدر الإشارة إلى بعض النقاط:
- A1-B1: اسم الجدول المحدد في A1 هو بالفعل نفس الاسم المستخدم في B1. يُلاحظ أن drop يسبق create في B1.
- A2-B2: يوضح طريقة إنشاء المفتاح الأساسي. أدى الوضع AUTO المحدد في A2 إلى السمة autoincrement الخاصة بـ MySQL5. عادةً ما يكون وضع إنشاء المفتاح الأساسي خاصًا بـ SGBD.
- A3-B3: يُظهر نوع SQL الخاص بـ MySQL5 لتمثيل نوع boolean في Java.
لنكرر هذا الاختبار مع SGBD آخر:
![]() |
- يحتوي المجلد [conf] [1] على ملفات [persistence.xml] لمختلف SGBD. خذ ملف Oracle [2] على سبيل المثال وضعه في المجلد [META-INF] [3] بدلاً من الملف السابق. محتواه هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- الفئات الدائمة -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- السجلات SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- الإنشاء التلقائي للمخطط -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- اللهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
<!-- خصائص DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
يُرجى من القارئ الاطلاع على الملحقات، القسم الخاص بـ Oracle (الفقرة 5.7)، لا سيما لفهم التكوين JDBC.
السطر 25 هو الوحيد المهم حقًا هنا: يتم إخبار Hibernate بأن SGBD هو الآن SGBD Oracle. يؤدي تنفيذ المهمة ant DDL إلى النتيجة [4] المذكورة أعلاه. يُلاحظ أن مخطط Oracle يختلف عن مخطط MySQL5. هذه إحدى نقاط قوة JPA: لا يحتاج المطور إلى الاهتمام بهذه التفاصيل، مما يزيد بشكل كبير من قابلية نقل تطويراته.
2.1.8. تنفيذ المهمة مقابل BD
ربما نتذكر أن المهمة ant المسماة BD تقوم بنفس ما تقوم به المهمة ant DDL ولكنها تقوم أيضًا بإنشاء قاعدة البيانات. لذلك يجب تشغيل المهمة SGBD. سنأخذ حالة SGBD و MySQL5، وندعو القارئ إلى نسخ الملف [conf/mysql5/persistence.xml] إلى المجلد [src/META-INF]. للتحقق من عمل المهمة، سنستخدم المكون الإضافي SQL Explorer (انظر الفقرة 5.2.6) للتحقق من حالة ملف jpa BD قبل وبعد تنفيذ المهمة ant BD.
أولاً، علينا إنشاء تكوين جديد ant لتنفيذ المهمة BD. يُطلب من القارئ اتباع الخطوات الموضحة لتكوين DDL في الفقرة 2.1.7. سيُسمى التكوين الجديد ant باسم BD:
![]() |
- إلى [1]: يتم نسخ التكوين السابق المسمى DDL
- إلى [2]: نسمي التكوين الجديد BD. وهي تنفذ المهمة ant BD [3] التي تقوم بإنشاء قاعدة البيانات فعليًا.
- بعد ذلك، قم بتشغيل SGBD و MySQL5 (الفقرة 5.5).
نستخدم الآن المكون الإضافي SQL Explorer لاستكشاف قواعد البيانات التي يديرها SGBD. يجب على القارئ أولاً التعرف على هذا المكون الإضافي إذا لزم الأمر (انظر الفقرة 5.2.6).
![]() |
- [1]: نفتح منظور SQL Explorer [Window / Open Perspective / Other]
- [2]: قم بإنشاء اتصال [mysql5-jpa] إذا لزم الأمر (انظر الفقرة 5.5.5، الصفحة 252) وافتحه
- [3]: نقوم بتسجيل الدخول jpa / jpa
- [4]: يتم الاتصال بـ MySQL5.
![]() |
- في [5]: لا يحتوي jpa في BD إلا على جدول واحد: [articles]
- في [6]: نبدأ تنفيذ المهمة ant BD. نظرًا لأننا في منظور [SQL Explorer]، لا نرى العرض [Console] الذي يعرض سجلات المهمة. يمكننا عرض هذا العرض [Window / Show View / ...] أو العودة إلى منظور Java [Window / Open Perspective / ...].
- في [7]: بمجرد اكتمال المهمة السابقة BD، يمكن العودة إلى منظور [SQL Explorer] وتحديث شجرة BD jpa.
- في [8]: نرى الجدول [jpa01_personne] الذي تم إنشاؤه.
يُطلب من القارئ إعادة إنشاء BD باستخدام SGBD أخرى. الإجراء الذي يجب اتباعه هو التالي:
- نسخ الملف [conf/<sgbd>/persistence.xml] إلى المجلد [src/META-INF] حيث <sgbd> هو SGBD الذي تم اختباره
- تشغيل <sgbd> باتباع التعليمات الواردة في المرفقات المتعلقة به
- في واجهة SQL Explorer، قم بإنشاء اتصال بـ <sgbd>. وهذا موضح أيضًا في المرفقات لكل ملف من ملفات SGBD
- أعد إجراء الاختبارات السابقة
بعد الوصول إلى هذه المرحلة، لدينا عدد من المكتسبات:
- نحن نفهم بشكل أفضل مفهوم الجسر بين الكائنات والعلاقات. هنا تم تحقيقه بواسطة Hibernate. سنستخدم Toplink لاحقًا.
- نعلم أن جسر الكائنات/العلاقات هذا يتم تكوينه في مكانين:
- في كائنات @Entity، حيث نحدد الروابط بين حقول الكائنات وأعمدة جداول BD
- في [META-INF/persistence.xml]، حيث نزود التنفيذ JPA بمعلومات حول عنصري الجسر بين الكائنات والعلاقات: كائنات @Entity (الكائنات) وقاعدة البيانات (العلاقات).
- لقد أنشأنا مهمتين ant، تسميان DDL و BD، اللتين تسمحان لنا بإنشاء قاعدة البيانات من التكوين السابق، حتى قبل كتابة أي كود Java.
الآن بعد أن تم تكوين طبقة JPA لتطبيقنا بشكل صحيح، يمكننا البدء في استكشاف API و JPA باستخدام كود Java.
2.1.9. : سياق استمرارية التطبيق
دعونا نوضح قليلاً بيئة تشغيل عميل JPA:
![]() |
نحن نعلم أن الطبقة JPA [2] تنشئ جسرًا بين الكائنات [3] والعلاقات [4]. يُطلق على مجموعة الكائنات التي تديرها الطبقة JPA في إطار هذا الجسر بين الكائنات والعلاقات اسم "سياق الاستمرارية". للوصول إلى بيانات سياق الاستمرارية، يجب على عميل JPA [1] المرور عبر الطبقة JPA [2]:
- يمكنه إنشاء كائن وطلب من الطبقة JPA جعله ثابتًا. يصبح الكائن عندئذ جزءًا من سياق الثبات.
- يمكنه أن يطلب من الطبقة [JPA] مرجعًا لكائن ثابت موجود.
- يمكنه تعديل كائن دائم تم الحصول عليه من الطبقة JPA.
- يمكنه أن يطلب من الطبقة JPA حذف كائن من سياق الاستمرارية.
تقدم الطبقة JPA للعميل واجهة تسمى [EntityManager] والتي، كما يوحي اسمها، تسمح بإدارة كائنات @Entity في سياق الاستمرارية. نعرض أدناه الطرق الرئيسية لهذه الواجهة:
تضع entity في سياق الاستمرارية | |
يزيل entity من سياق الاستمرارية | |
يدمج كائن entity من العميل غير المدار بواسطة سياق الاستمرارية مع الكائن entity من سياق الاستمرارية الذي له نفس المفتاح الأساسي. والنتيجة هي الكائن entity من سياق الاستمرارية. | |
يضع في سياق الاستمرارية كائنًا تم البحث عنه في قاعدة للقاعدة عبر مفتاحها الأساسي. يسمح النوع T للكائن للطبقة JPA معرفة الجدول الذي يجب الاستعلام عنه. يتم إرجاع الكائن الدائم الذي تم إنشاؤه بهذه الطريقة إلى العميل. | |
يُنشئ كائن Query من استعلام JPQL (لغة استعلامات الاستمرارية في Java). استعلام JPQL مشابه للاستعلام SQL باستثناء أنه يتم الاستعلام عن الكائنات بدلاً من الجداول. | |
طريقة مشابهة للطريقة السابقة، باستثناء أن queryText هو أمر SQL وليس JPQL. | |
طريقة مماثلة لـ createQuery، باستثناء أن الأمر JPQL queryText قد تم نقله إلى ملف تكوين وربطه باسم. وهذا الاسم هو معلمة الطريقة. |
يتمتع الكائن EntityManager بدورة حياة ليست بالضرورة هي نفس دورة حياة التطبيق. له بداية ونهاية. وبالتالي، يمكن لعميل JPA العمل بالتتابع مع كائنات EntityManager مختلفة. سياق الاستمرارية المرتبط بـ EntityManager له نفس دورة الحياة التي يتمتع بها. وهما لا ينفصلان عن بعضهما البعض. عندما يتم إغلاق كائن EntityManager، يتم مزامنة سياق الاستمرارية الخاص به مع قاعدة البيانات إذا لزم الأمر، ثم يختفي. يجب إنشاء كائن EntityManager جديد للحصول على سياق استمرارية جديد.
يمكن للعميل JPA إنشاء كائن EntityManager وبالتالي سياق استمرارية باستخدام الأمر التالي:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
- javax.persistence.Persistence هي فئة ثابتة تسمح بالحصول على مصنع (factory) لكائنات EntityManager. يرتبط هذا المصنع بوحدة استمرارية محددة. نتذكر أن ملف التكوين [META-INF/persistence.xml] يسمح بتعريف وحدات الاستمرارية وأن لهذه الوحدات أسماء:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
في المثال أعلاه، تسمى وحدة الاستمرارية jpa. وتأتي معها مجموعة كاملة من الإعدادات الخاصة بها، بما في ذلك SGBD التي تعمل معها. تقوم التعليمات [Persistence.createEntityManagerFactory("jpa")] بإنشاء مصنع كائنات من النوع EntityManagerFactory قادر على توفير كائنات EntityManager المخصصة لإدارة سياقات الاستمرارية المرتبطة بوحدة الاستمرارية المسماة jpa. يتم الحصول على كائن EntityManager وبالتالي على سياق استمرارية من الكائن EntityManagerFactory بالطريقة التالية:
تسمح الطرق التالية للواجهة [EntityManager] بإدارة دورة حياة سياق الاستمرارية:
يتم إغلاق سياق الاستمرارية. يفرض مزامنة سياق الاستمرارية مع قاعدة البيانات:
| |
يتم إفراغ سياق الاستمرارية من جميع كائناته ولكن لا يتم إغلاقه. | |
يتم مزامنة سياق الاستمرارية مع قاعدة البيانات بالطريقة الموضحة لـ close() |
يمكن للعميل JPA فرض مزامنة سياق الاستمرارية مع قاعدة البيانات باستخدام الطريقة [EntityManager].flush السابقة. يمكن أن تكون المزامنة صريحة أو ضمنية. في الحالة الأولى، يتعين على العميل إجراء عمليات flush عندما يرغب في إجراء عمليات المزامنة، وإلا فإن هذه العمليات تتم في أوقات معينة سنحددها لاحقًا. يتم إدارة وضع المزامنة من خلال الطرق التالية للواجهة [EntityManager]:
هناك قيمتان محتملتان لـ flushmode: FlushModeType.AUTO (الافتراضي): تتم المزامنة قبل كل استعلام SELECT يتم إجراؤه على قاعدة البيانات. FlushModeType.COMMIT: لا تتم المزامنة إلا في نهاية المعاملات على قاعدة البيانات. | |
يعرض وضع المزامنة الحالي |
لنلخص. في الوضع FlushModeType.AUTO، وهو الوضع الافتراضي، سيتم مزامنة سياق الاستمرارية مع قاعدة البيانات في الأوقات التالية:
- قبل كل عملية SELECT على قاعدة البيانات
- في نهاية معاملة على قاعدة البيانات
- بعد إجراء عملية flush أو close على سياق الاستمرارية
في الوضع FlushModeType.COMMIT، الأمر نفسه باستثناء العملية 1 التي لا تحدث. الوضع العادي للتفاعل مع الطبقة JPA هو وضع معاملاتي. يقوم العميل بعمليات متنوعة على سياق الاستمرارية، داخل معاملة. في هذه الحالة، تكون لحظات مزامنة سياق الاستمرارية مع قاعدة البيانات هي الحالتان 1 و 2 أعلاه في الوضع AUTO، والحالة 2 فقط في الوضع COMMIT.
نختتم بـ API من واجهة Query، وهي واجهة تسمح بإصدار أوامر JPQL على سياق الاستمرارية أو أوامر SQL مباشرة على قاعدة البيانات لاسترداد البيانات منها. واجهة Query هي كما يلي:
![]() |
سنضطر إلى استخدام الطرق من 1 إلى 4 المذكورة أعلاه:
- 1 - الطريقة getResultList تنفذ SELECT التي تعيد عدة كائنات. سيتم الحصول على هذه الكائنات في كائن List. هذا الكائن هو واجهة. توفر هذه الواجهة كائن Iterator الذي يسمح بتصفح عناصر القائمة L بالشكل التالي:
Iterator iterator = L.iterator();
while (iterator.hasNext()) {
// استغلال الكائن iterator.next() الذي يمثل العنصر الحالي في القائمة
...
}
يمكن أيضًا استخدام القائمة L مع كائن for:
for (Object o : L) {
// استخدام الكائن o
}
- 2 - الطريقة getSingleResult تنفذ أمر JPQL / SQL SELECT الذي يعيد كائنًا واحدًا.
- 3 - الطريقة executeUpdate تنفذ أمر SQL للتحديث أو الحذف وتُرجع عدد الأسطر التي تأثرت بالعملية.
- 4 - تسمح الطريقة setParameter(String, Object) بتعيين قيمة لمعلمة مسماة في أمر JPQL تم تعيين معلماته
- 5 - الطريقة setParameter(int, Object) ولكن المعلمة لا يتم تحديدها باسمها بل بموقعها في الأمر JPQL.
2.1.10. عميل أول JPA
لنعد إلى نظرة عامة على المشروع من منظور Java:
![]() |
نحن نعرف الآن كل شيء تقريبًا عن هذا المشروع باستثناء محتوى المجلد [src/tests] الذي ندرسه الآن. يحتوي المجلد على برنامجين لاختبار الطبقة JPA:
- [InitDB.java] هو برنامج يضع بضع أسطر في الجدول [jpa01_personne] في قاعدة البيانات. سيعطينا كوده العناصر الأولى للطبقة JPA.
- [Main.java] هو برنامج يقوم بعمليات CRUD على الجدول [jpa01_personne]. سيسمح لنا دراسة شفرته بالتطرق إلى المفاهيم الأساسية لسياق الاستمرارية ودورة حياة الكائنات في هذا السياق.
2.1.10.1. الكود
كود برنامج [InitDB.java] هو كما يلي:
package tests;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import entites.Personne;
public class InitDB {
// الثوابت
private final static String TABLE_NAME = "jpa01_personne";
public static void main(String[] args) throws ParseException {
// وحدة الثبات
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
// استرداد EntityManagerFactory من وحدة الاستمرارية
EntityManager em = emf.createEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف العناصر من جدول الأشخاص
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// إنشاء شخصين
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// نهاية المعاملة
tx.commit();
// نهاية EntityManager
em.close();
// نهاية EntityManagerFactory
emf.close();
// سجل
System.out.println("terminé ...");
}
}
يجب قراءة هذا الكود في ضوء ما تم شرحه في الفقرة 2.1.9.
- السطر 19: يتم طلب كائن EntityManagerFactory emf لوحدة الاستمرارية jpa (المحددة في persistence.xml). عادةً ما تتم هذه العملية مرة واحدة فقط خلال دورة حياة التطبيق.
- السطر 21: يتم طلب كائن EntityManager em لإدارة سياق الاستمرارية.
- السطر 23: يتم طلب كائن Transaction لإدارة معاملة. نذكر هنا أن العمليات على سياق الاستمرارية تتم داخل معاملة. سنرى أن هذا ليس إلزاميًا، ولكن قد نواجه مشاكل في هذه الحالة. إذا كان التطبيق يعمل في حاوية EJB3، فإن العمليات على سياق الاستمرارية تتم دائمًا داخل معاملة.
- السطر 24: تبدأ المعاملة
- السطر 26: تنفيذ أمر حذف SQL على الجدول "jpa01_personne" (nativeQuery). يتم ذلك لإفراغ الجدول من أي محتوى وبالتالي رؤية نتيجة تنفيذ التطبيق بشكل أفضل [InitDB]
- السطران 28-29: يتم إنشاء كائنين Personne p1 و p2. هذان كائنان عاديان ولا علاقة لهما في الوقت الحالي بسياق الاستمرارية. فيما يتعلق بسياق الاستمرارية، يقول Hibernate أن هذه الكائنات في حالة مؤقتة (transient) لتمييزها عن الكائنات المستمرة (persistent) التي يديرها سياق الاستمرارية. سنستخدم مصطلح "كائنات غير ثابتة" (non-persistent) للإشارة إلى أنها لم تُدار بعد بواسطة سياق الثبات، ومصطلح "كائنات ثابتة" (persistent) للإشارة إلى تلك التي تُدار بواسطته. سنجد فئة ثالثة من الكائنات، وهي الكائنات المنفصلة (detached)، وهي كائنات كانت ثابتة سابقًا ولكن تم إغلاق سياق الثبات الخاص بها. يمكن أن يحتفظ العميل بمراجع لهذه الكائنات، وهو ما يفسر عدم تدميرها بالضرورة عند إغلاق سياق الاستمرارية. ونقول عندئذ إنها في حالة منفصلة. تسمح العملية [EntityManager].merge بإعادة ربطها بسياق استمرارية تم إنشاؤه حديثًا.
- السطور 31-32: يتم دمج الشخصين p1 و p2 في سياق الاستمرارية بواسطة العملية [EntityManager].persist. وبذلك يصبحان كائنين مستمرين.
- السطور 35-37: يتم تنفيذ أمر JPQL "select p from Personne p order by p.nom asc". Personne ليست الجدول (الذي يُسمى jpa01_personne) بل الكائن @Entity المرتبط بالجدول. لدينا هنا استعلام JPQL (لغة استعلامات الثبات في Java) على سياق الثبات وليس أمر SQL على قاعدة البيانات. ومع ذلك، باستثناء الكائن Personne الذي حل محل الجدول jpa01_personne، فإن الصيغ متطابقة. تقوم حلقة for بتصفح قائمة (الأشخاص) الناتجة عن select لعرض كل عنصر منها على وحدة التحكم. نسعى هنا للتحقق من وجود العناصر الموضوعة في سياق الاستمرارية في الأسطر 31-32 في الجدول. بشكل شفاف، ستتم مزامنة سياق الاستمرارية مع قاعدة البيانات. في الواقع، سيتم إصدار استعلام select وقد ذكرنا أن هذه كانت إحدى الحالات التي تتم فيها المزامنة. لذلك، في هذه اللحظة، في الخلفية، سيقوم JPA / Hibernate بإصدار الأمرين SQL و insert اللذين سيقومان بإدراج الشخصين في الجدول jpa01_personne. لم تقم العملية persist بذلك. تدمج هذه العملية الكائنات في سياق الاستمرارية دون أن يكون لذلك أي تأثير على قاعدة البيانات. تتم العمليات الفعلية عند عمليات المزامنة، وهنا قبل select مباشرةً في قاعدة البيانات.
- السطر 39: ننهي المعاملة التي بدأت في السطر 24. ستحدث عملية مزامنة مرة أخرى. لن يحدث شيء هنا لأن سياق الاستمرارية لم يتغير منذ آخر عملية مزامنة.
- السطر 41: يتم إغلاق سياق الاستمرارية.
- السطر 43: يتم إغلاق مصنع EntityManager.
2.1.10.2. تنفيذ الكود
- تشغيل SGBD MySQL5
- نقل ملف conf/mysql5/persistence.xml إلى مجلد META-INF/persistence.xml إذا لزم الأمر
- تشغيل التطبيق [InitDB]
ونحصل على النتائج التالية:
![]() |
- في [1]: عرض وحدة التحكم في منظور Java. نحصل على النتيجة المتوقعة.
- في [2]: يتم التحقق من محتوى الجدول [jpa01_personne] باستخدام منظور SQL Explorer كما تم شرحه في الفقرة 2.1.8. يمكن ملاحظة نقطتين:
- تم إنشاء المفتاح الأساسي ID دون تدخل
- وينطبق الأمر نفسه على رقم الإصدار. نلاحظ أن الإصدار الأول يحمل الرقم 0..
لدينا هنا العناصر الأولى لثقافة JPA. لقد نجحنا في إدراج البيانات في جدول. سنبني على هذه الإنجازات لكتابة الاختبار الثاني، ولكن قبل ذلك لنتحدث عن السجلات.
2.1.11. تنفيذ سجلات Hibernate
من الممكن معرفة الأوامر SQL الصادرة على قاعدة البيانات بواسطة الطبقة JPA / Hibernate. من المثير للاهتمام معرفة ذلك لمعرفة ما إذا كانت طبقة JPA فعالة بقدر مطور كان سيكتب الأوامر SQL بنفسه.
مع JPA / Hibernate، يمكن التحقق من سجلات SQL في الملف [persistence.xml]:
<!-- الفئات الدائمة -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- السجلات SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- تسجيل الدخول JDBC -->
<property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
- الأسطر 4-6: لم تكن سجلات SQL نشطة في الوقت الحالي. يتم تفعيلها الآن عن طريق إزالة علامة التعليقات من الأسطر 3 و7.
نُعيد تشغيل التطبيق [InitDB]. تصبح عروض وحدة التحكم كما يلي:
- الأسطر 2-4: الأمر SQL delete الناتج عن التعليمات التالية:
// حذف عناصر من جدول الأشخاص
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
- السطور 5-18: الأوامر SQL insert الناتجة عن التعليمات:
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
- السطور 21-32: الأمر SQL select الصادر عن التعليمات:
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList())
إذا قمنا بعرض معلومات وسيطة على وحدة التحكم، فسنرى أن كتابة سجلات SQL لأمر I في كود Java تتم عند تنفيذ الأمر I. هذا لا يعني أن الأمر SQL المعروض يتم تنفيذه على قاعدة البيانات في ذلك الوقت. بل يتم تخزينه مؤقتًا للتنفيذ عند المزامنة التالية لسياق الاستمرارية مع قاعدة البيانات.
يمكن الحصول على سجلات أخرى عبر الملف [src/log4j.properties]:
![]() |
- في [1]، يتم استخدام الملف [log4j.properties] بواسطة الأرشيف [log4j-1.2.13.jar] [2] للأداة المسماة LOG4j (Logs for Java) المتاحة على الرابط [http://logging.apache.org/log4j/docs/index.html]. بعد وضعه في مجلد [src] الخاص بمشروع Eclipse، نعلم أن [log4j.properties] سيتم نسخه تلقائيًا إلى مجلد [bin] الخاص بمشروع [3]. وبعد ذلك، يصبح الملف موجودًا في مجلد classpath الخاص بالمشروع، ومن هناك سيقوم الأرشيف [2] باستخراجه.
يتيح لنا الملف [log4j.properties] التحكم في بعض سجلات Hibernate. خلال عمليات التشغيل السابقة، كان محتواه كما يلي:
# توجيه رسائل السجل إلى stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
# خيار مسجل السجلات الجذر
log4j.rootLogger=ERROR, stdout
# خيارات تسجيل Hibernate (INFO يعرض رسائل بدء التشغيل فقط)
#log4j.logger.org.hibernate=INFO
# تسجيل معلمات الربط في وقت التشغيل لـ JDBC
#log4j.logger.org.hibernate.type=DEBUG
لن أعلق كثيرًا على هذا التكوين لأنني لم أكن قد أخذت الوقت الكافي للتعرف بشكل جاد على LOG4j.
- توجد الأسطر 1-8 في جميع ملفات log4j.properties التي صادفتها
- السطور 10-14 موجودة في ملفات log4j.properties الخاصة بأمثلة Hibernate.
- السطر 11: يتحكم في سجلات Hibernate العامة. ونظرًا لأن السطر معلق، فإن هذه السجلات معطلة هنا. يمكن أن يكون هناك عدة مستويات من السجلات: INFO (معلومات عامة حول ما يفعله Hibernate)، WARN (يُعلمنا Hibernate بوجود مشكلة محتملة)، DEBUG (سجلات تفصيلية). المستوى INFO هو الأقل تفصيلاً، بينما الوضع DEBUG هو الأكثر تفصيلاً. تفعيل السطر 11 يسمح بمعرفة ما يفعله Hibernate، لا سيما عند بدء تشغيل التطبيق. وهذا غالباً ما يكون مفيداً.
- السطر 12، إذا كان نشطًا، يتيح معرفة الحجج المستخدمة فعليًا عند تنفيذ الاستعلامات SQL المحددة.
لنبدأ بإلغاء تعليق السطر 14
# تسجيل حجج وقت التشغيل لمعلمة الربط JDBC
log4j.logger.org.hibernate.type=DEBUG
ونعيد تنفيذ [InitDB]. السجلات الجديدة الناتجة عن هذا التعديل هي التالية (عرض جزئي):
- السطور 8-10 هي سجلات جديدة ناتجة عن تفعيل السطر 14 من [log4j.properties]. وهي تشير إلى القيم الخمس المخصصة للمعلمات الشكلية ? في الاستعلام المعلم في السطور 2-7. وبالتالي نرى أن العمود VERSION سيتلقى القيمة 0 (السطر 8).
الآن، لنقم بتنشيط السطر 11 من [log4j.properties]:
ونعيد تشغيل [InitDB]:
توفر قراءة هذه السجلات الكثير من المعلومات المثيرة للاهتمام:
- السطر 7: يشير Hibernate إلى اسم فئة @Entity التي عثر عليها
- السطر 8: يشير إلى أن الفئة [Personne] سيتم ربطها بالجدول [jpa01_personne]
- السطر 9: يشير إلى مجموعة اتصالات C3P0 التي سيتم استخدامها، واسم برنامج تشغيل Jdbc، وعنوان URL لقاعدة البيانات المراد إدارتها
- السطر 10: يقدم خصائص أخرى للربط Jdbc: المالك، نوع الالتزام، ...
- السطر 14: اللهجة المستخدمة للتواصل مع SGBD
- السطر 15: نوع المعاملة المستخدمة. يشير JDBCTransactionFactory إلى أن التطبيق يدير معاملاته بنفسه. ولا يتم تنفيذه في حاوية EJB3 التي توفر خدمة المعاملات الخاصة بها.
- السطور التالية تتعلق بخيارات تكوين Hibernate التي لم نواجهها. ندعو القارئ المهتم إلى قراءة وثائق Hibernate.
- السطر 37: سيتم عرض الأوامر SQL على وحدة التحكم. وقد تم طلب ذلك في [persistence.xml]:
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="use_sql_comments" value="true" />
- الأسطر 43-45: يتم تصدير مخطط قاعدة البيانات إلى ملفات SGBD و c.a.d. ويتم مسح قاعدة البيانات ثم إعادة إنشائها. تأتي هذه الآلية من التكوين الذي تم إجراؤه في [persistence.xml] (السطر 4 أدناه):
...
<property name="hibernate.connection.password" value="jpa" />
<!-- إنشاء المخطط تلقائيًا -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- اللهجة -->
...
عندما "يتعطل" أحد التطبيقات مع استثناء Hibernate لا يمكن فهمه، سنبدأ بتنشيط سجلات Hibernate في الوضع DEBUG في [log4j.properties] لفهم الأمر بشكل أوضح:
# خيار مسجل السجلات الجذري
log4j.rootLogger=ERROR, stdout
# خيارات تسجيل Hibernate (يعرض INFO رسائل بدء التشغيل فقط)
log4j.logger.org.hibernate=DEBUG
في بقية هذا المستند، يتم تعطيل السجلات افتراضيًا للحصول على عرض وحدة تحكم أكثر قابلية للقراءة.
2.1.12. اكتشاف لغة JPQL / HQL باستخدام وحدة التحكم Hibernate
ملاحظة: يتطلب هذا القسم المكون الإضافي Hibernate Tools (الفقرة 5.2.5).
في كود تطبيق [InitDB]، استخدمنا استعلام JPQL. JPQL (Java Persistence Query Language) هي لغة لاستعلام سياق الاستمرارية. كان الاستعلام التالي:
كان يختار جميع عناصر الجدول المرتبط بـ @Entity [Personne] ويعرضها بترتيب تصاعدي حسب الاسم. في الاستعلام أعلاه، p.nom هو حقل الاسم لمثيل p من فئة [Personne]. وبالتالي، يعمل الاستعلام JPQL على كائنات @Entity في سياق الاستمرارية وليس مباشرة على جداول قاعدة البيانات. ستقوم طبقة JPA بترجمة هذا الاستعلام JPQL إلى استعلام SQL مناسب لـ SGBD الذي تعمل معه. وهكذا، في حالة تنفيذ JPA / Hibernate المرتبط بـ SGBD MySQL5، يتم ترجمة الاستعلام JPQL السابق إلى الاستعلام SQL التالي:
select
personne0_.ID as ID0_,
personne0_.VERSION as VERSION0_,
personne0_.NOM as NOM0_,
personne0_.PRENOM as PRENOM0_,
personne0_.DATENAISSANCE as DATENAIS5_0_,
personne0_.MARIE as MARIE0_,
personne0_.NBENFANTS as NBENFANTS0_
from
jpa01_personne personne0_
order by
personne0_.NOM asc
استخدمت الطبقة JPA تكوين الكائن @Entity [Personne] لتوليد الأمر SQL الصحيح. هذا هو الجسر بين الكائن والعلاقة الذي تم تنفيذه هنا.
يوفر المكون الإضافي [Hibernate Tools] (الفقرة 5.2.5) أداة تسمى "Console Hibernate" تسمح
- إصدار أوامر JPQL أو مجموعة HQL (لغة استعلام Hibernate) في سياق الاستمرارية
- والحصول على النتائج
- معرفة المكافئ SQL الذي تم تنفيذه على قاعدة البيانات
تعد وحدة التحكم Hibernate أداة قيّمة لتعلم لغة JPQL والتعرّف على الجسر JPQL / SQL. من المعروف أن JPA مستوحى بشكل كبير من أدوات ORM مثل Hibernate أو Toplink. JPQL قريب جدًا من لغة HQL الخاصة بـ Hibernate ولكنه لا يتضمن جميع وظائفها. في وحدة التحكم Hibernate، يمكن إصدار أوامر HQL التي سيتم تنفيذها بشكل طبيعي في وحدة التحكم ولكنها ليست جزءًا من لغة JPQL وبالتالي لا يمكن استخدامها في عميل JPA. وعندما يكون الأمر كذلك، سنشير إلى ذلك.
لنقم بإنشاء وحدة تحكم Hibernate لمشروع Eclipse الحالي:
![]() |
- [1]: ننتقل إلى منظور [Hibernate Console] (Window / Open Perspective / Other)
- [2]: نقوم بإنشاء تكوين جديد في النافذة [Hibernate Configuration]
- باستخدام الزر [4]، نختار مشروع Java الذي يتم إنشاء تكوين Hibernate له. يظهر اسمه في [3].
- في [5]، نحدد الاسم الذي نريده لهذه التهيئة. هنا، استخدمنا [3].
- في [6]، نشير إلى أننا نستخدم تكوين JPA حتى يعرف الأداة أنه يجب عليها استخدام الملف [META-INF/persistence.xml]
- في [7]: نوضح أنه في ملف [META-INF/persistence.xml] هذا، يجب استخدام وحدة الاستمرارية التي تسمى jpa.
- في [8]، نقوم بالتحقق من صحة التكوين.
للمتابعة، يجب تشغيل SGBD. هنا، يتعلق الأمر بـ MySQL5.
![]() |
- في [1]: التكوين الذي تم إنشاؤه يعرض شجرة ذات ثلاثة فروع
- في [2]: الفرع [Configuration] يسرد الكائنات التي استخدمتها وحدة التحكم لتكوين نفسها: هنا @Entity Personne.
- في [3]: Session Factory هو مفهوم في Hibernate قريب من EntityManager في JPA. وهو يقوم بربط الكائنات بالعلاقات بفضل الكائنات الموجودة في الفرع [Configuration]. في [3]، يتم عرض كائنات سياق الاستمرارية، وهنا مرة أخرى @Entity Personne.
- في [4]: قاعدة البيانات التي يتم الوصول إليها عن طريق التكوين الموجود في [persistence.xml]. نجد فيها الجدول [jpa01_personne].
![]() |
- في [1]، يتم إنشاء محرر HQL
- في المحرر HQL،
- في [2]، نختار تكوين Hibernate المراد استخدامه إذا كان هناك أكثر من واحد
- في [3]، نكتب الأمر JPQL الذي نريد تنفيذه
- في [4]، يتم تنفيذه
- في [5]، نحصل على نتائج الاستعلام في النافذة [Hibernate Query Result]. قد نواجه صعوبتين هنا:
- لا نحصل على أي شيء (لا توجد أي سطور). استخدمت وحدة التحكم Hibernate محتوى [persistence.xml] لإنشاء اتصال مع SGBD. لكن هذا التكوين له خاصية تنص على إفراغ قاعدة البيانات:
<property name="hibernate.hbm2ddl.auto" value="create" />
لذلك يجب إعادة تشغيل التطبيق [InitDB] قبل إعادة تشغيل الأمر JPQL أعلاه.
- (تابع)
- لا توجد النافذة [Hibernate Query Result]. يمكن طلبها عبر [Window / Show View / ...]
تسمح النافذة [Hibernate Dynamic SQL preview] ([1] أدناه) بمشاهدة الاستعلام SQL الذي سيتم تشغيله لتنفيذ الأمر JPQL الذي نقوم بكتابته حالياً. بمجرد أن تصبح صيغة الأمر JPQL صحيحة، يظهر الأمر SQL المقابل في هذه النافذة:
![]() |
- في [2]، نقوم بمسح الأمر السابق HQL
- في [3]، يتم تنفيذ أمر جديد
- في [4]، النتيجة
- في [5]، الأمر SQL الذي تم تنفيذه على أساس
يوفر محرر HQL مساعدة في كتابة الأوامر HQL:
![]() |
- في [1]: بمجرد أن يعرف المحرر أن p هو كائن Personne، يمكنه أن يقترح علينا حقول p أثناء الكتابة.
- في [2]: أمر HQL غير صحيح. يجب كتابة where p.marie=true.
- في [3]: يتم الإبلاغ عن الخطأ في نافذة [SQL Preview]
ندعو القارئ إلى إصدار أوامر أخرى HQL / JPQL على القاعدة.
2.1.13. عميل ثانٍ JPA
لنعد إلى منظور Java للمشروع:
![]() |
- [InitDB.java] هو برنامج كان يضع بضعة أسطر في الجدول [jpa01_personne] في قاعدة البيانات. وقد سمح لنا دراسة كوده باكتساب العناصر الأولى من API JPA.
- [Main.java] هو برنامج يقوم بعمليات CRUD على الجدول [jpa01_personne]. ستسمح لنا دراسة كوده بالعودة إلى المفاهيم الأساسية لسياق الاستمرارية ودورة حياة الكائنات في هذا السياق.
2.1.13.1. هيكل الكود
سيقوم [Main.java] بتسلسل سلسلة من الاختبارات التي يهدف كل منها إلى إظهار جانب معين من JPA:
![]() |
تستدعي الطريقة [main]
- تستدعي بالتتابع الطرق من test1 إلى test11. سنقدم كود كل من هذه الطرق بشكل منفصل.
- كما تستخدم طرقًا مساعدة خاصة: clean ، dump، log، getEntityManager، getNewEntityManager.
نقدم الطريقة main والطرق المسماة المساعدة:
package tests;
...
import entites.Personne;
@SuppressWarnings("unchecked")
public class Main {
// الثوابت
private final static String TABLE_NAME = "jpa01_personne";
// سياق الاستمرارية
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// الكائنات المشتركة
private static Personne p1, p2, newp1;
public static void main(String[] args) throws Exception {
// تنظيف قاعدة البيانات
log("clean");clean();
// تفريغ الجدول
dump();
// اختبار1
log("test1");test1();
...
// اختبار 11
log("test11");test11();
// نهاية سياق الاستمرارية
if (em.isOpen())
em.close();
// إغلاق EntityManagerFactory
emf.close();
}
// استرداد EntityManager الحالي
private static EntityManager getEntityManager() {
if (em == null || !em.isOpen()) {
em = emf.createEntityManager();
}
return em;
}
// استرداد EntityManager جديد
private static EntityManager getNewEntityManager() {
if (em != null && em.isOpen()) {
em.close();
}
em = emf.createEntityManager();
return em;
}
// عرض محتوى الجدول
private static void dump() {
// سياق الاستمرارية الحالي
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// نهاية المعاملة
tx.commit();
}
// مسح BD
private static void clean() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف عناصر الجدول PERSONNES
em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
// نهاية المعاملة
tx.commit();
}
// السجلات
private static void log(String message) {
System.out.println("main : ----------- " + message);
}
// إنشاء كائنات
public static void test1() throws ParseException {
...
}
// تعديل كائن من السياق
public static void test2() {
...
}
// طلب كائنات
public static void test3() {
...
}
// حذف كائن ينتمي إلى سياق الاستمرارية
public static void test4() {
....
}
// فصل وإعادة ربط وتعديل
public static void test5() {
...
}
// حذف كائن لا ينتمي إلى سياق الاستمرارية
public static void test6() {
...
}
// تعديل كائن لا ينتمي إلى سياق الاستمرارية
public static void test7() {
...
}
// إعادة ربط كائن بسياق الاستمرارية
public static void test8() {
...
}
// تؤدي استعلامات select إلى مزامنة
// للقاعدة مع سياق الاستمرارية
public static void test9() {
....
}
// التحكم في الإصدار (قفل متفائل)
public static void test10() {
...
}
// تراجع معاملة
public static void test11() throws ParseException {
...
}
}
- السطر 13: الكائن EntityManagerFactory emf الذي تم إنشاؤه من وحدة الاستمرارية jpa المحددة في [persistence.xml]. سيسمح لنا هذا بإنشاء سياقات استمرارية متنوعة على مدار التطبيق.
- السطر 14: سياق استمرارية EntityManager em لم يتم تهيئته بعد
- السطر 17: ثلاثة كائنات [Personne] مشتركة بين الاختبارات
- السطر 21: يتم إفراغ الجدول jpa01_personne ثم عرضه في السطر 24 للتأكد من أننا نبدأ من جدول فارغ.
- الأسطر 27-31: سلسلة من الاختبارات
- السطران 34-35: إغلاق سياق الثبات em إذا كان مفتوحًا.
- السطر 38: إغلاق الكائن EntityManagerFactory emf.
- الأسطر 42-47: تجعل الطريقة [getEntityManager] كائن EntityManager (أو سياق الاستمرارية) الحالي أو جديدًا إذا لم يكن موجودًا (الأسطر 43-44).
- الأسطر 50-56: الطريقة [getNewEntityManager] تجعل سياق الاستمرارية جديدًا. إذا كان هناك سياق موجود مسبقًا، يتم إغلاقه (الأسطر 51-52)
- الأسطر 59-72: تعرض الطريقة [dump] محتوى الجدول [jpa01_personne]. وقد سبق أن ظهر هذا الرمز في [InitDB].
- السطور 75-85: تقوم الطريقة [clean] بإفراغ الجدول [jpa01_personne]. وقد سبق أن ظهر هذا الرمز في [InitDB].
- الأسطر 88-90: تعرض الطريقة [log] على وحدة التحكم الرسالة التي يتم تمريرها إليها كمعلمة حتى يتم ملاحظتها.
يمكننا الآن الانتقال إلى دراسة الاختبارات.
2.1.13.2. الاختبار 1
رمز الاختبار 1 هو التالي:
// إنشاء كائنات
public static void test1() throws ParseException {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// إنشاء أشخاص
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
// نهاية المعاملة
tx.commit();
// عرض الجدول
dump();
}
سبق أن صادفنا هذا الرمز في [InitDB]: فهو ينشئ شخصين ويضعهما في سياق الاستمرارية.
- السطر 4: نطلب سياق الاستمرارية الحالي
- السطران 6-7: يتم إنشاء الشخصين
- السطور 9-15: يتم وضع الشخصين في سياق الاستمرارية داخل معاملة.
- السطر 15: بسبب التزام المعاملة، تتم مزامنة سياق الاستمرارية مع قاعدة البيانات. سيتم إضافة الشخصين إلى الجدول [jpa01_personne].
- السطر 17: يتم عرض الجدول
عرض وحدة التحكم لهذا الاختبار الأول هو كما يلي:
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
2.1.13.3. الاختبار 2
رمز الاختبار 2 هو التالي:
// تعديل كائن من السياق
public static void test2() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// زيادة عدد أطفال p1
p1.setNbenfants(p1.getNbenfants() + 1);
// يتم تعديل حالته الاجتماعية
p1.setMarie(false);
// يتم حفظ الكائن p1 تلقائيًا (فحص التغييرات)
// عند المزامنة التالية (التثبيت أو الاستعلام)
// نهاية المعاملة
tx.commit();
// يتم عرض الجدول الجديد
dump();
}
- يهدف الاختبار 2 إلى تعديل كائن من سياق الاستمرارية ثم عرض محتوى الجدول لمعرفة ما إذا كان التعديل قد تم
- السطر 4: يتم استرداد سياق الاستمرارية الحالي
- السطران 6-7: ستتم العمليات في معاملة
- السطران 9 و11: يتم تغيير عدد أبناء الشخص p1 وكذلك حالته الاجتماعية
- السطر 15: نهاية المعاملة، وبالتالي مزامنة سياق الاستمرارية مع قاعدة البيانات
- السطر 17: عرض الجدول
عرض وحدة التحكم للاختبار 2 هو كما يلي:
- السطر 4: الشخص p1 قبل التعديل
- السطر 8: الشخص p1 بعد التعديل. تجدر الإشارة إلى أن رقم الإصدار الخاص به قد تغير إلى 1. ويتم زيادة هذا الرقم بمقدار 1 عند كل تحديث للسطر.
2.1.13.4. الاختبار 3
رمز الاختبار 3 هو كما يلي:
// طلب الكائنات
public static void test3() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// طلب الشخص p1
Personne p1b = em.find(Personne.class, p1.getId());
// نظرًا لأن p1 موجود بالفعل في سياق الاستمرارية، لم يتم الوصول إلى قاعدة البيانات
// p1b و p1 هما نفس المراجع
System.out.format("p1==p1b ? %s%n", p1 == p1b);
// طلب كائن غير موجود يجعل المؤشر 1 null
Personne px = em.find(Personne.class, -4);
System.out.format("px==null ? %s%n", px == null);
// نهاية المعاملة
tx.commit();
}
- يستهدف الاختبار 3 الطريقة [EntityManager.find] التي تسمح بالبحث عن كائن في قاعدة البيانات لوضعه في سياق الاستمرارية. لن نوضح بعد الآن المعاملة التي تحدث في جميع الاختبارات إلا عندما يتم استخدامها بطريقة غير معتادة.
- السطر 9: نطلب من سياق الاستمرارية الشخص الذي له نفس المفتاح الأساسي للشخص p1. هناك حالتان:
- p1 موجود بالفعل في سياق الاستمرارية. وهذا هو الحال هنا. وبالتالي لا يتم الوصول إلى قاعدة البيانات. تكتفي الطريقة find بإرجاع مرجع إلى الكائن المستمر.
- p1 ليس موجودًا في سياق الاستمرارية. عندئذ يتم الوصول إلى قاعدة البيانات، عبر المفتاح الأساسي الذي تم تقديمه. يتم وضع السطر المسترد في سياق الاستمرارية وتقوم find بإرجاع مرجع هذا الكائن المستمر الجديد.
- السطر 12: يتم التحقق من أن find قد أعاد مرجع الكائن p1 الموجود بالفعل في السياق
- السطر 14: يتم طلب كائن لا يوجد لا في سياق الاستمرارية ولا في قاعدة البيانات. ثم تعرض الطريقة find المؤشر null. يتم التحقق من هذه النقطة في السطر 15.
عرض وحدة التحكم لاختبار 3 هو كما يلي:
2.1.13.5. الاختبار 4
رمز الاختبار 4 هو كما يلي:
// حذف كائن ينتمي إلى سياق الاستمرارية
public static void test4() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم حذف الكائن الثابت p2
em.remove(p2);
// نهاية المعاملة
tx.commit();
// عرض الجدول الجديد
dump();
}
- يُعنى الاختبار 4 بالطريقة [EntityManager.remove] التي تسمح بحذف عنصر من سياق الاستمرارية وبالتالي من قاعدة البيانات.
- السطر 9: يتم إزالة الشخص p2 من سياق الاستمرارية
- السطر 11: مزامنة السياق مع قاعدة البيانات
- السطر 13: عرض الجدول. عادةً، لا ينبغي أن يكون الشخص p2 موجودًا بعد الآن.
عرض وحدة التحكم للاختبار 4 هو كما يلي:
- السطر 3: الشخص p2 في test1
- الأسطر 12-14: لم يعد موجودًا بعد انتهاء test4.
2.1.13.6. الاختبار 5
رمز الاختبار 5 هو كما يلي:
// فصل وإعادة ربط وتعديل
public static void test5() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// p1 مفصول
Personne oldp1=p1;
// إعادة ربط p1 بالسياق الجديد
p1 = em.find(Personne.class, p1.getId());
// التحقق
System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);
// نهاية المعاملة
tx.commit();
// زيادة عدد أبناء p1
p1.setNbenfants(p1.getNbenfants() + 1);
// عرض الجدول الجديد
dump();
}
- يُعنى الاختبار 5 بدورة حياة الكائنات المُخزّنة عبر عدة سياقات استمرارية متتالية. حتى الآن، كنا نستخدم دائمًا نفس سياق الاستمرارية عبر الاختبارات المختلفة.
- السطر 4: يتم طلب سياق استمرارية جديد. تغلق الطريقة [getNewEntityManager] السياق السابق وتفتح سياقًا جديدًا. ونتيجة لذلك، لم تعد الكائنات p1 و p2 التي تحتفظ بها التطبيق في حالة استمرارية. فقد كانت تنتمي إلى سياق تم إغلاقه. يُقال إنهما في حالة منفصلة. فهما لا ينتميان إلى سياق الاستمرارية الجديد.
- السطران 6-7: بداية المعاملة. سيتم استخدامها هنا بطريقة غير معتادة.
- السطر 9: يتم تسجيل عنوان الكائن p1 الذي أصبح منفصلاً الآن.
- السطر 11: نطلب من سياق الاستمرارية الشخص p1 (الذي يحمل المفتاح الأساسي لـ p1). وبما أن السياق جديد، فإن الشخص p1 غير موجود فيه. لذلك سيتم الوصول إلى قاعدة البيانات. وسيتم وضع الكائن الذي تم إرجاعه في السياق الجديد.
- السطر 13: يتم التحقق من أن الكائن الدائم p1 في السياق يختلف عن الكائن oldp1 الذي كان الكائن القديم p1 المنفصل.
- السطر 15: تنتهي المعاملة
- السطر 17: يتم تعديل الكائن الدائم الجديد p1 خارج المعاملة. ماذا يحدث في هذه الحالة؟ نريد معرفة ذلك.
- السطر 19: نطلب عرض الجدول. نذكر أنه بسبب select الصادر عن الطريقة dump، تتم مزامنة سياق الاستمرارية مع قاعدة البيانات تلقائيًا.
عرض وحدة التحكم لاختبار 5 هو كما يلي:
- السطر 5: لقد قامت الطريقة find بالفعل بالوصول إلى قاعدة البيانات، وإلا لكان المؤشران متساويين
- السطران 7 و 3: لقد زاد عدد أبناء p1 بمقدار 1. وبالتالي، تم أخذ التعديل، الذي تم إجراؤه خارج المعاملة، في الاعتبار. وهذا يعتمد في الواقع على SGBD المستخدم. في SGBD، يتم دائمًا تنفيذ أمر SQL ضمن معاملة. إذا لم يقم العميل JPA ببدء معاملة صريحة بنفسه، فسيقوم SGBD ببدء معاملة ضمنية. هناك حالتان شائعتان:
- 1 - يخضع كل أمر SQL فردي لمعاملة، تفتح قبل الأمر وتغلق بعده. يُقال إننا في وضع autocommit. لذلك، يبدو الأمر كما لو أن العميل JPA يقوم بمعاملات لكل أمر SQL.
- 2 - لا يعمل SGBD في وضع autocommit ويبدأ معاملة ضمنية عند الأمر الأول SQL الذي يصدره العميل JPA خارج معاملة ويترك للعميل إغلاقها. تصبح جميع الأوامر SQL التي يصدرها العميل JPA جزءًا من المعاملة الضمنية. ويمكن أن تنتهي هذه المعاملة بناءً على أحداث مختلفة: يقوم العميل بإغلاق الاتصال، أو يبدأ معاملة جديدة، ...
نحن في موقف يعتمد على تكوين SGBD. لذلك لدينا كود غير قابل للنقل. سنعرض لاحقًا كودًا بدون معاملات وسنرى أن جميع SGBD لا تتصرف بنفس الطريقة تجاه هذا الكود. لذلك سنعتبر أن العمل خارج المعاملات هو خطأ في البرمجة.
- السطر 7: لاحظ أن رقم الإصدار قد تغير إلى 2.
2.1.13.7. الاختبار 6
كود الاختبار 6 هو التالي:
// حذف كائن لا ينتمي إلى سياق الاستمرارية
public static void test6() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف p1 الذي لا ينتمي إلى السياق الجديد
try {
em.remove(p1);
// نهاية المعاملة
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
// إجراء تراجع للمعاملة
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// عرض الجدول الجديد
dump();
}
- يسعى الاختبار 6 إلى حذف كائن لا ينتمي إلى سياق الاستمرارية.
- السطر 4: يتم طلب سياق استمرارية جديد. وبالتالي يتم إغلاق السياق القديم وتصبح الكائنات التي كان يحتوي عليها منفصلة. وهذا هو الحال بالنسبة للكائن p1 في الاختبار 5 السابق.
- السطران 6-7: بداية المعاملة.
- السطر 10: يتم حذف الكائن المنفصل p1. ونحن نعلم أن هذا سيؤدي إلى حدوث استثناء، لذا قمنا بتطويق العملية بعبارة try/catch.
- السطر 12: لن يتم تنفيذ الالتزام.
- السطور 16-21: يجب أن تنتهي المعاملة بـ commit (يتم التحقق من صحة جميع عمليات المعاملة) أو بـ rollback (يتم إلغاء جميع عمليات المعاملة). حدثت استثناء، لذا نقوم بعملية rollback للمعاملة. لا يوجد ما يجب التراجع عنه لأن العملية الوحيدة في المعاملة قد فشلت، لكن rollback تنهي المعاملة. هذه هي المرة الأولى التي نستخدم فيها العملية [EntityTransaction].rollback. كان يجب أن نفعل ذلك منذ الأمثلة الأولى. لم نفعل ذلك من أجل الحفاظ على بساطة الكود. ومع ذلك، يجب على القارئ أن يتذكر أن حالة rollback للمعاملة يجب أن تكون متوقعة دائمًا في الكود.
- السطر 24: يتم عرض الجدول. عادةً، لا ينبغي أن يتغير.
عرض وحدة التحكم للاختبار 6 هو كما يلي:
- السطر 6: فشل حذف p1. توضح رسالة الاستثناء أننا أردنا حذف كائن منفصل، وبالتالي لا يشكل جزءًا من السياق. وهذا غير ممكن.
- السطر 8: الشخص p1 لا يزال موجودًا.
2.1.13.8. الاختبار 7
رمز الاختبار 7 هو كما يلي:
// تعديل كائن لا ينتمي إلى سياق الاستمرارية
public static void test7() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم زيادة عدد العناصر التابعة لـ p1 التي لا تنتمي إلى السياق الجديد
p1.setNbenfants(p1.getNbenfants() + 1);
// نهاية المعاملة
tx.commit();
// عرض الجدول الجديد - لا ينبغي أن يكون قد تغير
dump();
}
- يهدف الاختبار 7 إلى تعديل كائن لا ينتمي إلى سياق الاستمرارية ومراقبة تأثير ذلك على قاعدة البيانات. يمكننا أن نتصور أنه لا يوجد أي تأثير. وهذا ما تظهره نتائج الاختبار.
- السطر 4: يتم طلب سياق استمرارية جديد. لدينا إذن سياق جديد لا يحتوي على كائنات مستمرة.
- السطران 6-7: بداية المعاملة.
- السطر 9: يتم تعديل الكائن المنفصل p1. هذه عملية لا تتضمن سياق الاستمرارية em. لذلك لا يتوقع حدوث استثناء أو شيء من هذا القبيل. إنها عملية أساسية على POJO.
- السطر 11: يؤدي الالتزام إلى مزامنة السياق مع قاعدة البيانات. هذا السياق فارغ. وبالتالي، لا يتم تعديل قاعدة البيانات.
- السطر 24: يتم عرض الجدول. عادةً، لا ينبغي أن يتغير.
عرض وحدة التحكم لاختبار 7 هو كما يلي:
- السطر 7: لم يتغير الشخص p1 في قاعدة البيانات. بالنسبة للاختبار التالي، يجب أن نتذكر أن عدد أطفاله في الذاكرة أصبح الآن 5.
2.1.13.9. الاختبار 8
رمز الاختبار 8 هو كما يلي:
// إعادة ربط كائن بسياق الاستمرارية
public static void test8() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// إعادة ربط الكائن المنفصل p1 بالسياق الجديد
newp1 = em.merge(p1);
// أصبح newp1 هو الذي يشكل الآن جزءًا من السياق، وليس p1
// نهاية المعاملة
tx.commit();
// يتم عرض الجدول الجديد - لا بد أن عدد أبناء p1 قد تغير
dump();
}
- يعيد الاختبار 8 ربط كائن منفصل بسياق الاستمرارية.
- السطر 4: يتم طلب سياق استمرارية جديد. لدينا إذن سياق جديد لا يحتوي على كائنات مستمرة.
- السطران 6-7: بداية المعاملة.
- السطر 9: يتم إعادة ربط الكائن المنفصل p1 بسياق الاستمرارية. قد تتضمن عملية الدمج عدة عمليات:
- الحالة 1: يوجد في سياق الاستمرارية كائن مستمر ps1 له نفس المفتاح الأساسي للكائن المنفصل p1. يتم نسخ محتوى p1 إلى ps1، ويشير merge إلى ps1.
- الحالة 2: لا يوجد في سياق الاستمرارية كائن مستمر ps1 له نفس المفتاح الأساسي للكائن المنفصل p1. يتم عندئذٍ الاستعلام عن قاعدة البيانات لمعرفة ما إذا كان الكائن المطلوب موجودًا في قاعدة البيانات. إذا كان الأمر كذلك، يتم نقل هذا الكائن إلى سياق الاستمرارية، ويصبح الكائن الدائم ps1 ونعود إلى الحالة 1 السابقة.
- الحالة 3: لا يوجد، لا في سياق الاستمرارية ولا في قاعدة البيانات، كائن له نفس المفتاح الأساسي للكائن المنفصل p1. عندئذ يتم إنشاء كائن جديد [Personne] (new)، ثم وضعه في سياق الاستمرارية. ثم نعود إلى الحالة 1.
- في النهاية: يظل الكائن المنفصل p1 منفصلاً. تُرجع العملية merge مرجعًا (هنا newp1) إلى الكائن الدائم ps1 الناتج عن merge. يجب أن تعمل تطبيق العميل الآن مع الكائن الدائم ps1 وليس مع الكائن المنفصل p1.
- يجب ملاحظة وجود اختلاف بين الحالتين 1 و 3 فيما يتعلق بالطلب SQL المبرمج لـ merge: في الحالتين 1 و 2، يكون الأمر UPDATE بينما في الحالة 3، يكون الأمر INSERT.
- السطر 12: يؤدي الالتزام إلى مزامنة السياق مع قاعدة البيانات. لم يعد هذا السياق فارغًا. فهو يحتوي على الكائن newp1. سيتم الاحتفاظ بهذا الكائن في قاعدة البيانات.
- السطر 24: يتم عرض الجدول للتحقق منه.
عرض وحدة التحكم لاختبار 8 هو كما يلي:
- كان عدد أبناء p1 هو 4 في الاختبار 6 (السطر 4)، ثم ارتفع إلى 5 في الاختبار 7 ولكن لم يتم حفظه في قاعدة البيانات (السطر 7). بعد merge، تم حفظ newp1 في قاعدة البيانات: السطر 10، لدينا بالفعل 5 أطفال.
- السطر 10: تم تغيير رقم إصدار newp1 إلى 3.
2.1.13.10. الاختبار 9
رمز الاختبار 9 هو التالي:
// تؤدي استعلامات select إلى إجراء مزامنة
// للقاعدة مع سياق الاستمرارية
public static void test9() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم زيادة عدد أطفال newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// عرض الأشخاص - لا بد أن عدد أطفال newp1 قد تغير
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// نهاية المعاملة
tx.commit();
}
- يهدف الاختبار 9 إلى إظهار آلية مزامنة السياق التي تحدث تلقائيًا قبل select.
- السطر 5: لا يتم تغيير سياق الاستمرارية. وبالتالي، فإن newp1 موجود بداخله.
- السطران 7-8: بداية المعاملة.
- السطر 10: يتم زيادة عدد العناصر التابعة للكائن الدائم newp1 بمقدار 1 (5 -> 6).
- السطور 12-15: يتم عرض الجدول بواسطة أمر select. سيتم مزامنة السياق مع قاعدة البيانات قبل تنفيذ select.
- السطر 17: نهاية المعاملة
لرؤية المزامنة، يتم تشغيل عرض سجلات Hibernate في الوضع DEBUG (log4j.properties):
# خيار مسجل الجذر
log4j.rootLogger=ERROR, stdout
# خيارات تسجيل Hibernate (INFO يعرض رسائل بدء التشغيل فقط)
log4j.logger.org.hibernate=DEBUG
عرض وحدة التحكم للاختبار 9 هو كما يلي:
- السطر 1: يبدأ الاختبار 9
- الأسطر 2-6: تبدأ معاملة Jdbc. يتم تعطيل وضع autocommit لـ SGBD (السطر 5)
- السطر 7: عرض ناتج عن السطر 12 من كود Java. ستؤدي الأسطر التالية من كود Java إلى إصدار select وبالتالي إلى مزامنة سياق الاستمرارية مع قاعدة البيانات.
- السطر 8: الأمر JPQL الذي نريد إصداره قد تم إصداره بالفعل. يجده Hibernate في ذاكرة التخزين المؤقتة لـ "الاستعلامات المعدة مسبقًا".
- السطر 9: يعلن Hibernate أنه سيقوم بتفريغ سياق الاستمرارية
- السطران 11-12: يكتشف Hibernate (Hb) أن الكيان Personne#1 (بالمفتاح الأساسي 1) قد تم تغييره (dirty).
- السطران 12-13: يعلن Hb أنه يقوم بتحديث هذا العنصر ويغير رقم إصداره من 3 إلى 4.
- السطر 15: ستؤدي مزامنة السياق إلى 0 عملية إدراج، و1 عملية تحديث (update)، و0 عملية حذف (delete)
- السطور 17-34: مزامنة السياق (flush). ملاحظة: زيادة رقم الإصدار (السطر 19)، الأمر SQL update المُعد (السطر 21)، قيم معلمات الأمر update (السطور 24-31).
- السطر 35: يبدأ الأمر select
- السطر 38: الأمر SQL الذي سيتم تنفيذه
- السطر 40: لا يعرض select سوى سطر واحد
- السطر 42: يكتشف Hb أن كيان Personne#1 الذي استردته عملية select من قاعدة البيانات موجود بالفعل في سياق الاستمرارية الخاص به. وبالتالي، لا يقوم بنسخ السطر المسترد من قاعدة البيانات إلى السياق، وهي العملية التي يطلق عليها اسم "الترطيب".
- السطر 43: يتحقق مما إذا كانت الكائنات التي أعادها select تحتوي على تبعيات (مفاتيح خارجية بشكل عام) يجب تحميلها أيضًا (مجموعات غير كسولة). لا توجد تبعيات هنا.
- السطر 44: عرض ناتج عن كود Java
- السطر 45: نهاية معاملة Jdbc المطلوبة بواسطة كود Java
- السطر 46: تبدأ المزامنة التلقائية للسياق التي تحدث أثناء commit.
- السطر 48: يكتشف Hb أن السياق لم يتغير منذ المزامنة السابقة.
- السطر 50: نهاية commit.
مرة أخرى، تثبت سجلات Hibernate في وضع DEBUG فائدتها الكبيرة في معرفة ما يفعله Hibernate بالضبط.
2.1.13.11. الاختبار 10
رمز الاختبار 10 هو التالي:
// التحكم في الإصدار (قفل متفائل)
public static void test10() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// زيادة إصدار newp1 مباشرة في قاعدة البيانات (استعلام أصلي)
em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
// نهاية المعاملة
tx.commit();
// بداية معاملة جديدة
tx = em.getTransaction();
tx.begin();
// يتم زيادة عدد أطفال newp1
newp1.setNbenfants(newp1.getNbenfants() + 1);
// نهاية المعاملة - يجب أن تفشل لأن newp1 لم يعد لديه الإصدار الصحيح
try {
tx.commit();
} catch (RuntimeException e1) {
System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
// يتم التراجع عن المعاملة
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
}
}
// إغلاق السياق الذي لم يعد محدثًا
em.close();
// تفريغ الجدول - لا بد أن إصدار p1 قد تغير
dump();
}
- يهدف الاختبار 10 إلى إظهار الآلية التي يوفرها الحقل version في @Entity Personne، المزود بالسمة JPA @Version. لقد أوضحنا أن هذه التعليقة التوضيحية تؤدي إلى زيادة قيمة العمود المرتبط بالتعليقة التوضيحية @Version في قاعدة البيانات عند كل عملية update تتم على السطر الذي تنتمي إليه. هذه الآلية، التي تُعرف أيضًا باسم القفل المتفائل (optimistic locking)، تفرض أن يكون لدى العميل الذي يرغب في تعديل كائن O في قاعدة البيانات أحدث إصدار منه. إذا لم يكن لديه ذلك، فهذا يعني أن الكائن قد تم تعديله منذ حصوله عليه ويجب إخطاره بذلك.
- السطر 4: لا يتم تغيير سياق الاستمرارية. وبالتالي، فإن newp1 موجود فيه.
- السطران 6-7: بداية معاملة.
- السطر 9: يتم زيادة إصدار الكائن newp1 بمقدار 1 (4 -> 5) مباشرة في قاعدة البيانات. تتجاوز الاستعلامات من النوع nativeQuery سياق الاستمرارية وتدخل مباشرة إلى قاعدة البيانات. والنتيجة هي أن الكائن المستمر newp1 وصورته في قاعدة البيانات لم يعودا يحملان نفس الإصدار.
- السطر 10: نهاية المعاملة الأولى
- السطران 13-14: بداية معاملة ثانية
- السطر 16: يزداد عدد العناصر التابعة للكائن الدائم newp1 بمقدار 1 (6 -> 7).
- السطر 19: نهاية المعاملة. وبالتالي، تتم عملية مزامنة. ستؤدي هذه العملية إلى تحديث عدد العناصر التابعة لـ newp1 في قاعدة البيانات. وستفشل هذه العملية لأن الكائن الدائم newp1 له الإصدار 4، في حين أن الكائن المراد تحديثه في قاعدة البيانات له الإصدار 5. سيتم إطلاق استثناء، وهو ما يبرر وجود try / catch في الكود.
- السطر 21: يتم عرض الاستثناء وسبب حدوثه.
- السطر 25: التراجع عن المعاملة
- السطر 33: عرض الجدول: يجب أن نرى أن إصدار newp1 هو 5 في قاعدة البيانات.
عرض وحدة التحكم لاختبار 10 هو كما يلي:
- السطر 5: يطلق الالتزام استثناءً بالفعل. وهو من النوع [javax.persistence.RollbackException]. الرسالة المرتبطة بها غامضة. إذا اهتممنا بالسبب وراء هذا الاستثناء (Exception.getCause)، نرى أن لدينا استثناء Hibernate بسبب محاولة تعديل سطر في قاعدة البيانات دون وجود الإصدار الصحيح.
- السطر 7: نرى أن إصدار newp1 في قاعدة البيانات قد تم تغييره بالفعل إلى 5 بواسطة nativeQuery.
2.1.13.12. الاختبار 11
رمز الاختبار 11 هو التالي:
// تراجع معاملة
public static void test11() throws ParseException {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = null;
try {
tx = em.getTransaction();
tx.begin();
// إعادة ربط p1 بالسياق من خلال البحث عنه في قاعدة البيانات
p1 = em.find(Personne.class, p1.getId());
// زيادة عدد أطفال p1
p1.setNbenfants(p1.getNbenfants() + 1);
// عرض الأشخاص - لا بد أن عدد أبناء p1 قد تغير
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// إنشاء شخصين يحملان نفس الاسم، وهو أمر محظور بموجب DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// استمرارية الأشخاص
em.persist(p3);
em.persist(p4);
// نهاية المعاملة
tx.commit();
} catch (RuntimeException e1) {
// حدثت مشكلة
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// التخلي عن السياق الحالي
em.clear();
}
// تفريغ - لا ينبغي أن تتغير الجدولة بسبب التراجع
dump();
}
- يُعنى الاختبار 11 بآلية rollback الخاصة بإحدى المعاملات. تعمل المعاملة على أساس "كل شيء أو لا شيء": إما أن يتم تنفيذ جميع العمليات SQL التي تحتوي عليها بنجاح (commit)، أو يتم إلغاؤها جميعًا في حالة فشل إحداها (rollback).
- السطر 4: نستمر بنفس سياق الاستمرارية. ربما يتذكر القارئ أن السياق قد أُغلق بعد تعطل الاختبار السابق. في هذه الحالة، يقدم [getEntityManager] سياقًا جديدًا تمامًا، وبالتالي فارغًا.
- الأسطر 7-27: محاولة / استثناء واحد للتحكم في المشاكل التي سنواجهها
- السطور 8-9: بداية معاملة ستحتوي على عدة عمليات SQL
- السطر 11: يتم البحث عن p1 في قاعدة البيانات ووضعه في السياق
- السطر 13: زيادة عدد أبناء p1 (6 -> 7)
- الأسطر 15-18: يتم عرض محتوى قاعدة البيانات، مما سيؤدي إلى مزامنة السياق. في قاعدة البيانات، سيتغير عدد أبناء p1 إلى 7، وهو ما يجب أن تؤكده شاشة العرض.
- السطران 20-21: إنشاء شخصين باسمين متطابقين هما p3 و p4. ولكن حقل الاسم في @Entity Personne له السمة unique=true، مما أدى إلى فرض قيد التفرد على العمود NOM في الجدول [jpa01_personne].
- السطران 23-24: يتم وضع الأشخاص p3 و p4 في سياق الاستمرارية.
- السطر 26: يتم تثبيت المعاملة. يتبع ذلك تزامن ثانٍ للسياق، بعد أن تم التزامن الأول عند select. ستصدر JPA أمرين SQL و insert للأشخاص p3 و p4. سيتم إدراج p3. بالنسبة لـ p4، سيطلق SGBD استثناءً، لأن p4 يحمل نفس اسم p3. وبالتالي، لا يتم إدراج p4 ويقوم برنامج تشغيل Jdbc بإرسال استثناء إلى العميل.
- السطر 27: يتم التعامل مع الاستثناء
- الأسطر 29-31: يتم عرض الاستثناء وسببيه السابقين في سلسلة الاستثناءات التي أوصلتنا إلى هذه المرحلة.
- السطر 34: يتم التراجع عن المعاملة النشطة حاليًا. وقد بدأت هذه المعاملة في السطر 9 من كود Java. منذ ذلك الحين، تم تنفيذ عملية update لتعديل عدد أطفال p1 ثم عملية insert للشخص p3. سيتم إلغاء كل ذلك بواسطة التراجع.
- السطر 39: يتم إفراغ سياق الاستمرارية
- السطر 42: يتم عرض الجدول [jpa01_personne]. يجب التحقق من أن p1 لا يزال لديه 6 أطفال وأن لا p3 ولا p4 موجودان في الجدول.
عرض وحدة التحكم للاختبار 11 هو كما يلي:
main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
- السطر 3: ارتفع عدد العناصر التابعة لـ p1 من 6 إلى 7 في قاعدة البيانات، وارتفعت إصدارة p1 إلى 6.
- السطر 4: الاستثناء الذي تم استرداده عند إجراء عملية التثبيت (commit) للمعاملة. إذا قرأنا جيدًا، نرى أن السبب هو مفتاح مكرر X (الاسم). إن إدراج p4 هو ما تسبب في هذا الخطأ، في حين أن p3 الذي تم إدراجه بالفعل يحمل أيضًا الاسم X.
- السطر 7: الجدول بعد التراجع. استعاد p1 إصداره 5 وعدد أبنائه 6، ولم يتم إدراج p3 و p4.
2.1.13.13. الاختبار 12
رمز الاختبار 12 هو التالي:
// نكرر نفس العملية ولكن بدون المعاملات
// نحصل على نفس النتيجة السابقة مع SGBD : FIREBIRD، ORACLE XE، POSTGRES، MYSQL5
// مع SQLSERVER نحصل على جدول فارغ. يتم ترك الاتصال في حالة تمنع إعادة تنفيذ
// للبرامج. لذا يجب إعادة تشغيل الخادم.
// نفس الشيء مع SGBD Derby
// HSQL يُدرج الشخص الأول - لا يوجد تراجع
public static void test12() throws ParseException {
// نعيد ربط p1
p1 = em.find(Personne.class, p1.getId());
// يتم زيادة عدد أطفال p1
p1.setNbenfants(p1.getNbenfants() + 1);
// عرض الأشخاص - لا بد أن عدد أبناء p1 قد تغير
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// إنشاء شخصين يحملان نفس الاسم، وهو أمر محظور بموجب DDL
Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
// استمرارية الأشخاص
em.persist(p3);
em.persist(p4);
// تفريغ سيؤدي إلى مزامنة سياق em مع BD
try {
dump();
} catch (RuntimeException e3) {
System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
.getCause().getMessage());
}
// يتم إغلاق السياق الحالي
em.close();
// تفريغ
dump();
}
- الاختبار 12 يعيد نفس ما فعله الاختبار 11 ولكن خارج المعاملة. نريد أن نرى ما يحدث في هذه الحالة.
- الأسطر 1-6: تعطي نتائج الاختبارات باستخدام مختلف SGBD:
- مع عدد معين من SGBD (Firebird، Oracle، MySQL5، Postgres) نحصل على نفس النتيجة التي حصلنا عليها في الاختبار 11. مما يدفعنا إلى الاعتقاد بأن هذه SGBD قد بدأت بنفسها معاملة تغطي جميع أوامر SQL المستلمة حتى تلك التي تسببت في الخطأ، وأنها بدأت بنفسها rollback.
- مع SGBD أخرى (SQL Server، Apache Derby) يحدث تعطل للتطبيق و/أو لـ SGBD.
- مع SGBD و HSQLDB، يبدو أن المعاملة التي فتحها SGBD تعمل في وضع autocommit: يتم تثبيت تعديل عدد العناصر التابعة لـ p1 وإدراج p3. فقط إدراج p4 هو الذي يفشل.
وبالتالي، نحصل على نتيجة تعتمد على SGBD، مما يجعل التطبيق غير قابل للنقل. تجدر الإشارة إلى أن العمليات على سياق الاستمرارية يجب أن تتم دائمًا ضمن معاملة.
2.1.14. تغيير SGBD
لنعد إلى بنية الاختبار لمشروعنا الحالي:
![]() |
لا ترى تطبيق العميل [3] سوى واجهة JPA [5]. ولا ترى لا التنفيذ الفعلي لها، ولا الهدف SGBD. لذلك يجب أن نتمكن من تغيير هذين العنصرين من السلسلة دون إجراء تغييرات في العميل [3]. وهذا ما نحاول رؤيته الآن بالبدء بتغيير SGBD. لقد استخدمنا حتى الآن MySQL5. نقدم ستة أخرى موصوفة في الملاحق (الفقرة 5) على أمل أن يكون من بينها SGBD المفضل لدى القارئ.
في جميع الأحوال، فإن التعديل المطلوب إجراؤه في مشروع Eclipse بسيط (انظر أدناه): استبدال ملف persistence.xml [1] الخاص بتكوين الطبقة JPA بأحد الملفات الموجودة في المجلد conf [2] الخاص بالمشروع. برامج التشغيل JDBC و SGBD موجودة بالفعل في المكتبة [jpa-divers] و [3] و [4].
![]() |
2.1.14.1. Oracle 10g Express
يتم عرض Oracle 10g Express في الملاحق في الفقرة 5.7. ملف persistence.xml الخاص بـ Oracle هو كما يلي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<!-- فئات ثابتة -->
<property name="hibernate.archive.autodetection" value="class, hbm" />
<!-- السجلات SQL
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="use_sql_comments" value="true"/>
-->
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
<property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
<!-- إنشاء المخطط تلقائيًا -->
<property name="hibernate.hbm2ddl.auto" value="create" />
<!-- اللهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
<!-- خصائص DataSource c3p0 -->
<property name="hibernate.c3p0.min_size" value="5" />
<property name="hibernate.c3p0.max_size" value="20" />
<property name="hibernate.c3p0.timeout" value="300" />
<property name="hibernate.c3p0.max_statements" value="50" />
<property name="hibernate.c3p0.idle_test_period" value="3000" />
</properties>
</persistence-unit>
</persistence>
هذا التكوين مطابق للتكوين الذي تم إجراؤه لـ SGBD MySQL5، باستثناء التفاصيل التالية:
- الأسطر 15-18 التي تكوّن الارتباط JDBC مع قاعدة البيانات
- السطر 22: الذي يحدد اللهجة SQL المراد استخدامها
بالنسبة للأمثلة القادمة، سنكتفي بتحديد الأسطر التي تتغير. للحصول على شرح للتكوين، يرجى الرجوع إلى الملحق المخصص لـ SGBD المستخدم. ويرد فيه مثال على استخدام الوصلة JDBC في كل مرة، في سياق المكون الإضافي [SQL Explorer]. باستخدام المعلومات الواردة في الملحق، سيتمكن القارئ من تكرار عملية التحقق من نتيجة تطبيق [InitDB] التي تمت في الفقرة 2.1.10.2.
نقوم كما هو موضح في الفقرة المذكورة أعلاه:
- تشغيل SGBD Oracle
- وضع conf/oracle/persistence.xml في META-INF/persistence.xml
- تشغيل التطبيق [InitDB]
نحصل على النتائج التالية على وحدة التحكم:
![]() |
بعد ذلك، لن نعرض هذه اللقطة التي تظل كما هي. والأمر الأكثر إثارة للاهتمام هو منظور SQL لاستكشاف الارتباط بين JDBC و SGBD. سنتبع الخطوات الموضحة في الفقرة 2.1.8.
![]() |
- في [1]: الاتصال بـ Oracle
- إلى [2]: شجرة الاتصال بعد تنفيذ [InitDB]
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD.
2.1.14.2. PostgreSQL 8.2
يتم عرض PostgreSQL 8.2 في المرفقات في الفقرة 5.6. ملفه persistence.xml هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
<property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- اللهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
</persistence-unit>
</persistence>
لتشغيل [InitDB]:
- قم بتشغيل SGBD PostgreSQL
- ضع conf/postgres/persistence.xml في META-INF/persistence.xml
- تشغيل التطبيق [InitDB]
منظور SQL لاستكشاف الارتباط بين JDBC و SGBD هو كما يلي:
![]() |
- في [1]: الارتباط مع PostgreSQL
- في [2]: شجرة الارتباط بعد تنفيذ [InitDB]
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD
2.1.14.3. SQL Server Express 2005
يتم عرض SQL Server Express 2005 في الملاحق في الفقرة 5.8، الصفحة 270. ملفه persistence.xml هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
<property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- لهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
</persistence-unit>
</persistence>
لتشغيل [InitDB]:
- قم بتشغيل SGBD SQL Server
- ضع conf/sqlserver/persistence.xml في META-INF/persistence.xml
- تشغيل التطبيق [InitDB]
منظور SQL Explorer للربط بين JDBC و SGBD هو كما يلي:
![]() |
- في [1]: الاتصال بـ SQL Server
- في [2]: شجرة الاتصال بعد تنفيذ [InitDB]
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD
2.1.14.4. Firebird 2.0
يتم عرض Firebird 2.0 في الملاحق في الفقرة 5.4. ملفه persistence.xml هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
<property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
<property name="hibernate.connection.username" value="sysdba" />
<property name="hibernate.connection.password" value="masterkey" />
...
<!-- لهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
</persistence-unit>
</persistence>
لتشغيل [InitDB]:
- قم بتشغيل SGBD Firebird
- ضع conf/firebird/persistence.xml في META-INF/persistence.xml
- قم بتشغيل التطبيق [InitDB]
إن منظور SQL لربط JDBC بـ SGBD هو كما يلي:
![]() |
- في [1]: الاتصال بـ Firebird
- في [2]: شجرة الاتصال بعد تنفيذ [InitDB]
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD.
2.1.14.5. Apache Derby
يتم عرض Apache Derby في الملاحق في الفقرة 5.10. ملفه persistence.xml هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
<property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
<property name="hibernate.connection.username" value="jpa" />
<property name="hibernate.connection.password" value="jpa" />
...
<!-- لهجة -->
...
</persistence-unit>
</persistence>
لتشغيل [InitDB]:
- قم بتشغيل SGBD Apache Derby
- ضع conf/derby/persistence.xml في META-INF/persistence.xml
- قم بتشغيل التطبيق [InitDB]
منظور SQL Explorer للربط بين JDBC و SGBD هو كما يلي:
![]() |
- في [1]: الاتصال بـ Apache Derby
- في [2]: شجرة الاتصال بعد تنفيذ [InitDB]. يُلاحظ الجدول [HIBERNATE_UNIQUE_KEY] الذي تم إنشاؤه بواسطة JPA / Hibernate لتوليد القيم المتتالية للمفتاح الأساسي ID تلقائيًا. لقد أشرنا سابقًا إلى أن هذه الآلية غالبًا ما تكون خاصة. ويمكننا أن نرى ذلك بوضوح هنا. بفضل JPA، لا يتعين على المطور الخوض في تفاصيل SGBD.
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD.
2.1.14.6. يُعرض HSQLDB
يتم عرض HSQLDB في المرفقات في الفقرة 5.9. ملفه persistence.xml هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
<!-- اتصال JDBC -->
<property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
<property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
<property name="hibernate.connection.username" value="sa" />
<!--
<property name="hibernate.connection.password" value="" />
-->
...
<!-- لهجة -->
<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
</properties>
</persistence-unit>
</persistence>
لتشغيل [InitDB]:
- قم بتشغيل SGBD HSQL
- ضع conf/hsql/persistence.xml في META-INF/persistence.xml
- تشغيل التطبيق [InitDB]
منظور SQL Explorer للربط بين JDBC و SGBD هو كما يلي:
![]() |
- في [1]: الارتباط مع HSQL
- في [2]: شجرة الارتباط بعد تنفيذ [InitDB].
- في [3]: بنية الجدول [jpa01_personne]
- في [4]: محتواها.
بعد ذلك، يُطلب من القارئ تشغيل التطبيق [Main] ثم إيقاف SGBD.
2.1.15. تغيير التنفيذ JPA
لنعد إلى بنية الاختبار لمشروعنا الحالي:
![]() |
أظهرت الدراسة السابقة أننا تمكنا من تغيير SGBD [7] دون تغيير أي شيء في كود العميل [3]. نقوم الآن بتغيير التنفيذ JPA [6] ونظهر مرة أخرى أن ذلك يتم بشكل شفاف بالنسبة لرمز العميل [3]. نأخذ تطبيق TopLink [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html]:
![]() |
2.1.15.1. مشروع Eclipse
بمناسبة تغيير التنفيذ JPA، نقوم بإنشاء مشروع Eclipse جديد حتى لا نلوث المشروع الحالي. في الواقع، يستخدم المشروع الجديد مكتبات استمرارية قد تتعارض مع مكتبات Hibernate:
![]() |
- في [1]: يحتوي المجلد [<exemples>/toplink/direct/personnes-entites] على مشروع Eclipse. قم باستيراد هذا المشروع.
- إلى [2]: المشروع [toplink-personnes-entites] المستورد. وهو مطابق (تم الحصول عليه عن طريق النسخ) للمشروع [hibernate-personne-entites] باستثناء تفصيلين:
- الملف [META-INF/persistence.xml] [3] يقوم الآن بتكوين طبقة JPA / Toplink
- تم استبدال المكتبة [jpa-hibernate] بالمكتبة [jpa-toplink] [4] و [5] (انظر الفقرة 1.5).
- في [6]: يحتوي المجلد [conf] على نسخة من الملف [persistence.xml] لكل SGBD.
- في [7]: المجلد [ddl] الذي سيحتوي على البرامج النصية SQL لإنشاء مخطط قاعدة البيانات.
2.1.15.2. تكوين الطبقة JPA / Toplink
نعلم أن الطبقة JPA يتم تكوينها بواسطة الملف [META-INF/persistence.xml]. وهذا الملف يقوم الآن بتكوين تطبيق JPA / Toplink. ومحتواه لطبقة JPA المتصلة بـ SGBD MySQL5 هو كما يلي؛
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- فئات ثابتة -->
<class>entites.Personne</class>
<!-- خصائص وحدة الاستمرارية -->
<properties>
<!-- اتصال JDBC -->
<property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
<property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="MySQL4" />
<!-- خادم التطبيق -->
<property name="toplink.target-server" value="None" />
<!-- إنشاء المخطط -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- السجلات -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
- السطر 3: لم يتغير
- السطر 5: المزود هو الآن Toplink. سيتم العثور على الفئة المذكورة هنا في المكتبة [jpa-toplink] ([1] أدناه):
![]() |
- السطر 7: تُستخدم علامة <class> لتسمية جميع فئات @Entity في المشروع، وهنا فقط فئة Personne. كان لدى Hibernate خيار تكوين يتيح لنا تجنب تسمية هذه الفئات. فقد كان يقوم بالبحث في ملف classpath الخاص بالمشروع للعثور على فئات @Entity.
- السطر 9: العلامة <properties> التي تقدم خصائص خاصة بالتنفيذ JPA المستخدم، وهنا Toplink.
- الأسطر 11-14: تكوين ارتباط Jdbc مع SGBD MySQL5
- الأسطر 15-18: تكوين مجموعة اتصالات Jdbc التي يديرها Toplink أصلاً:
- السطور 15 و16: الحد الأقصى والحد الأدنى لعدد الاتصالات في مجموعة اتصالات القراءة. القيمة الافتراضية (2،2)
- السطران 17 و18: الحد الأقصى والحد الأدنى لعدد الاتصالات في مجموعة اتصالات الكتابة. القيمة الافتراضية (10,2)
- السطر 20: الهدف SGBD. قائمة SGBD القابلة للاستخدام متوفرة في الحزمة [oracle.toplink.essentials.platform.database] (انظر [2] أعلاه). لا يوجد SGBD MySQL5 في قائمة [2]، لذلك تم اختيار MySQL4. يدعم Toplink عددًا أقل قليلاً من SGBD مقارنةً بـ Hibernate. وبالتالي، من بين السبعة SGBD المستخدمة في أمثلةنا، لا يتم دعم Firebird. كما لا يوجد Oracle في القائمة. إنه في الواقع موجود في حزمة أخرى ([3] أعلاه). إذا كان الهدف SGBD في هاتين الحزمتين محددًا بواسطة الفئة <Sgbd>Platform.class، فسيتم كتابة العلامة على النحو التالي:
<property name="toplink.target-database" value="<Sgbd>" />
- السطر 22: يحدد خادم التطبيق إذا كان التطبيق يعمل على خادم من هذا النوع. القيم المحتملة الحالية (None، OC4J_10_1_3، SunAS9). القيمة الافتراضية (None).
- الأسطر 24-28: عند تهيئة الطبقة JPA، يُطلب منها تنظيف قاعدة البيانات المحددة بواسطة ارتباط Jdbc في الأسطر 11-14. وبذلك نبدأ من قاعدة فارغة.
- السطر 24: يُطلب من Toplink إجراء drop متبوعًا بـ create لجداول مخطط قاعدة البيانات
- السطر 25: سنطلب من Toplink إنشاء البرامج النصية SQL للعمليات drop و create. يحدد application-location المجلد الذي سيتم إنشاء هذه البرامج النصية فيه. الافتراضي: (المجلد الحالي).
- السطر 26: اسم البرنامج النصي SQL للعمليات create.. الافتراضي: createDDL.jdbc.
- السطر 27: اسم البرنامج النصي SQL لعمليات drop.. الافتراضي: dropDDL.jdbc.
- السطر 28: وضع إنشاء المخطط (الافتراضي: both):
- both: البرامج النصية وقاعدة البيانات
- database: قاعدة البيانات فقط
- sql-script: البرامج النصية فقط
- السطر 30: يتم تعطيل (OFF) سجلات Toplink. مستويات تسجيل الدخول المختلفة المتاحة هي التالية: OFF، SEVERE، WARNING، INFO، CONFIG، FINE، FINER، FINEST. الافتراضي: INFO.
يمكن الرجوع إلى الرابط [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html] للحصول على تعريف شامل لعلامات <property> التي يمكن استخدامها مع Toplink.
2.1.15.3. اختبار [InitDB]
لا يوجد شيء آخر للقيام به. نحن جاهزون لتنفيذ الاختبار الأول [InitDB]:
- تشغيل SGBD، هنا MySQL5
- تنفيذ [InitDB]
![]() |
- في [1]: عرض وحدة التحكم. نجد النتائج التي تم الحصول عليها بالفعل باستخدام JPA / Hibernate.
- في [3]: نفتح المنظور [SQL Explorer] ثم نفتح الاتصال [mysql5-jpa]
- في [4]: شجرة قاعدة البيانات jpa. نكتشف أن تنفيذ [InitDB] قد أنشأ جدولين: [jpa01_personne] الذي كان متوقعًا والجدول [sequence] الذي لم يكن متوقعًا.
![]() |
- في [5]: بنية الجدول [jpa01_personne] وفي [6] محتواه
- في [7]: بنية الجدول [sequence] وفي [8] محتواه.
كان ملف التكوين [persistence.xml] يطلب إنشاء نصوص برمجية لـ DDL:
<!-- إنشاء مخطط -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/mysql5" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
لنلقِ نظرة على ما تم إنشاؤه في المجلد [ddl/mysql5]:
![]() |
create.sql
CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- السطر 1: DDL من الجدول [jpa01_personne]. نلاحظ أن Toplink لم تستخدم السمة autoincrement للمفتاح الأساسي ID. مما يعني أنه لا يوجد زيادة تلقائية لهذا المفتاح عند إدراج الأسطر.
- السطر 2: DDL من الجدول [sequence]. يبدو أن اسمها يشير إلى أن Toplink تستخدم هذا الجدول لتوليد قيم المفتاح الأساسي ID.
- السطر 3: إدراج سطر واحد في [SEQUENCE]
drop.sql
DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
- السطر 1: حذف الجدول [jpa01_personne]
- السطر 2: حذف سطر معين من الجدول [SEQUENCE]. لا يتم حذف الجدول نفسه ولا أي أسطر أخرى قد يحتوي عليها.
لمعرفة المزيد عن دور الجدول [SEQUENCE]، يتم تفعيل سجلات Toplink في [persistence.xml]، على مستوى FINE، وهو مستوى يتتبع الأوامر SQL الصادرة عن Toplink:
<!-- السجلات -->
<property name="toplink.logging.level" value="FINE" />
يتم إعادة تشغيل InitDB. أدناه، لم يتم الاحتفاظ سوى بعرض جزئي لشاشة وحدة التحكم:
...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/jpa
User: jpa@localhost
Database: MySQL Version: 5.0.37-community-nt
Driver: MySQL-AB JDBC Driver Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--اتصال(30708295)--موضوع(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL، DATENAISSANCE DATE NOT NULL، NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL، SEQ_COUNT DECIMAL(38)، PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Thread(Thread[main,5,main])--استثناء [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) القيم ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--حذف من jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--اتصال(14069849)--مؤشر ترابط(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ؟ WHERE SEQ_NAME = ?
bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ؟
bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID، PRENOM، DATENAISSANCE، NOM، MARIE، VERSION، NBENFANTS) VALUES (؟، ؟، ؟، ؟، ؟، ؟، ؟)
bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID، PRENOM، DATENAISSANCE، NOM، MARIE، VERSION، NBENFANTS) VALUES (؟، ؟، ؟، ؟، ؟، ؟، ؟)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM، MARIE، VERSION، NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--اتصال(30708295)--موضوع(Thread[main,5,main])--قطع الاتصال
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--خيط (Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa تسجيل الخروج بنجاح
...
terminé ...
- الأسطر 2-5: اتصال بـ SGBD مع معلماته. في الواقع، تُظهر السجلات أن Toplink ينشئ 3 اتصالات بـ SGBD. يجب التحقق مما إذا كان هذا العدد مرتبطًا بأحد قيم التكوين المستخدمة لمجموعة اتصالات Jdbc:
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
- السطر 7: حذف الجدول [jpa01_personne]. وهذا أمر طبيعي، لأن الملف [persistence.xml] يطلب تنظيف قاعدة بيانات jpa.
- السطر 8: إنشاء الجدول [jpa01_personne]. نلاحظ أن المفتاح الأساسي ID لا يحتوي على السمة autoincrement.
- السطر 9: إنشاء الجدول [SEQUENCE] الموجود بالفعل، والذي تم إنشاؤه أثناء التنفيذ السابق.
- الأسطر 10-13: يبلغ Toplink عن خطأ في إنشاء الجدول [SEQUENCE].
- السطر 15-18: يقوم Toplink بتنظيف الجدول [SEQUENCE]. بعد هذا التنظيف، تحتوي الجدول [SEQUENCE] على سطر واحد (SEQ_NAME، SEQ_COUNT) بالقيم ('SEQ_GEN'، 1).
- السطر 18: تم إفراغ الجدول [jpa01_personne].
- السطران 19-20: يقوم Toplink بتمرير السطر الوحيد الذي يحتوي على SEQ_NAME='SEQ_GEN' من الجدول [SEQUENCE]، من القيمة ('SEQ_GEN'، 1) إلى القيمة ('SEQ_GEN', 51)
- السطر 21: يسترد Toplink القيمة 51 من السطر ('SEQ_GEN', 51) من الجدول [SEQUENCE].
- السطور 24-27: يقوم Toplink بإدراج الشخصين 'Martin' و 'Durant' في الجدول [jpa01_personne]. هناك لغز هنا: تتلقى المفاتيح الأساسية لهاتين السطرين القيمتين 2 و 3 دون أن نعرف كيف تم الحصول على هاتين القيمتين. لا نعرف ما إذا كانت القيمة SEQ_COUNT (51) التي تم الحصول عليها في السطر 21 قد استُخدمت في شيء ما. تجدر الإشارة إلى أن قيمة إصدار الأسطر هي 1، في حين أن Hibernate كان يبدأ من 0.
- السطر 28: يقوم Toplink بإنشاء SELECT للحصول على جميع السطور من الجدول [jpa01_personne]
- السطران 29-30: السطور المعروضة بواسطة عميل Java
- السطران 31-32: يقوم Toplink بإغلاق اتصال. وسيكرر العملية لكل اتصال من الاتصالات المفتوحة في البداية.
في النهاية، لا نعرف بالضبط دور الجدول [SEQUENCE]، ولكن يبدو أنه يلعب دورًا في إنشاء قيم المفتاح الأساسي ID. وبالنظر إلى مستوى السجلات الأكثر تفصيلاً، FINEST، نتعرف أكثر قليلاً على دور الجدول [SEQUENCE].
<!-- السجلات -->
<property name="toplink.logging.level" value="FINEST" />
لم نحتفظ أدناه سوى بالسجلات المتعلقة بإدراج الشخصين في الجدول. وهنا نرى آلية توليد قيم المفتاح الأساسي:
- السطر 4: نرى أن الرقم 51 المستخرج من الجدول [SEQUENCE] في السطر 2 يُستخدم لتحديد نطاق قيم للمفتاح الأساسي: [2,51]
- السطر 5: يحصل الشخص الأول على القيمة 2 كمفتاح أساسي
- السطر 8: يحصل الشخص الثاني على القيمة 3 كمفتاح أساسي
- السطر 12: يوضح إدارة الإصدار للشخص الأول
- السطر 17: نفس الشيء بالنسبة للشخص الثاني
يُظهر مستوى السجلات [FINEST] أيضًا حدود المعاملات الصادرة عن Toplink. تُظهر دراسة هذه السجلات ما يفعله Toplink، وهي وسيلة رائعة لفهم الجسر بين الكائنات والعلاقات.
نستخلص من ما سبق:
- أن تطبيقات JPA المختلفة ستولد مخططات قواعد بيانات مختلفة. في هذا المثال، لم يولد كل من Hibernate و Toplink نفس المخططات.
- أن مستويات السجلات FINE و FINER و FINEST الخاصة بـ Toplink يجب استخدامها عندما نرغب في الحصول على توضيحات حول ما يفعله Toplink بالضبط.
2.1.15.4. اختبار [Main]
نقوم الآن بتنفيذ الاختبار [Main]:
![]() |
- في [1]: اجتازت جميع الاختبارات باستثناء الاختبار 11 [2]
- في [3]: السطر 376، سطر الكود الذي حدثت فيه الاستثناء
الرمز الذي ينتج الاستثناء هو التالي:
} catch (RuntimeException e1) {
// واجهنا مشكلة
System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
.getMessage());
try {
...
- السطر [3]: سطر الاستثناء. لدينا NullPointerException، مما يشير إلى أن إحدى الطرق getCause في السطرين 4 و5 قد أعادت مؤشرًا null. يفترض تعبير مثل [e1.getCause().getCause()] أن سلسلة الاستثناءات تحتوي على 3 عناصر [e1.getCause().getCause(), e1.getCause(), e1]. إذا كانت تحتوي على عنصرين فقط، فسيؤدي التعبير الأول إلى حدوث استثناء.
نقوم بتعديل الكود السابق بحيث يعرض فقط الاستثناءين الأخيرين من سلسلة الاستثناءات:
} catch (RuntimeException e1) {
// واجهنا مشكلة
System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
e1.getCause().getClass().getName(), e1.getCause().getMessage());
try {
...
عند التنفيذ، نحصل على النتيجة التالية:
...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read.
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
هذه المرة، اجتاز الاختبار 11. تم طلب عرض الاستثناءات (الأسطر 6-10) بواسطة كود Java (السطر 3 من الكود أعلاه). نذكر أن الاختبار 11 كان يربط، في نفس المعاملة، عدة عمليات SQL، إحداها فشلت وكان من المفترض أن تؤدي إلى التراجع عن المعاملة. حالات الجدول [jpa01_personne] قبل (السطر 3) وبعد الاختبار (السطر 12) متطابقة تمامًا، مما يدل على أن التراجع قد حدث.
تجدر الإشارة هنا إلى نقطة مهمة: إن تطبيقات JPA / Hibernate و JPA / Toplink ليست قابلة للتبادل بنسبة 100%. في هذا المثال، يتعين علينا تغيير كود العميل JPA لتجنب NullPointerException. سنواجه هذه المشكلة لاحقًا ومرة أخرى في سياق استثناء.
2.1.16. تغيير SGBD في التنفيذ JPA / Toplink
لنعد إلى بنية الاختبار لمشروعنا الحالي:
![]() |
في السابق، كان الملف SGBD المستخدم في [7] هو MySQL5. نوضح هنا مع Oracle كيفية التغيير إلى SGBD. في جميع الأحوال، التعديل المطلوب إجراؤه في مشروع Eclipse بسيط (انظر أدناه): استبدال ملف persistence.xml [1] الخاص بتكوين طبقة JPA بأحد الملفات الموجودة في المجلد conf ( [2] و [3]) في المشروع.
![]() |
2.1.16.1. يتم عرض Oracle 10g Express
يتم عرض Oracle 10g Express في الملاحق في الفقرة 5.7. ملف persistence.xml من Oracle لـ Toplink هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- فئات ثابتة -->
<class>entites.Personne</class>
<!-- خصائص وحدة الاستمرارية -->
<properties>
<!-- اتصال JDBC -->
<property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
<property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
<property name="toplink.jdbc.user" value="jpa" />
<property name="toplink.jdbc.password" value="jpa" />
<property name="toplink.jdbc.read-connections.max" value="3" />
<property name="toplink.jdbc.read-connections.min" value="1" />
<property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
<!-- SGBD -->
<property name="toplink.target-database" value="Oracle" />
<!-- خادم التطبيق -->
<property name="toplink.target-server" value="None" />
<!-- إنشاء المخطط -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
<property name="toplink.application-location" value="ddl/oracle" />
<property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
<property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />
<!-- السجلات -->
<property name="toplink.logging.level" value="OFF" />
</properties>
</persistence-unit>
</persistence>
هذا التكوين مطابق للتكوين الذي تم إجراؤه لـ SGBD MySQL5، باستثناء التفاصيل التالية:
- الأسطر 11-14 التي تكوّن الارتباط JDBC مع قاعدة البيانات
- السطر 20: الذي يحدد الهدف SGBD
- السطر 25: الذي يحدد مجلد إنشاء البرامج النصية SQL لـ DDL
لتنفيذ الاختبار [InitDB]:
- قم بتشغيل SGBD Oracle
- ضع conf/oracle/persistence.xml في META-INF/persistence.xml
- قم بتشغيل التطبيق [InitDB]
نحصل على النتائج التالية على وحدة التحكم وفي منظور [SQL Explorer]:
![]() |
- [1]: عرض وحدة التحكم
- [2]: الاتصال [oracle-jpa] في مستكشف SQL
- [3]: قاعدة البيانات jpa
- [4]: أنشأ InitDB جدولين: JPA01_PERSONNE و SEQUENCE، كما هو الحال مع MySQL5. في بعض الأحيان، تظهر جداول [BIN*] في [4]. وهي تتوافق مع الجداول التي تم حذفها. لملاحظة هذه الظاهرة، يكفي إعادة تشغيل [InitDB]. تتضمن مرحلة تهيئة الطبقة JPA عملية تنظيف لقاعدة البيانات jpa يتم خلالها إزالة الجدول [JPA01_PERSONNE]:
![]() |
في [A]، تظهر جدول [BIN]. لا تحذف Oracle نهائيًا جدولًا خضع لعملية drop، بل تضعه في سلة المهملات [Recycle Bin]. يمكن رؤية سلة المهملات هذه [B] باستخدام أداة SQL Developer الموضحة في الفقرة 5.7.4. في [B]، يمكن مسح الجدول [JPA01_PERSONNE] الموجود في سلة المهملات. يؤدي ذلك إلى إفراغ سلة المهملات [C]. إذا تم تحديث الجداول (النقر بزر الماوس الأيمن / Refresh) في SQL Explorer، نلاحظ أن الجدول BIN لم يعد موجودًا [D].
- [5, 6]: بنية ومحتوى الجدول [JPA01_PERSONNE]
- [7, 8]: بنية ومحتوى الجدول [SEQUENCE]
ها هو! يُطلب من القارئ الآن تشغيل التطبيق [Main] على Oracle.
2.1.16.2. القوائم الأخرى SGBD
لن نعرض سوى القليل عن SGBD الأخرى. ما عليك سوى تكرار الإجراء المتبع مع Oracle. تجدر الإشارة إلى النقاط التالية:
- بغض النظر عن SGBD، يستخدم Toplink دائمًا نفس التقنية لتوليد قيم المفتاح الأساسي ID للجدول [JPA01_PERSONNE]: فهو يستخدم الجدول [SEQUENCE] الموضح أعلاه.
- لا يتعرف Toplink على SGBD Firebird. توجد قاعدة بيانات عامة لهذه الحالات:
مع قاعدة البيانات العامة هذه المسماة [Auto]، تفشل الاختبارات مع Firebird بسبب أخطاء في صيغة SQL. يستخدم Toplink للمفتاح الأساسي ID، نوعًا SQL Number(10) لا يتعرف عليه Firebird. لذلك يجب اختيار SGBD الذي له نفس أنواع SQL مثل Firebird (في هذا المثال). هذا هو الحال مع Apache Derby:
<!-- تسجيل الدخول JDBC -->
<property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
<!-- SGBD -->
<!--
TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
-->
<property name="toplink.target-database" value="Derby" />
...
- لا يستطيع Toplink إنشاء المخطط الأصلي للقاعدة لـ SGBD HSQLDB. أي أن التوجيه:
<!-- إنشاء مخطط -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />
تفشل في حالة HSQLDB. والسبب في ذلك هو خطأ في بناء الجملة عند إنشاء الجدول [jpa01_personne]:
[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--اتصال(29775659)--موضوع(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--اتصال(29775659)--موضوع(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL، DATENAISSANCE DATE NOT NULL، NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Thread(Thread[main,5,main])--استثناء [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]
السطر 4، بناء الجملة NOM VARCHAR(30) UNIQUE NOT NULL غير مقبولة من قبل HSQL. استخدمت Hibernate الصيغة: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).
بشكل عام، كان Hibernate أكثر كفاءة من Toplink في التعرف على SGBD التي أجريت عليها اختبارات هذا المستند.
2.1.17. الخلاصة
تتوقف دراسة @Entity [Personne] عند هذا الحد. من الناحية النظرية، لم يتم إنجاز الكثير: فقد درسنا الجسر بين الكائنات والعلاقات في أبسط الحالات: كائن @Entity <--> جدول. ومع ذلك، فقد سمح لنا دراسته بتقديم الأدوات التي سنستخدمها في هذا المستند بأكمله. وهذا سيسمح لنا بالتقدم بشكل أسرع قليلاً من الآن فصاعداً في دراسة الحالات الأخرى للجسر بين الكائنات والعلاقات التي سنقوم بدراستها:
- إلى الكائن @Entity [Personne] السابق، سنضيف حقل adresse تم نمذجته بواسطة فئة [Adresse]. على جانب قاعدة البيانات، سنرى تطبيقين ممكنين. الكائنان [Personne] و [Adresse] ينتجان
- جدولًا واحدًا [personne] يتضمن العنوان
- جدولين [personne] و [adresse] مرتبطين بعلاقة مفتاح أجنبي من النوع واحد إلى واحد.
- مثال على علاقة واحد إلى عدة حيث ترتبط الجدولة [article] بالجدولة [categorie] بواسطة مفتاح خارجي
- مثال على علاقة "عدة إلى عدة" حيث ترتبط الجدولان [personne] و [activite] بواسطة جدول ربط [personne_activite].
2.2. مثال 2: علاقة واحد إلى واحد عبر تضمين
2.2.1. مخطط قاعدة البيانات
1 ![]() | 2 |
- في [1]: قاعدة البيانات (المكون الإضافي Azurri Clay)
- في [2]: DDL التي تم إنشاؤها بواسطة Hibernate لـ MySQL5
الجدول [jpa02_personne] هو الجدول [jpa01_personne] الذي تمت دراسته سابقًا والذي تمت إضافة عنوان إليه (الأسطر 12-18 من DDL).
2.2.2. الكائنات @Entity التي تمثل قاعدة البيانات
سيتم تمثيل عنوان الشخص بالفئة [Adresse] التالية:
package entites;
...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {
// الحقول
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
// المنشئات
public Adresse() {
}
public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
}
// المُستردات والمُعيّنات
...
// toString
public String toString() {
return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
}
}
- يكمن الابتكار الرئيسي في التعليق التوضيحي @Embeddable في السطر 5. الفئة [Adresse] غير مخصصة لإنشاء جدول، لذا فهي لا تحتوي على التعليق التوضيحي @Entity. تشير العلامة @Embeddable إلى أن الفئة مخصصة للاندماج في كائن @Entity وبالتالي في الجدول المرتبط به. ولهذا السبب، في مخطط قاعدة البيانات، لا تظهر الفئة [Adresse] كجدول منفصل، بل كجزء من الجدول المرتبط بـ @Entity [Personne].
لم تتغير @Entity [Personne] كثيرًا عن الإصدار السابق: فقد تمت إضافة حقل adresse إليها فقط:
package entites;
...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@Embedded
private Adresse adresse;
// المنشئات
public Personne() {
}
...
}
- يتم التعديل في السطور 33-34. أصبح للكائن [Personne] الآن حقل adresse من النوع Adresse. هذا بالنسبة لـ POJO. التعليق التوضيحي @Embedded مخصص لجسر الكائن/العلاقة. وهو يشير إلى أن الحقل [Adresse adresse] يجب أن يتم تغليفه في نفس الجدول الذي يحتوي على الكائن [Personne].
2.2.3. بيئة الاختبارات
سنقوم بإجراء اختبارات مشابهة جدًا لتلك التي درسناها سابقًا. وستُجرى في السياق التالي:
![]() |
التنفيذ المستخدم هو JPA / Hibernate [6]. مشروع Eclipse للاختبارات هو التالي:
![]() |
لا يختلف مشروع Eclipse [1] عن السابق إلا في أكواد Java الخاصة به [2]. البيئة (المكتبات – persistence.xml – قاعدة البيانات – ملفات التكوين، DDL – نصوص Ant) هي نفسها التي تمت دراستها سابقًا، لا سيما في الفقرة 2.1.5. وسيظل هذا هو الحال بالنسبة لمشاريع Hibernate القادمة، ولن نعود إلى هذه البيئة مرة أخرى، باستثناء حالات استثنائية. وعلى وجه الخصوص، فإن الملفات persistence.xml التي تهيئ طبقة JPA/Hibernate لمختلف SGBD هي تلك التي تمت دراستها بالفعل وتوجد في المجلد <conf>.
إذا كان لدى القارئ أي شك حول الإجراءات التي يجب اتباعها، فيُرجى الرجوع إلى تلك التي تم اتباعها في الدراسة السابقة.
يوجد مشروع Eclipse [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.2.4. إنشاء ملف DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لـ SGBD MySQL5 هو التالي:
drop table if exists jpa02_hb_personne;
create table jpa02_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
لقد أدرك Hibernate بشكل صحيح أن عنوان الشخص يجب أن يتم دمجه في الجدول المرتبط بـ @Entity Personne (الأسطر 11-17).
2.2.5. InitDB
رمز [InitDB] هو كما يلي:
package tests;
...
public class InitDB {
// الثوابت
private final static String TABLE_NAME = "jpa02_hb_personne";
public static void main(String[] args) throws ParseException {
// سياق الاستمرارية
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// يتم استرداد EntityManager من EntityManagerFactory السابق
em = emf.createEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// طلب
Query sql1;
// حذف العناصر من الجدول PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
sql1.executeUpdate();
// إنشاء أشخاص
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// إنشاء عناوين
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// الربط بين الشخص <--> العنوان
p1.setAdresse(a1);
p2.setAdresse(a2);
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// نهاية المعاملة
tx.commit();
// نهاية EntityManager
em.close();
// نهاية EntityManagerFactory
emf.close();
// سجل
System.out.println("terminé...");
}
}
لا يوجد شيء جديد في هذا الرمز. كل شيء سبق أن تمت معالجته. يؤدي تنفيذ [InitDB] مع MySQL5 إلى النتائج التالية:
![]() |
![]() |
- [1]: عرض وحدة التحكم
- [2]: الجدول [jpa02_hb_personne] في منظور SQL Explorer
- [3] و [4]: هيكلها ومحتواها.
2.2.6. الرئيسية
الفئة [Main] هي كما يلي:
package tests;
...
import entites.Adresse;
import entites.Personne;
@SuppressWarnings( { "unused", "unchecked" })
public class Main {
// ثوابت
private final static String TABLE_NAME = "jpa02_hb_personne";
// سياق الاستمرارية
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = null;
// الكائنات المشتركة
private static Personne p1, p2, newp1;
private static Adresse a1, a2, a3, a4, newa1, newa4;
public static void main(String[] args) throws Exception {
// يتم استرداد EntityManager من EntityManagerFactory
em = emf.createEntityManager();
// تنظيف قاعدة البيانات
log("clean");clean();
// تفريغ الجدول
dumpPersonne();
// اختبار1
log("test1"); test1();
// اختبار 2
log("test2"); test2();
// اختبار 3
log("test3"); test3();
// اختبار 4
log("test4"); test4();
// اختبار 5
log("test5");test5();
// نهاية سياق الاستمرارية
if (em != null && em.isOpen())
em.close();
// إغلاق EntityManagerFactory
emf.close();
}
// استرداد EntityManager الحالي
private static EntityManager getEntityManager() {
...
}
// استرداد EntityManager جديد
private static EntityManager getNewEntityManager() {
...
}
// عرض محتوى جدول "شخص"
private static void dumpPersonne() {
...
}
// مسح BD
private static void clean() {
...
}
// السجلات
private static void log(String message) {
...
}
// إنشاء كائنات
public static void test1() throws ParseException {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// إنشاء الأشخاص
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// إنشاء عناوين
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
// الارتباطات بين الأشخاص والعناوين
p1.setAdresse(a1);
p2.setAdresse(a2);
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
// نهاية المعاملة
tx.commit();
// تفريغ
dumpPersonne();
}
// تعديل كائن من السياق
public static void test2() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// زيادة عدد أطفال p1
p1.setNbenfants(p1.getNbenfants() + 1);
// يتم تعديل حالته الاجتماعية
p1.setMarie(false);
// يتم حفظ الكائن p1 تلقائيًا (فحص التغييرات)
// أثناء المزامنة التالية (commit أو select)
// نهاية المعاملة
tx.commit();
// يتم عرض الجدول الجديد
dumpPersonne();
}
// حذف كائن ينتمي إلى سياق الاستمرارية
public static void test4() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم حذف الكائن المرفق p2
em.remove(p2);
// نهاية المعاملة
tx.commit();
// عرض الجدول الجديد
dumpPersonne();
}
// فصل وإعادة ربط وتعديل
public static void test5() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// إعادة ربط p1 بالسياق الجديد
p1 = em.find(Personne.class, p1.getId());
// نهاية المعاملة
tx.commit();
// تغيير عنوان p1
p1.getAdresse().setVille("Paris");
// عرض الجدول الجديد
dumpPersonne();
}
}
مرة أخرى، لا شيء لم نره من قبل. عرض وحدة التحكم هو كما يلي:
يُطلب من القارئ ربط النتائج بالرمز.
2.2.7. تنفيذ JPA / Toplink
نستخدم الآن تطبيق JPA / Toplink:
![]() |
المشروع الجديد لـ Eclipse للاختبارات هو التالي:
![]() |
أكواد Java مطابقة لتلك الموجودة في مشروع Hibernate السابق. البيئة (المكتبات – persistence.xml – sgbd - ملفات conf، ddl – نصوص ant) هي نفسها التي تمت دراستها في الفقرة 2.1.15.2. وسيظل الحال كذلك بالنسبة لمشاريع Toplink القادمة، وباستثناء بعض الحالات، لن نعود إلى هذه البيئة مرة أخرى. وعلى وجه الخصوص، فإن الملفات persistence.xml التي تهيئ طبقة JPA/Toplink لمختلف SGBD هي تلك التي تمت دراستها بالفعل وتوجد في المجلد <conf>.
إذا كان لدى القارئ أي شك حول الإجراءات التي يجب اتباعها، فيُرجى الرجوع إلى تلك التي تم اتباعها في الدراسة السابقة.
يوجد مشروع Eclipse [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
يؤدي تنفيذ [InitDB] مع SGBD و MySQL5 إلى النتائج التالية:
![]() |
![]() |
- [1]: عرض وحدة التحكم
- [2]: الجداول [jpa02_tl_personne] و [SEQENCE] في منظور SQL Explorer
- [3] و [4]: هيكل ومحتوى [jpa02_tl_personne].
البرامج النصية SQL التي تم إنشاؤها في ddl/mysql5 [5] هي التالية:
create.sql
CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.3. مثال 3: علاقة واحد إلى واحد عبر مفتاح خارجي
2.3.1. مخطط قاعدة البيانات
1 ![]() | 2 |
- في [1]: قاعدة البيانات. هذه المرة، يتم وضع عنوان الشخص في جدول [adresse] خاص به. يرتبط الجدول [personne] بهذا الجدول بواسطة مفتاح خارجي.
- في [2]: الجدول DDL الذي تم إنشاؤه بواسطة Hibernate لـ MySQL5:
- السطور 9-20: الجدول [adresse] الذي سيتم ربطه بالفئة [Adresse] التي أصبحت كائنًا @Entity.
- السطر 10: المفتاح الأساسي للجدول [adresse]
- السطر 30: بدلاً من العنوان الكامل، نجد الآن في الجدول [personne]، المعرف [adresse_id] لهذا العنوان.
- الأسطر 34-38: الشخص (adresse_id) هو مفتاح خارجي على العنوان (id).
2.3.2. الكائنات @Entity التي تمثل قاعدة البيانات
يتم الآن تمثيل شخص له عنوان بالفئة [Personne] التالية:
package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
...
}
- الأسطر 32-34: عنوان الشخص
- السطر 32: تشير التعليقة التوضيحية @OneToOne إلى علاقة واحد إلى واحد: لكل شخص عنوان واحد على الأقل. السمة cascade = CascadeType.ALL تعني أن أي عملية (persist، merge، remove) على @Entity [Personne] يجب أن تتسلسل إلى @Entity [Adresse]. من وجهة نظر سياق الاستمرارية em، يعني هذا ما يلي. إذا كان p شخصًا ولديه عنوانه:
- فإن عملية em.persist(p) الصريحة ستؤدي إلى عملية em.persist(a) ضمنية
- ستؤدي عملية em.merge(p) الصريحة إلى عملية em.merge(a) ضمنية
- ستؤدي العملية الصريحة em.remove(p) إلى عملية ضمنية em.remove(a)
- السطر 32: تشير التعليقة التوضيحية @OneToOne إلى علاقة واحد إلى واحد: لكل شخص عنوان واحد على الأقل. السمة cascade = CascadeType.ALL تعني أن أي عملية (persist، merge، remove) على @Entity [Personne] يجب أن تتسلسل إلى @Entity [Adresse]. من وجهة نظر سياق الاستمرارية em، يعني هذا ما يلي. إذا كان p شخصًا ولديه عنوانه:
تُظهر التجربة أن هذه التسلسلات الضمنية ليست حلاً سحرياً. فالمطور ينتهي به الأمر إلى نسيان وظيفتها. وقد يُفضل استخدام عمليات صريحة في الكود. هناك أنواع مختلفة من التسلسلات. كان من الممكن كتابة التعليق التوضيحي @OneToOne على النحو التالي:
//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)
يقبل السمة cascade هنا قيمة عبارة عن مصفوفة من الثوابت تحدد أنواع التسلسلات المطلوبة.
يطلب السمة fetch=FetchType.LAZY من Hibernate تحميل التبعية في اللحظة الأخيرة. عندما نضع قائمة بالأشخاص في سياق الاستمرارية، لا نريد بالضرورة إدراج عناوينهم فيها. على سبيل المثال، قد لا نرغب في هذه العناوين إلا لشخص معين يختاره المستخدم عبر واجهة ويب. أما السمة fetch=FetchType.EAGER، فهي تطلب التحميل الفوري للتبعيات.
- (تابع)
- السطر 33: التعليق التوضيحي @JoinColumn يحدد المفتاح الأجنبي الذي تمتلكه جدول @Entity [Personne] في جدول @Entity [Adresse]. يحدد السمة name اسم العمود الذي يستخدم كمفتاح خارجي. تفرض السمة unique=true العلاقة واحد إلى واحد: لا يمكن أن يكون هناك نفس القيمة مرتين في العمود [adresse_id]. تفرض السمة nullable=false أن يكون لكل شخص عنوان.
يتم الآن تمثيل عنوان الشخص بواسطة @Entity [Adresse] التالية:
package entites;
...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {
// الحقول
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
// المنشئات
public Adresse() {
}
...
}
- السطر 4: تصبح الفئة [Adresse] كائنًا @Entity. وبالتالي، ستصبح موضوعًا لجدول في قاعدة البيانات.
- الأسطر 9-12: مثل أي كائن @Entity، فإن [Adresse] له مفتاح أساسي. وقد تم تسميته Id ويحتوي على نفس التعليقات التوضيحية (القياسية) للمفتاح الأساسي Id الخاص بـ @Entity [Personne].
- السطور 39-40: العلاقة واحد إلى واحد مع @Entity [Personne]. هناك عدة تفاصيل دقيقة هنا:
- أولاً، الحقل personne ليس إلزامياً. وهو يسمح لنا، انطلاقاً من عنوان ما، بالرجوع إلى الشخص الوحيد الذي يمتلك هذا العنوان. لو لم نكن نرغب في هذه الميزة، لما كان الحقل personne موجوداً، وكان كل شيء سيعمل على أي حال.
- تم بالفعل تكوين العلاقة الثنائية التي تربط بين الكيانين [Personne] و [Adresse] في الكيان @Entity [Personne]:
@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
ولكي لا تتعارض التكوينات الثنائية مع بعضهما البعض، يُعتبر أحدهما principale والآخر inverse. هذه هي العلاقة المسماة principale التي تديرها الجسر بين الكائنات والعلاقات. أما العلاقة الأخرى المسماة inverse، فلا تدار بشكل مباشر: بل تدار بشكل غير مباشر من خلال العلاقة principale. في @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
هو السمة mappedBy التي تشكل العلاقة واحد إلى واحد أعلاه، والعلاقة inverse من العلاقة principale واحد إلى واحد المحددة بواسطة الحقل adresse في @Entity [Personne].
2.3.3. مشروع Eclipse / Hibernate 1
التنفيذ JPA المستخدم هنا هو تنفيذ Hibernate. مشروع Eclipse للاختبارات هو التالي:
![]() |
المشروع موجود [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.3.4. إنشاء DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لـ SGBD MySQL5 هو الملف الموضح في بداية هذه الفقرة.
2.3.5. InitDB
رمز [InitDB] هو التالي:
package tests;
...
import entites.Adresse;
import entites.Personne;
public class InitDB {
// الثوابت
private final static String TABLE_PERSONNE = "jpa03_hb_personne";
private final static String TABLE_ADRESSE = "jpa03_hb_adresse";
public static void main(String[] args) throws ParseException {
// سياق الاستمرارية
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// يتم استرداد EntityManager من EntityManagerFactory السابق
em = emf.createEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// الطلب
Query sql1;
// حذف العناصر من الجدول PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// حذف العناصر من الجدول ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// إنشاء أشخاص
Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// إنشاء عناوين
Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// الربط بين الأشخاص <--> العناوين
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// استمرارية الأشخاص وعناوينهم بشكل متسلسل
em.persist(p1);
em.persist(p2);
// والعناوين a3 و a4 غير المرتبطة بالأشخاص
em.persist(a3);
em.persist(a4);
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// عرض العناوين
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
// نهاية المعاملة
tx.commit();
// نهاية EntityManager
em.close();
// نهاية EntityManagerFactory
emf.close();
// سجل
System.out.println("terminé...");
}
}
نحن نعلق فقط على ما يمثل اهتمامًا جديدًا مقارنة بما تمت دراسته بالفعل:
- السطور 31-32: يتم إنشاء شخصين
- السطور 34-37: يتم إنشاء أربعة عناوين
- السطور 39-42: نربط الأشخاص (p1، p2) بالعناوين (a1، a2). العناوين (a3، a4) هي عناوين يتيمة. لا يوجد أي شخص يشير إليها. يسمح بذلك الرمز DDL. إذا كان لكل شخص عنوان بالضرورة، فإن العكس ليس صحيحًا.
- السطور 44-45: يتم الاحتفاظ بالأشخاص (p1، p2). نظرًا لأننا وضعنا سمة cascade = CascadeType.ALL على العلاقة واحد إلى واحد التي تربط شخصًا بعنوانه، فإن العناوين (a1، a2) لهذين الشخصين يجب أن تخضع أيضًا لـ persist. هذا ما نريد التحقق منه. بالنسبة للعناوين اليتيمة (a3، a4)، نحن مضطرون إلى القيام بذلك بشكل صريح (السطور 47-48).
- السطور 51-53: عرض جدول الأشخاص
- السطور 56-57: عرض جدول العناوين
يؤدي تنفيذ [InitDB] مع MySQL5 إلى النتائج التالية:
![]() |
![]() |
- [1]: عرض وحدة التحكم
- [2]: الجداول [jpa03_hb_*] في منظور SQL Explorer
- [3]: جدول الأشخاص
- [4]: جدول العناوين. جميعها موجودة بالفعل. يجب أيضًا ملاحظة الارتباط بين العمود [adresse_id] في [3] والعمود [id] في [4] (مفتاح خارجي).
2.3.6. الصفحة الرئيسية
تتضمن الفئة [Main] ستة اختبارات سنستعرضها.
2.3.6.1. Test1
هذا الاختبار هو التالي:
// إنشاء كائنات
public static void test1() throws ParseException {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// إنشاء أشخاص
p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// إنشاء عناوين
a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
// الارتباطات بين الأشخاص والعناوين
p1.setAdresse(a1);
a1.setPersonne(p1);
p2.setAdresse(a2);
a2.setPersonne(p2);
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
// والعناوين a3 و a4 غير المرتبطة بالأشخاص
em.persist(a3);
em.persist(a4);
// نهاية المعاملة
tx.commit();
// يتم عرض الجداول
dumpPersonne();
dumpAdresse();
}
هذا الرمز مأخوذ من [InitDB]. ونتيجته هي كما يلي:
تم ملء الجدولين.
2.3.6.2. Test2
هذا الاختبار هو التالي:
// تعديل كائن من السياق
public static void test2() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// زيادة عدد أطفال p1
p1.setNbenfants(p1.getNbenfants() + 1);
// يتم تعديل حالته الاجتماعية
p1.setMarie(false);
// يتم حفظ الكائن p1 تلقائيًا (فحص التغييرات)
// أثناء المزامنة التالية (commit أو select)
// نهاية المعاملة
tx.commit();
// يتم عرض الجدول الجديد
dumpPersonne();
}
ونتيجته هي كما يلي:
- السطر 4: زاد عدد أطفال الشخص p1 بمقدار 1، وتغيرت نسخته من 0 إلى 1
2.3.6.3. Test4
هذا الاختبار هو التالي:
// حذف كائن ينتمي إلى سياق الاستمرارية
public static void test4() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم حذف الكائن المرفق p2
em.remove(p2);
// نهاية المعاملة
tx.commit();
// عرض الجداول الجديدة
dumpPersonne();
dumpAdresse();
}
- السطر 9: يتم حذف الشخص p2. هذا الشخص له علاقة تسلسلية مع العنوان a2. لذا يجب حذف العنوان a2 أيضًا.
نتيجة الاختبار 4 هي كما يلي:
- الشخص p2 الموجود في السطر 3 من الاختبار 1 لم يعد موجودًا في الاختبار 4
- وينطبق الأمر نفسه على عنوانه a2، الموجود في السطر 7 من الاختبار 1 وغير موجود في الاختبار 4.
2.3.6.4. Test5
هذا الاختبار هو التالي:
// فصل وإعادة ربط وتعديل
public static void test5() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// إعادة ربط p1 بالسياق الجديد
p1 = em.find(Personne.class, p1.getId());
// يتم تغيير عنوان p1
p1.getAdresse().setVille("Paris");
// نهاية المعاملة
tx.commit();
// يتم عرض الجداول الجديدة
dumpPersonne();
dumpAdresse();
}
- السطر 4: لدينا سياق استمرار جديد، وبالتالي فارغ.
- السطر 9: نضع الشخص p1 فيه. يتم البحث عن p1 في قاعدة البيانات لأنه غير موجود في السياق. أما العناصر التابعة لـ p1 (عنوانه)، فلا يتم استرجاعها من قاعدة البيانات لأننا كتبنا:
@OneToOne(..., fetch=FetchType.LAZY)
هذا هو مفهوم "التحميل المتأخر" أو "التحميل في الوقت المناسب": لا يتم جلب تبعيات كائن ثابت إلى الذاكرة إلا عند الحاجة إليها.
- السطر 11: يتم تعديل حقل المدينة في عنوان p1. وبسبب getAdresse، وإذا لم يكن عنوان p1 موجودًا بالفعل في سياق الاستمرارية، فسيتم إحضاره من خلال قراءة قاعدة البيانات.
- السطر 13: يتم التحقق من صحة المعاملة، مما سيؤدي إلى مزامنة سياق الاستمرارية مع قاعدة البيانات. سيلاحظ هذا أن عنوان الشخص p1 قد تم تعديله وسيقوم بحفظه.
يؤدي تنفيذ test5 إلى النتائج التالية:
- لقد تغيرت مدينة الشخص p1 (السطر 3 test4، السطر 10 test5) بالفعل من أنجيه (السطر 5 test4) إلى باريس (السطر 12 test5).
2.3.6.5. Test6
هذا الاختبار هو التالي:
// حذف كائن العنوان
public static void test6() {
EntityTransaction tx = null;
// سياق ثبات جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
tx = em.getTransaction();
tx.begin();
// إعادة ربط العنوان a3 بالسياق الجديد
a3 = em.find(Adresse.class, a3.getId());
System.out.println(a3);
// حذفها
em.remove(a3);
// نهاية المعاملة
tx.commit();
// تفريغ جدول العناوين
dumpAdresse();
}
- السطر 5: نحن في سياق استمرار جديد، وبالتالي فارغ.
- السطر 10: نضع العنوان a3 في سياق الاستمرارية
- السطر 13: نحذفه. كان عنوانًا يتيمًا (غير مرتبط بشخص). لذا، يمكن حذفه.
نتيجة التنفيذ هي كما يلي:
- اختفى العنوان a3 من الاختبار 5 (السطر 6) من عناوين الاختبار 6 (السطران 11-12)
2.3.6.6. Test7
هذا الاختبار هو التالي:
// التراجع
public static void test7() {
EntityTransaction tx = null;
try {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
tx = em.getTransaction();
tx.begin();
// إعادة ربط العنوان a1 بالسياق الجديد
newa1 = em.find(Adresse.class, a1.getId());
// إعادة ربط العنوان a4 بالسياق الجديد
newa4 = em.find(Adresse.class, a4.getId());
// محاولة حذفهما - من المفترض أن يتم إثارة استثناء لأنه لا يمكن حذف عنوان مرتبط بشخص، وهو ما ينطبق على newa1
em.remove(newa4);
em.remove(newa1);
// نهاية المعاملة
tx.commit();
} catch (RuntimeException e1) {
// حدثت مشكلة
System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
.getCause());
try {
if (tx.isActive())
tx.rollback();
} catch (RuntimeException e2) {
System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
}
// نقوم بإلغاء السياق الحالي
em.clear();
}
// تفريغ - لا بد أن جدول العناوين لم يتغير بسبب التراجع
dumpAdresse();
}
- test7: نختبر التراجع عن معاملة
- السطر 6: نحن في سياق استمرارية جديد، وبالتالي فارغ.
- السطر 11: نضع العنوان a1 في سياق الاستمرارية، تحت المرجع newa1
- السطر 13: نضع العنوان a4 في سياق الاستمرارية، تحت المرجع newa4
- السطران 15-16: يتم حذف العنوانين newa1 و newa4. newa1 هو عنوان الشخص p1، وبالتالي في قاعدة البيانات p1 يشير إلى newa1 بواسطة مفتاح خارجي. لذلك، ستفشل عملية حذف newa1 وستطلق استثناءً عند مزامنة سياق الاستمرارية عند إتمام المعاملة (السطر 18). وستتعرض هذه المعاملة لـ rollback (السطر 25)، وبالتالي سيتم إلغاء عمليتي المعاملة. لذلك، يجب أن نلاحظ أن العنوان newa4، الذي كان من الممكن حذفه بشكل قانوني، لم يتم حذفه.
يؤدي التنفيذ إلى النتيجة التالية:
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- جدول عناوين الاختبار 7 (السطران 12-13) مطابق لجدول الاختبار 6 (السطران 4-5). يبدو أن عملية التراجع قد تمت. ومع ذلك، فإن رسالة الخطأ في السطر 9 تمثل لغزًا وتستحق المزيد من البحث. يبدو أن الاستثناء الذي حدث ليس هو الاستثناء المتوقع. يجب تمرير سجلات Hibernate في log4j.properties في وضع DEBUG لفهم الأمر بشكل أوضح:
# خيار مسجل الجذر
log4j.rootLogger=ERROR, stdout
# خيارات تسجيل Hibernate (INFO يعرض رسائل بدء التشغيل فقط)
log4j.logger.org.hibernate=DEBUG
ونلاحظ عندئذٍ أنه عندما تم وضع العنوان a1 في سياق الاستمرارية، وضع Hibernate فيه أيضًا الشخص p1، ربما بسبب العلاقة الفردية لـ @Entity [Adresse]:
@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;
على الرغم من أننا طلبنا " LazyLoading " هنا، إلا أن التبعية [Personne] يتم تحميلها على الفور. وهذا يعني على الأرجح أن السمة fetch=FetchType.LAZY لا معنى لها هنا. نلاحظ بعد ذلك أنه عند إتمام المعاملة، أعد Hibernate حذف العناوين a1 و a4 وكذلك حفظ الشخص p1. وهنا تحدث الاستثناء: نظرًا لأن الشخص p1 لديه تسلسل على عنوانه، فإن Hibernate يريد أيضًا الاحتفاظ بالعنوان a1 في حين أنه قد تم حذفه للتو. Hibernate هو الذي يطلق الاستثناء وليس برنامج تشغيل Jdbc. ومن هنا تأتي الرسالة في السطر 9 أعلاه. علاوة على ذلك، يمكن ملاحظة أن rollback في السطر 25 لم يتم تنفيذه أبدًا لأن المعاملة أصبحت غير نشطة. وبالتالي، فإن الاختبار في السطر 24 يمنع rollback.
وبالتالي، لم يتم تحقيق الهدف المطلوب: إظهار عملية التراجع. في الواقع، لم يتم إصدار أي أمر SQL على قاعدة البيانات. سنستخلص بعض النقاط:
- أهمية تفعيل سجلات التفاصيل لفهم ما يفعله ORM
- إذا كان ORM يمكن أن يسهل حياة المطور، فإنه يمكن أيضًا أن يعقدها من خلال إخفاء السلوكيات التي قد يحتاج المطور إلى معرفتها. هنا، طريقة تحميل تبعيات @Entity.
2.3.7. مشروع Eclipse / Hibernate 2
نقوم بنسخ / لصق مشروع Eclipse / Hibernate من أجل تعديل تكوين كائنات @Entity بشكل طفيف:
![]() |
المشروع موجود [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
نقوم بتعديل @Entity [Adresse] فقط بحيث لا يكون لها علاقة عكسية واحد إلى واحد مع @Entity [Personne]:
package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {
// الحقول
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
...
@Column(length = 20, nullable = false)
private String pays;
// @OneToOne(mappedBy = "العنوان"، fetch=FetchType.LAZY)
// private Personne personne;
// المصممون
public Adresse() {
}
- السطران 25-26: تم حذف العلاقة العكسية @OneToOne. يجب أن نفهم جيدًا أن العلاقة العكسية ليست ضرورية أبدًا. العلاقة الرئيسية هي الوحيدة الضرورية. يمكن استخدام العلاقة العكسية للراحة. هنا، كانت تسمح بالحصول على مالك العنوان بطريقة بسيطة. يمكن دائمًا استبدال العلاقة العكسية باستعلام JPQL. وهذا ما سنوضحه في المثال التالي.
تم استنساخ برامج الاختبار بالضبط كما هي. ما يهمنا هو الاختبار 7 فقط، وهو الذي رأينا فيه العلاقة العكسية واحد إلى واحد قيد التشغيل. نضيف أيضًا اختبارًا رقم 8 لإظهار كيف يمكن، بدون العلاقة العكسية "العنوان -> الشخص"، الحصول على الشخص الذي يمتلك عنوانًا معينًا.
لا يتغير الاختبار 7. يعطي تنفيذه الآن النتائج التالية (السجلات معطلة):
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- اختبار 7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
- هذه المرة، لدينا بالفعل الاستثناء المتوقع: وهو الذي أطلقه برنامج Jdbc لأننا أردنا حذف سطر في الجدول [adresse] مرجعي بواسطة مفتاح خارجي لسطر في الجدول [personne]. يوضح السطر [10] سبب الخطأ.
- تمت عملية التراجع بنجاح: في نهاية الاختبار 7، الجدول [adresse] (السطران 12-13) هو نفسه الذي كان لدينا في نهاية الاختبار 6 (السطران 4-5).
ما الفرق مع الاختبار 7 في مشروع Eclipse السابق؟ لماذا لدينا هنا استثناء Jdbc لم نتمكن من الحصول عليه في الاختبار السابق؟ لأن @Entity [Adresse] لم يعد لها علاقة عكسية واحد إلى واحد مع @Entity [Personne]، فهي تُدار بشكل منفصل بواسطة Hibernate. عندما تم إدخال العنوان newa1 في سياق الاستمرارية، لم يقم Hibernate بإدخال الشخص p1 الذي يمتلك هذا العنوان في هذا السياق أيضًا. وبالتالي، تم حذف العناوين newa1 و newa4 دون وجود كيانات Personne في السياق.
الآن، كيف يمكننا من العنوان newa1 الحصول على الشخص p1 الذي يمتلك هذا العنوان؟ هذا سؤال مشروع. الاختبار 8 التالي يجيب عليه:
// علاقة عكسية واحد إلى واحد
// تم تنفيذها بواسطة استعلام JPQL
public static void test8() {
EntityTransaction tx = null;
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// بداية المعاملة
tx = em.getTransaction();
tx.begin();
// إعادة ربط العنوان a1 بالسياق الجديد
newa1 = em.find(Adresse.class, a1.getId());
// استرداد مالك هذا العنوان
Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
.getSingleResult();
// عرضها
System.out.println("adresse=" + newa1);
System.out.println("personne=" + p1);
// نهاية المعاملة
tx.commit();
}
- السطر 6: سياق استمرارية جديد فارغ
- السطران 8-9: بداية المعاملة
- السطر 11: يتم إدخال العنوان a1 في سياق الاستمرارية ويتم الإشارة إليه بواسطة newa1.
- السطر 13: يتم استرداد الشخص p1 الذي له العنوان newa1 من خلال استعلام JPQL. نعلم أن [Personne] و [Adresse] مرتبطان بعلاقة مفتاح خارجي. في الفئة [Personne]، الحقل [adresse] الذي يحتوي على التعليق التوضيحي @OneToOne هو الذي يجسد هذه العلاقة. تقوم الكتابة JPQL "select p from Personne p join p.adresse a" بإجراء ربط بين الجدولين [personne] و [adresse]. المكافئ SQL الذي تم إنشاؤه في وحدة تحكم Hibernate (انظر الأمثلة في الفقرة 2.1.12) هو التالي:
يمكننا أن نرى بوضوح ربط الجدولين. أصبح كل شخص مرتبطًا الآن بعنوانه. يبقى أن نوضح أننا مهتمون فقط بالعنوان newa1. يصبح الاستعلام "select p from Personne p join p.adresse a where a.id=:adresseId". يُلاحظ استخدام الأسماء المستعارة p و a. تستخدم الاستعلامات JPQL الأسماء المستعارة بشكل مكثف. وبالتالي، فإن التعبير "from Personne p join p.adresse a" يجعل الشخص ممثلاً بالاسم المستعار p وعنوانه (p.adresse) بالاسم المستعار a. تقوم عملية التقييد "where a.id=:adresseId" بتقييد الصفوف المطلوبة لتقتصر فقط على الأشخاص p الذين لديهم القيمة:adresseId كمعرف لعنوانهم a. :adresseId يُسمى معلمة، والأمر JPQL يُسمى أمرًا مُعَرَّفًا. عند التنفيذ، يجب أن تتلقى هذه المعلمة قيمة. هذه هي الطريقة
التي تسمح بتعيين قيمة لمعلمة محددة باسمها. وتجدر الإشارة إلى أن setParameter تُرجع كائن Query، تمامًا مثل الطريقة createQuery. وبالتالي يمكن تسلسل استدعاءات الطرق [em.createQuery(...).setParameter(...).getSingleResult(...)]، حيث أن الطرق [setParameter, getSingleResult] هي طرق تابعة للواجهة Query. يتم استخدام الطريقة [getSingleResult] لطلبات Select التي لا تعرض سوى نتيجة واحدة. وهذا هو الحال هنا.
- السطران 16-17: يتم عرض العنوان newa1 والشخص p1 الذي يمتلك هذا العنوان، للتحقق.
والنتيجة التي تم الحصول عليها هي التالية:
وهي صحيحة. نستخلص من هذا المثال أن العلاقة العكسية واحد إلى واحد بين @entity [Adresse] و@entity [Personne] لم تكن ضرورية. وقد أظهرت التجربة هنا أن حذفها أدى إلى سلوك أكثر قابلية للتنبؤ به للكود. وهذا هو الحال غالبًا.
2.3.8. وحدة التحكم Hibernate
استخدم الاختبار 8 السابق أمر JPQL لإجراء ربط بين الكيانين Personne و Adresse. على الرغم من تشابهها مع لغة SQL، فإن لغات Hibernate مثل JPQL وJPA أو HQL تتطلب التعلم، وتعد وحدة التحكم في Hibernate ممتازة لهذا الغرض. لقد استخدمناها بالفعل في الفقرة 2.1.12، لاستغلال جدول واحد. نكرر ذلك هنا لاستغلال جدولين مرتبطين بعلاقة مفتاح أجنبي.
لنقم بإنشاء وحدة تحكم Hibernate لمشروع Eclipse الحالي:
![]() |
- [1]: ننتقل إلى منظور [Hibernate Console] (Window / Open Perspective / Other)
- [2]: نقوم بإنشاء تكوين جديد
- باستخدام الزر [4]، نختار مشروع Java الذي تم إنشاء تكوين Hibernate من أجله. يظهر اسمه في [3].
- في [5]، نسمي هذا التكوين بالاسم الذي نريده. هنا، استخدمنا اسم مشروع Java.
- في [6]، نحدد أننا نستخدم تكوين JPA حتى يعرف الأداة أنه يجب عليها استخدام الملف [META-INF/persistence.xml]
- في [7]: نحدد في هذا الملف [META-INF/persistence.xml] أنه يجب استخدام وحدة الاستمرارية التي تسمى jpa.
- في [8]، نقوم بالتحقق من صحة التكوين.
للمتابعة، يجب تشغيل SGBD. هنا، يتعلق الأمر بـ MySQL5.
![]() |
- في [1]: التكوين الذي تم إنشاؤه يعرض شجرة ذات ثلاثة فروع
- في [2]: الفرع [Configuration] يسرد الكائنات التي استخدمتها وحدة التحكم لتكوين نفسها: هنا @Entity Personne و Adresse.
- في [3]: Session Factory هو مفهوم في Hibernate قريب من EntityManager في JPA. وهو يقوم بربط الكائنات بالعلاقات بفضل كائنات الفرع [Configuration]. في [3]، يتم عرض كائنات سياق الاستمرارية، وهنا مرة أخرى @Entity Personne و Adresse.
- في [4]: قاعدة البيانات التي يتم الوصول إليها عن طريق التكوين الموجود في [persistence.xml]. نجد فيها الجداول [jpa04_hb_*] التي تم إنشاؤها بواسطة مشروع Eclipse الحالي الخاص بنا.
![]() |
- في [1]، يتم إنشاء محرر HQL
- في المحرر HQL،
- في [2]، نختار تكوين Hibernate المراد استخدامه إذا كان هناك أكثر من تكوين (وهذا هو الحال هنا)
- في [3]، نكتب الأمر JPQL الذي نريد تنفيذه، وهنا الأمر JPQL من الاختبار 8
- في [4]، يتم تنفيذه
- في [5]، نحصل على نتائج الاستعلام في النافذة [Hibernate Query Result].
- في [6]، تتيح النافذة [Hibernate Dynamic SQL preview] رؤية الاستعلام SQL الذي تم تشغيله.
طريقة أخرى للحصول على نفس النتيجة:
![]() |
- في [1]: الأمر JPQL الذي يقوم بدمج الكيانات Personne و Adresse. يُطلق [ref1] على هذا الشكل اسم "الربط ثيتا".
- في [2]: المكافئ SQL
- في [3]: النتيجة
شكل ثالث مقبول فقط من قبل Hibernate (HQL):
![]() |
- في [1]: الأمر HQL. لا يقبل JPQL الترميز p.adresse.id. فهو لا يقبل سوى مستوى واحد من الإحالة.
- في [2]: المكافئ SQL. نلاحظ أنه يتجنب الانضمام بين الجداول.
- في [3]: النتيجة
فيما يلي أمثلة أخرى:
![]() |
- في [1]: قائمة الأشخاص مع عناوينهم
- في [2]: المكافئ SQL.
- في [3]: النتيجة
![]() |
- في [1]: قائمة العناوين مع مالكها إن وجد أو لا شيء (الربط الخارجي الأيمن: الكيان Adresse الذي سيوفر الأسطر التي لا علاقة لها بـ Personne يقع على يمين الكلمة الرئيسية join).
- في [2]: المكافئ SQL.
- في [3]: النتيجة
تجدر الإشارة إلى أن الكيان Personne هو الوحيد الذي يرتبط بالكيان Adresse. لم يعد العكس صحيحًا منذ أن تم حذف العلاقة العكسية واحد إلى واحد المسماة personne في الكيان Adresse. لو كانت هذه العلاقة العكسية موجودة، لكان من الممكن كتابة:
![]() |
- في [1]: قائمة العناوين مع مالكها إن وجد أو لا شيء إن لم يكن هناك مالك (الربط الخارجي الأيسر: الكيان Adresse الذي سيوفر الأسطر التي لا علاقة لها بـ Personne يقع على يسار الكلمة الرئيسية join).
- في [2]: المكافئ SQL.
- في [3]: النتيجة
ندعو القارئ بشدة إلى التدرب على لغة JPQL باستخدام وحدة التحكم Hibernate.
2.3.9. تنفيذ JPA / Toplink
نستخدم الآن تطبيق JPA / Toplink:
![]() |
المشروع الجديد لـ Eclipse للاختبارات هو التالي:
![]() |
رموز Java مطابقة لتلك الموجودة في مشروع Hibernate السابق. البيئة (المكتبات – persistence.xml – قاعدة البيانات – ملفات conf وddl – نصوص ant) هي تلك التي تمت دراستها في الفقرة 2.1.15.2. يوجد مشروع Eclipse [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
تم تعديل الملف <persistence.xml> في نقطة واحدة، وهي الكيانات المعلنة:
<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
<!-- المزود -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- فئات ثابتة -->
<class>entites.Personne</class>
<class>entites.Adresse</class>
<!-- خصائص وحدة الاستمرارية -->
...
- السطران 5 و6: الكيانان المُديران
يؤدي تشغيل [InitDB] مع SGBD MySQL5 إلى النتائج التالية:
![]() |
في [1]، عرض وحدة التحكم، وفي [2]، الجدولان [jpa04_tl] اللذان تم إنشاؤهما، وفي [3] البرامج النصية SQL التي تم إنشاؤها. ومحتواها كما يلي:
create.sql
CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
2.4. المثال 4: علاقة واحد إلى عدة
2.4.1. مخطط قاعدة البيانات
1 ![]() | 2 |
- في [1]، قاعدة البيانات وفي [2]، قاعدة البيانات الخاصة بها DDL (MySQL5)
ينتمي عنصر A(id, version, name) إلى فئة C(id, version, name) واحدة فقط. يمكن أن تحتوي فئة C على 0 أو 1 أو عدة عناصر. لدينا علاقة واحد إلى عدة (فئة -> عنصر) والعلاقة العكسية عدة إلى واحد (عنصر -> فئة). تتجسد هذه العلاقة من خلال المفتاح الأجنبي الذي تمتلكه الجدول [article] في الجدول [categorie] (السطور 24-28 من DDL).
2.4.2. كائنات @Entity التي تمثل قاعدة البيانات
يتم تمثيل المقالة بواسطة @Entity [Article] التالية:
package entites;
...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {
// الحقول
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// العلاقة الرئيسية Article (many) -> Category (one)
// مُنفَّذة بواسطة مفتاح خارجي (categorie_id) في المقالة
// كل مقال له بالضرورة فئة واحدة (nullable=false)
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "categorie_id", nullable = false)
private Categorie categorie;
// منشئات
public Article() {
}
// مُستردات ومُعيّنات
...
// toString
public String toString() {
return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
}
}
- الأسطر 9-11: المفتاح الأساسي لـ @Entity
- الأسطر 13-15: رقم إصدارها
- السطور 17-18: اسم المادة
- الأسطر 20-25: علاقة "عدة إلى واحد" تربط @Entity Article بـ @Entity Categorie:
- السطر 23: التعليق التوضيحي ManyToOne. يشير Many إلى @Entity Article الذي نحن فيه، ويشير One إلى @Entity Categorie (السطر 25). يمكن أن تحتوي فئة (One) على عدة عناصر (Many).
- السطر 24: التعليق التوضيحي ManyToOne يحدد عمود المفتاح الأجنبي في الجدول [article]. وستسمى (name) categorie_id ويجب أن تحتوي كل سطر على قيمة في هذا العمود (nullable=false).
- السطر 25: الفئة التي ينتمي إليها العنصر. عندما يتم وضع عنصر في سياق الاستمرارية، يُطلب عدم وضع فئته فيه على الفور (fetch=FetchType.LAZY، السطر 23). لا نعرف ما إذا كان لهذا الطلب معنى. سنرى.
يتم تمثيل الفئة بواسطة @Entity [Categorie] التالية:
package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
// الحقول
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// العلاقة العكسية فئة (واحدة) -> مقال (عدة) من العلاقة مقال (عدة) -> فئة (واحدة)
// إدراج متسلسل فئة -> إدراج مقالات
// تسلسل تحديث الفئة -> تحديث المقالات
// التسلسل التسلسلي للحذف: الفئة -> حذف المقالات
@OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
private Set<Article> articles = new HashSet<Article>();
// منشئون
public Categorie() {
}
// المُستردات والمُعيّنات
...
// toString
public String toString() {
return String.format("Categorie[%d,%d,%s]", id, version, nom);
}
// الارتباط الثنائي الاتجاه الفئة <--> المقالة
public void addArticle(Article article) {
// يتم إضافة المقالة إلى مجموعة مقالات الفئة
articles.add(article);
// يتم تغيير فئة المقالة
article.setCategorie(this);
}
}
- الأسطر 8-11: المفتاح الأساسي لـ @Entity
- الأسطر 12-14: إصدارها
- السطور 16-17: اسم الفئة
- الأسطر 19-24: مجموعة (set) مقالات الفئة
- السطر 23: تشير التعليقات التوضيحية @OneToMany إلى علاقة واحد إلى عدة. يشير One إلى @Entity [Categorie] التي نحن فيها، ويشير Many إلى النوع [Article] في السطر 24: فئة واحدة (One) لها عدة (Many) مقالات.
- السطر 23: التعليق هو عكس (mappedBy) التعليق ManyToOne الموضوع على الحقل categorie في الكيان @Entity Article: mappedBy=categorie. العلاقة ManyToOne الموضوعة على الحقل categorie في الكيان @Entity Article هي العلاقة الرئيسية. وهي ضرورية. وهي تجسد علاقة المفتاح الأجنبي التي تربط @Entity Article بـ @Entity Categorie. العلاقة OneToMany الموضوعة على الحقل articles في @Entity Categorie هي العلاقة العكسية. وهي ليست ضرورية. وهي وسيلة ملائمة للحصول على المقالات من فئة معينة. وبدون هذه الوسيلة، سيتم الحصول على هذه المقالات من خلال استعلام JPQL.
- السطر 23: تطلب cascadeType.ALL أن يتم تطبيق العمليات (persist، merge، remove) التي يتم إجراؤها على @Entity Categorie بشكل متسلسل على مقالاتها.
- السطر 24: سيتم وضع العناصر في فئة ما في كائن من نوع Set<Article>. لا يقبل النوع Set التكرارات. وبالتالي لا يمكن وضع نفس العنصر مرتين في كائن Set<Article>. ماذا يعني "نفس العنصر"؟ للقول بأن المقالة a هي نفس المقالة b، تستخدم Java التعبير a.equals(b). في فئة Object، الأم لجميع الفئات، يكون a.equals(b) صحيحًا إذا كان a==b، c.a.d. إذا كان للكائنين a و b نفس موقع الذاكرة. قد نرغب في القول بأن العنصرين a و b متماثلان إذا كان لهما نفس الاسم. في هذه الحالة، يجب على المطور إعادة تعريف طريقتين في الفئة [Article]:
- equals: التي يجب أن تعطي نتيجة صحيحة إذا كان للعنصرين نفس الاسم
- hashCode: يجب أن تُرجع قيمة عددية متطابقة لكائنين [Article] تعتبرهما الطريقة equals متساويين. هنا، سيتم بناء القيمة بناءً على اسم العنصر. يمكن أن تكون القيمة التي ترجعها hashCode أي عدد صحيح. وتُستخدم في حاويات كائنات مختلفة، لا سيما القواميس (Hashtable).
يمكن للعلاقة OneToMany استخدام أنواع أخرى غير Set لتخزين العنصر المتعدد (Many)، مثل كائنات List على سبيل المثال. لن نتطرق إلى هذه الحالات في هذا المستند. يمكن للقارئ الاطلاع عليها في [ref1].
- السطر 38: تسمح لنا الطريقة [addArticle] بإضافة عنصر إلى فئة. تقوم الطريقة بتحديث طرفي العلاقة OneToMany التي تربط [Categorie] بـ [Article].
2.4.3. مشروع Eclipse / Hibernate 1
التنفيذ JPA المستخدم هنا هو تنفيذ Hibernate. مشروع Eclipse للاختبارات هو التالي:
![]() |
المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.4.4. إنشاء ملف DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لـ SGBD MySQL5 هو الملف الموضح في بداية هذا المثال، في الفقرة 2.4.1.
2.4.5. InitDB
رمز [InitDB] هو التالي:
package tests;
...
public class InitDB {
// الثوابت
private final static String TABLE_ARTICLE = "jpa05_hb_article";
private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";
public static void main(String[] args) {
// سياق الاستمرارية
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// يتم استرداد EntityManager من EntityManagerFactory السابق
em = emf.createEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// طلب
Query sql1;
// حذف العناصر من الجدول ARTICLE
sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
sql1.executeUpdate();
// حذف العناصر من الجدول CATEGORIE
sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
sql1.executeUpdate();
// إنشاء ثلاث فئات
Categorie categorieA = new Categorie();
categorieA.setNom("A");
Categorie categorieB = new Categorie();
categorieB.setNom("B");
Categorie categorieC = new Categorie();
categorieC.setNom("C");
// إنشاء 3 مقالات
Article articleA1 = new Article();
articleA1.setNom("A1");
Article articleA2 = new Article();
articleA2.setNom("A2");
Article articleB1 = new Article();
articleB1.setNom("B1");
// ربطها بفئتها
categorieA.addArticle(articleA1);
categorieA.addArticle(articleA2);
categorieB.addArticle(articleB1);
// حفظ الفئات وإدراج المقالات بشكل متسلسل
em.persist(categorieA);
em.persist(categorieB);
em.persist(categorieC);
// عرض الفئات
System.out.println("[categories]");
for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
System.out.println(p);
}
// عرض المقالات
System.out.println("[articles]");
for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
System.out.println(p);
}
// نهاية المعاملة
tx.commit();
// نهاية EntityManager
em.close();
// نهاية EntityMangerFactory
emf.close();
// سجل
System.out.println("terminé...");
}
}
- السطور 22-27: يتم إفراغ الجداول [article] و [categorie]. تجدر الإشارة إلى أنه يجب البدء بالجدول الذي يحتوي على المفتاح الخارجي. إذا بدأنا بالجدول [categorie]، فسنحذف الفئات المشار إليها في أسطر الجدول [article]، وهذا ما سيرفضه الجدول SGBD.
- السطور 29-34: يتم إنشاء ثلاث فئات A، B، C
- الأسطر 36-41: يتم إنشاء ثلاث مقالات A1، A2، B1 (يشير الحرف إلى الفئة)
- الأسطر 43-45: يتم وضع المقالات الثلاثة في فئاتها المعنية
- الأسطر 47-49: يتم وضع الفئات الثلاث في سياق الاستمرارية. وبسبب التسلسل الهرمي "الفئة -> العنصر"، سيتم وضع العناصر فيها أيضًا. وبالتالي، فإن جميع الكائنات التي تم إنشاؤها موجودة الآن في سياق الاستمرارية.
- الأسطر 50-59: يتم استدعاء سياق الاستمرارية للحصول على قائمة الفئات والمواد. نعلم أن هذا سيؤدي إلى مزامنة السياق مع قاعدة البيانات. في هذه اللحظة، سيتم تسجيل الفئات والمواد في الجداول الخاصة بها.
يؤدي تنفيذ [InitDB] مع MySQL5 إلى النتائج التالية:
![]() |
- [1]: عرض وحدة التحكم
- [2]: الجداول [jpa05_hb_*] في منظور SQL Explorer
- [3]: جدول الفئات
- [4]: جدول المقالات. يُلاحظ وجود رابط لـ [categorie_id] في [4] مع [id] في [3] (مفتاح خارجي).
2.4.6. الصفحة الرئيسية
تسلسل الفئة [Main] الاختبارات التي نستعرضها باستثناء الاختبارين 1 و 2 اللذين يستخدمان كود [InitDB] لتهيئة قاعدة البيانات.
2.4.6.1. Test3
هذا الاختبار هو التالي:
// البحث عن عنصر معين
public static void test3() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// تحميل الفئة
Categorie categorie = em.find(Categorie.class, categorieA.getId());
// عرض الفئة والمواد المرتبطة بها
System.out.format("Articles de la catégorie %s :%n", categorie);
for (Article a : categorie.getArticles()) {
System.out.println(a);
}
// إنهاء المعاملة
tx.commit();
}
- السطر 4: لدينا سياق استمرارية جديد وبالتالي فارغ
- السطران 6-7: بداية المعاملة
- السطر 9: يتم جلب الفئة A من قاعدة البيانات إلى سياق الاستمرارية
- السطر 11: يتم عرض الفئة A
- السطور 12-14: يتم عرض عناصر الفئة A. هنا يظهر فائدة العلاقة العكسية OneToMany لعناصر @Entity Categorie. وجودها يوفر علينا إجراء استعلام JPQL لطلب عناصر الفئة A. للحصول عليها، نستخدم الطريقة get للحقل articles.
والنتائج هي كما يلي:
- السطر 20: الفئة A
- السطران 21-22: المنتجان من الفئة A
2.4.6.2. Test4
هذا الاختبار هو التالي:
// حذف عنصر
@SuppressWarnings("unchecked")
public static void test4() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// تحميل عنصر A1
Article newarticle1 = em.find(Article.class, articleA1.getId());
// حذف عنصر A1 (لا توجد فئة محملة حاليًا)
em.remove(newarticle1);
// toplink: يجب إزالة المقالة من فئتها وإلا سيتعطل الاختبار 6
// hibernate: هذا غير ضروري
newarticle1.getCategorie().getArticles().remove(newarticle1);
// نهاية المعاملة
tx.commit();
// تفريغ المقالات
dumpArticles();
}
- الاختبار 4 يحذف المادة A1
- السطر 5: نبدأ من سياق جديد وفارغ
- السطر 10: يتم نقل المادة A1 إلى سياق الاستمرارية. وسيتم الإشارة إليها هناك بواسطة newarticle1.
- السطر 12: يتم حذفه من السياق
- السطر 15: الفئات A و B و C والمواد A1 و A2 و B1، إذا لم تعد دائمة، فإنها لا تزال موجودة في الذاكرة. يتم فصلها ببساطة عن سياق الاستمرارية. يتم إزالة المادة A1 التي تنتمي إلى مواد الفئة A. سيسمح ذلك لاحقًا بإعادة ربط الفئة A بسياق الاستمرارية. إذا لم يتم ذلك، فسيتم ربط الفئة A بمجموعة من المواد التي تم حذف إحداها. لا يبدو أن هذا يزعج Hibernate ولكنه يتسبب في تعطل Toplink.
- السطر 19: يتم عرض جميع العناصر للتحقق من اختفاء A1.
النتائج هي كما يلي:
لقد اختفى المقال A1 بالفعل.
2.4.6.3. Test5
هذا الاختبار هو التالي:
// تعديل مادة واحدة
public static void test5() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// تعديل articleA2
articleA2.setNom(articleA2.getNom() + "-");
// تم إعادة articleA2 إلى سياق الاستمرارية
em.merge(articleA2);
// نهاية المعاملة
tx.commit();
// تفريغ العناصر
dumpArticles();
}
- الاختبار 5 يغير اسم المادة A2
- السطر 4: نبدأ من سياق جديد وفارغ
- السطر 9: نغير اسم العنصر المنفصل A2 ليصبح "A2-".
- السطر 11: تم إعادة ربط الكائن المنفصل A2 بسياق الاستمرارية. وتجدر الإشارة إلى أن A2 لا يزال كائنًا منفصلاً. الكائن em.merge(articleA2) هو الذي أصبح الآن جزءًا من سياق الاستمرارية. لم يتم تخزين هذا الكائن هنا في متغير كما هو معتاد. وبالتالي فهو غير قابل للوصول.
- السطر 13: مزامنة سياق الاستمرارية مع قاعدة البيانات. سيتم تعديل العنصر A2 في قاعدة البيانات وستتغير رقم إصداره من N إلى N+1. لم تعد نسخة الذاكرة المنفصلة articleA2 صالحة. وينطبق الأمر نفسه على الكائن المنفصل الذي يمثل الفئة A لأنه يحتوي على articleA2 ضمن مقالاته.
- السطر 15: يتم عرض جميع العناصر للتحقق من تغيير اسم العنصر A2
والنتائج هي كما يلي:
لقد تم تغيير اسم المادة A2 بالفعل.
2.4.6.4. Test6
هذا الاختبار هو التالي:
// تعديل فئة واحدة ومقالاتها
public static void test6() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// تحميل الفئة
categorieA = em.find(Categorie.class, categorieA.getId());
// قائمة مقالات الفئة A
for (Article a : categorieA.getArticles()) {
a.setNom(a.getNom() + "-");
}
// تعديل اسم الفئة
categorieA.setNom(categorieA.getNom() + "-");
// نهاية المعاملة
tx.commit();
// تفريغ الفئات والمنتجات
dumpCategories();
dumpArticles();
}
- الاختبار 6 يغير اسم الفئة A وجميع مقالاتها
- السطر 4: نبدأ من سياق جديد وفارغ
- السطر 9: نبحث عن الفئة A في قاعدة البيانات. لا نقوم بعملية merge للكائن المنفصل categorieA لأننا نعلم أن لديه مرجعًا إلى المادة A2 التي أصبحت قديمة. لذلك نبدأ من الصفر.
- السطران 11-12: نقوم بتغيير اسم جميع العناصر في الفئة A. مرة أخرى، نستخدم العلاقة العكسية OneToMany عبر الطريقة getArticles.
- السطر 15: يتم تعديل اسم الفئة أيضًا
- السطر 17: نهاية المعاملة. تتم مزامنة السياق مع قاعدة البيانات. سيتم تحديث جميع كائنات السياق التي تم تعديلها في قاعدة البيانات.
- السطران 21-22: يتم عرض العناصر والفئات للتحقق
النتائج هي كما يلي:
تم تغيير اسم المنتج A2 مرة أخرى وكذلك الفئة A.
2.4.6.5. Test7
هذا الاختبار هو التالي:
// حذف فئة
public static void test7() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// استمرارية catégorieB ودمج (merge) المنتجات المرتبطة
Categorie mergedcategorieB = em.merge(categorieB);
// حذف الفئة وحذف (delete) العناصر المرتبطة بها بشكل متسلسل
em.remove(mergedcategorieB);
// نهاية المعاملة
tx.commit();
// تفريغ الفئات والمقالات
dumpCategories();
dumpArticles();
}
- الاختبار 7 يحذف الفئة B وبالتالي مقالاتها
- السطر 4: نبدأ من سياق جديد وفارغ
- السطر 9: الفئة B موجودة في الذاكرة ككائن منفصل عن سياق الاستمرارية. نعيد دمجها (merge) في سياق الاستمرارية. وبالتالي، ستخضع مقالاتها (المقالة B1) لعملية merge وبالتالي ستُعاد إلى سياق الاستمرارية.
- السطر 11: الآن بعد أن أصبحت الفئة B في السياق، يمكن حذفها (remove). بالتسلسل، ستخضع مقالاتها أيضًا لعملية remove. هذه العملية ممكنة لأن العملية merge في السطر 9 أعادتها إلى سياق الاستمرارية.
- السطر 13: نهاية المعاملة. سيتم مزامنة السياق. سيتم حذف كائنات السياق التي خضعت لعملية remove من قاعدة البيانات.
- السطران 15-16: يتم عرض العناصر والفئات للتحقق
النتائج هي كما يلي:
لقد اختفت الفئة B والمنتج B1 بالفعل.
2.4.6.6. Test8
هذا الاختبار هو التالي:
// الاستعلامات
@SuppressWarnings("unchecked")
public static void test8() {
// سياق استمرارية جديد
EntityManager em = getNewEntityManager();
// معاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// قائمة المنتجات من الفئة A
List articles = em
.createQuery(
"select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
.getResultList();
// عرض العناصر
System.out.println("Articles de la catégorie A");
for (Object a : articles) {
System.out.println(a);
}
// نهاية المعاملة
tx.commit();
}
- يوضح الاختبار 7 كيفية استرداد العناصر من فئة ما دون المرور بالعلاقة العكسية. وهذا يدل على أن هذه العلاقة ليست ضرورية.
- السطر 4: نبدأ من سياق جديد وفارغ
- السطر 10: استعلام JPQL يطلب جميع المقالات من فئة اسمها يبدأ بحرف A
- الأسطر 15-17: عرض نتيجة الاستعلام.
النتائج هي كما يلي:
2.4.7. مشروع Eclipse / Hibernate 2
نقوم بنسخ / لصق مشروع Eclipse / Hibernate لتوضيح نقطة حول مفهوم العلاقة الرئيسية / العلاقة العكسية التي أنشأناها حول التعليق التوضيحي @ManyToOne (رئيسية) لـ @Entity [Article] والعلاقة العكسية @OneToMany (عكسية) لـ @Entity [Categorie]. نريد أن نوضح أنه إذا لم يتم الإعلان عن هذه العلاقة الأخيرة على أنها عكسية للأخرى، فإن المخطط الذي تم إنشاؤه لقاعدة البيانات سيكون مختلفًا تمامًا عن المخطط الذي تم إنشاؤه سابقًا.
![]() |
في [1] المشروع الجديد في Eclipse. في [2] أكواد Java، وفي [3] البرنامج النصي ant الذي سيقوم بإنشاء مخطط قاعدة البيانات SQL. المشروع موجود في [4] في مجلد الأمثلة [5]. سنقوم باستيراده.
سنقوم فقط بتعديل @Entity [Categorie] بحيث لا يتم تعريف علاقتها @OneToMany مع @Entity [Article] لم تعد تُعلن على أنها عكس العلاقة @ManyToOne التي تربط @Entity [Article] بـ @Entity [Categorie]:
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {
// الحقول
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@SuppressWarnings("unused")
@Version
private int version;
@Column(length = 30)
private String nom;
// علاقة OneToMany غير عكسية (عدم وجود mappedby) الفئة (واحدة) -> المنتج (عدة)
// تم تنفيذها بواسطة جدول ربط Categorie_Article بحيث يمكن الوصول من فئة
// يمكن الوصول إلى المقالات في هذه الفئة
@OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
// المصنعين
...
- الأسطر 18-22: نريد أيضًا الحفاظ على إمكانية العثور على المقالات في فئة معينة بفضل العلاقة @OneToMany الواردة في السطر 21. لكننا نريد معرفة تأثير السمة mappedBy التي تجعل من العلاقة عكس العلاقة الرئيسية المحددة في مكان آخر، في @Entity أخرى. هنا، تمت إزالة mappedBy.
نقوم بتنفيذ مهمة ant-DLL (انظر الفقرة 2.1.7) باستخدام SGBD MySQL5. المخطط الناتج هو التالي:
![]() |
يجب ملاحظة النقاط التالية:
- تم إنشاء جدول جديد [categorie_article] [1]. لم يكن موجودًا من قبل.
- وهي عبارة عن جدول ربط بين الجداول [categorie] [2] و [article] [3]. إذا كانت الكائنات Article a1 و a2 جزءًا من الفئة c1، فسيتم العثور في جدول الربط على الأسطر التالية:
حيث c1 و a1 و a2 هي المفاتيح الأساسية للكائنات المقابلة.
- تم إنشاء جدول الربط [categorie_article] [1] بواسطة Hibernate بحيث يمكن، انطلاقًا من كائن Categorie c، العثور على كائنات Article a التابعة لـ c. العلاقة @OneToMany هي التي فرضت إنشاء هذه الجدولة. ولأننا لم نعلنها عكسية للعلاقة الرئيسية @ManyToOne الخاصة بـ @Entity Article، لم يكن Hibernate يعلم أنه يمكنه استخدام هذه العلاقة الرئيسية لاسترداد المقالات من فئة c. لذا فقد لجأ إلى طريقة أخرى.
- من خلال هذا المثال، يمكننا فهم مفاهيم العلاقات principale و inverse بشكل أفضل. تستخدم إحداهما (العكسية) خصائص الأخرى (الرئيسية).
المخطط SQL لهذه القاعدة البيانات لـ MySQL5 هو التالي:
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D26D17756;
alter table jpa05_hb_categorie_jpa06_hb_article
drop
foreign key FK79D4BA1D424C61C9;
alter table jpa06_hb_article
drop
foreign key FK4547168FECCE8750;
drop table if exists jpa05_hb_categorie;
drop table if exists jpa05_hb_categorie_jpa06_hb_article;
drop table if exists jpa06_hb_article;
create table jpa05_hb_categorie (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
primary key (id)
) ENGINE=InnoDB;
create table jpa05_hb_categorie_jpa06_hb_article (
jpa05_hb_categorie_id bigint not null,
articles_id bigint not null,
primary key (jpa05_hb_categorie_id, articles_id),
unique (articles_id)
) ENGINE=InnoDB;
create table jpa06_hb_article (
id bigint not null auto_increment,
version integer not null,
nom varchar(30),
categorie_id bigint not null,
primary key (id)
) ENGINE=InnoDB;
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id),
add constraint FK79D4BA1D26D17756
foreign key (jpa05_hb_categorie_id)
references jpa05_hb_categorie (id);
alter table jpa05_hb_categorie_jpa06_hb_article
add index FK79D4BA1D424C61C9 (articles_id),
add constraint FK79D4BA1D424C61C9
foreign key (articles_id)
references jpa06_hb_article (id);
alter table jpa06_hb_article
add index FK4547168FECCE8750 (categorie_id),
add constraint FK4547168FECCE8750
foreign key (categorie_id)
references jpa05_hb_categorie (id);
- السطور 19-24، إنشاء الجدول [categorie] والسطور 33-39، إنشاء الجدول [article]. تجدر الإشارة إلى أنهما مطابقان لما كانا عليه في المثال السابق.
- الأسطر 26-31: إنشاء جدول الربط [categorie_article] بسبب وجود العلاقة غير العكسية @OneToMany لـ @Entity Categorie. الصفوف في هذه الجدولة من النوع [c,a] حيث c هو المفتاح الأساسي لفئة c و a هو المفتاح الأساسيلصنف a التابع للفئة c. يتكون المفتاح الأساسي لجدول الربط هذا من المفتاحين الأساسيين [c,a] المربوطين معًا (السطر 29).
- السطور 41-45: قيد المفتاح الأجنبي من الجدول [categorie_article] إلى الجدول [categorie]
- الأسطر 47-51: قيد المفتاح الأجنبي من الجدول [categorie_article] إلى الجدول [article]
- الأسطر 53-57: قيد المفتاح الأجنبي من الجدول [article] إلى الجدول [categorie]
يُطلب من القارئ تنفيذ الاختبارات [InitDB] و [Main]. فهي تعطي نفس النتائج السابقة. ومع ذلك، فإن مخطط قاعدة البيانات زائد عن الحاجة وستتدهور الأداء مقارنة بالإصدار السابق. من الضروري بلا شك التعمق في مسألة العلاقات العكسية/الرئيسية هذه لمعرفة ما إذا كانت التهيئة الجديدة ستؤدي إلى المزيد من التعارضات بسبب وجود علاقتين مستقلتين لتمثيل نفس الشيء: العلاقة متعددة إلى واحد التي تربط الجدول [article] بالجدول [categorie].
2.4.8. التنفيذ JPA / Toplink - 1
نستخدم الآن تطبيق JPA / Toplink:
![]() |
مشروع Eclipse مع Toplink هو نسخة من مشروع Eclipse مع Hibernate، الإصدار 1:
![]() |
أكواد Java مطابقة لتلك الموجودة في مشروع Hibernate - الإصدار 1 - السابق. البيئة (المكتبات – persistence.xml – قاعدة البيانات – ملفات conf وddl – نصوص ant) هي تلك التي تمت دراستها في الفقرة 2.1.15.2. يوجد مشروع Eclipse [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
تم تعديل الملف <persistence.xml> [2] في نقطة واحدة، وهي الكيانات المعلنة:
...
<!-- الفئات الدائمة -->
<class>entites.Categorie</class>
<class>entites.Article</class>
...
- السطران 3 و 4: الكيانان المداران
يؤدي تنفيذ [InitDB] مع SGBD MySQL5 إلى النتائج التالية:
![]() |
في [1]، عرض وحدة التحكم، في [2]، الجدولان [jpa05_tl] اللذان تم إنشاؤهما، وفي [3] البرامج النصية SQL التي تم إنشاؤها. ومحتواها كما يلي:
create.sql
CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
drop.sql
ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
يتم تنفيذ [Main] دون أخطاء.
2.4.9. تنفيذ JPA / Toplink - 2
هذا المشروع Eclipse مستمد من المشروع السابق عن طريق النسخ. وبما أنه تم تنفيذه باستخدام Hibernate، يتم إزالة السمة mappedBy من العلاقة @OneToMany الخاصة بـ @Entity Categorie.
@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {
// الحقول
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private int version;
@Column(length = 30)
private String nom;
// علاقة OneToMany غير عكسية (عدم وجود mappedby) الفئة (واحدة) ->
// مقالة (many)
// مُنفَّذة بواسطة جدول ربط Categorie_Article بحيث من
// من فئة
// يمكن الوصول إلى عدة مقالات
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Article> articles = new HashSet<Article>();
يكون المخطط SQL الذي تم إنشاؤه لـ MySQL5 كما يلي:
create.sql
CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
- السطر 2: جدول الربط الذي يجسد العلاقة @OneToMany غير العكسية السابقة.
يتم تنفيذ [InitDB] دون أخطاء، لكن تنفيذ [Main] يتعطل عند الاختبار 7 مع السجلات (FINEST) التالية:
- السطر 3: merge في الفئة B
- السطر 4: يتم وضع المقالة التابعة B1 في السياق
- السطر 5: نفس الشيء بالنسبة للفئة B نفسها
- السطر 6: remove في الفئة B
- السطر 7: remove على المادة B1 (بالتسلسل)
- السطر 8: يتم طلب commit الخاص بالمعاملة بواسطة كود Java
- السطر 9: تبدأ معاملة - يبدو أنها لم تكن قد بدأت بعد.
- السطر 10: سيتم حذف العنصر B1 بواسطة عملية DELETE في الجدول [article]. وهنا تكمن المشكلة. تحتوي جدول الربط [categorie_article] على مرجع إلى السطر B1 في جدول [article]. سيؤدي حذف B1 في [article] إلى انتهاك قيد مفتاح أجنبي.
- السطر 13 وما بعده: تحدث الاستثناء
ما الاستنتاج؟
- مرة أخرى، لدينا مشكلة قابلية النقل بين Hibernate و Toplink: نجح Hibernate في هذا الاختبار
- لا يدعم Toplink بشكل جيد أنه عندما تكون علاقتان في الواقع معكوسة لبعضهما البعض، لا يتم إعلان إحداهما كعلاقة رئيسية والأخرى كعلاقة معكوسة. يمكننا قبول ذلك لأن هذه الحالة تمثل في الواقع خطأ في التكوين. في مثالنا، لا توجد علاقة بين الجدول [article] وجدول الربط [categorie_article]. لذلك يبدو من الطبيعي ألا يحاول Toplink العمل مع الجدول [categorie_article] عند إجراء عملية على الجدول [article].
2.5. المثال 5: علاقة متعددة إلى متعددة مع جدول ربط صريح
2.5.1. مخطط قاعدة البيانات
![]() |
- في [1]، قاعدة البيانات MySQL5
نحن نعرف بالفعل الجداول [personne] [2] و [adresse] [3]. وقد تمت دراستها في الفقرة 2.3.1. نأخذ النسخة التي يكون فيها عنوان الشخص موضوع جدول خاص به [adresse] و [3]. في الجدول [personne]، تتجسد العلاقة التي تربط الشخص بعنوانه في قيد مفتاح أجنبي.
يمارس الشخص أنشطة. وترد هذه الأنشطة في الجدول [activite] [4]. يمكن للشخص ممارسة عدة أنشطة ويمكن أن يمارس نشاطًا ما عدة أشخاص. وبالتالي، تربط علاقة متعددة الأطراف الجدولين [personne] و [activite]. وتتجسد هذه العلاقة في جدول الربط [personne_activite] [5].
2.5.2. كائنات @Entity التي تمثل قاعدة البيانات
سيتم تمثيل الجداول السابقة بواسطة كائنات @Entity التالية:
- سيمثل @Entity Personne الجدول [personne]
- ستمثل @Entity Adresse الجدول [adresse]
- ستمثل @Entity Activite الجدول [activite]
- @Entity PersonneActivite سيمثل الجدول [personne_activite]
العلاقات بين هذه الكيانات هي كما يلي:
- تربط علاقة واحد إلى واحد الكيان Personne بالكيان Adresse: شخص p له عنوان a. الكيان Personne الذي يمتلك المفتاح الأجنبي سيكون له العلاقة الرئيسية، والكيان Adresse له العلاقة العكسية.
- تربط علاقة متعددة الأطراف بين الكيانين Personne و Activite: حيث يمارس الشخص الواحد عدة أنشطة، وتُمارس النشاط الواحد من قبل عدة أشخاص. يمكن تحقيق هذه العلاقة مباشرةً من خلال تعليق @ManyToMany في كل من الكيانين، بحيث يُعلن أحدهما عكس الآخر. سيتم استكشاف هذا الحل لاحقًا. هنا، نحقق العلاقة متعددة-إلى-متعددة عن طريق علاقتين واحد-إلى-متعدد:
- علاقة واحد إلى عدة تربط الكيان Personne بالكيان PersonneActivite: يتم الإشارة إلى سطر واحد (One) من الجدول [personne] بواسطة عدة (Many) أسطر من الجدول [personne_activite]. ستحتفظ الجدول [personne_activite] الذي يحتوي على المفتاح الأجنبي بالعلاقة الرئيسية @ManyToOne، بينما ستحتفظ الكيان Personne بالعلاقة العكسية @OneToMany.
- علاقة واحد إلى عدة تربط الكيان Activite بالكيان PersonneActivite: يتم الإشارة إلى سطر واحد (One) من الجدول [activite] من قبل عدة (Many) أسطر من الجدول [personne_activite]. ستحتفظ الجدول [personne_activite] الذي يحتوي على المفتاح الأجنبي بالعلاقة @ManyToOne الرئيسية، بينما ستحتفظ الكيان Activite بالعلاقة @OneToMany العكسية.
الكيان @Entity Personne هو التالي:
@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// العلاقة الرئيسية Personne (واحدة) -> Adresse (واحدة)
// تم تنفيذها بواسطة المفتاح الأجنبي الشخص (adresse_id) -> العنوان
// إدراج متسلسل شخص -> إدراج عنوان
// تسلسل تحديث شخص -> تحديث عنوان
// التسلسل التسلسلي حذف شخص -> حذف عنوان
// يجب أن يكون لكل شخص عنوان واحد (nullable=false)
// عنوان واحد ينتمي إلى شخص واحد فقط (فريد=صحيح)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// علاقة شخص (واحد) -> PersonneActivite (كثير)
// عكس العلاقة الحالية PersonneActivite (متعدد) -> شخص (واحد)
// حذف متسلسل شخص -> حذف PersonneActivite
@OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();
// المصنعون
هذه الكيان @Entity معروفة. نحن نعلق فقط على العلاقات التي تربطها بالكيانات الأخرى:
- السطور 30-39: علاقة واحد إلى واحد @OneToOne مع @Entity Adresse، متمثلة في مفتاح خارجي [adresse_id] (السطر 38) الذي ستحتوي عليه الجدول [personne] في الجدول [adresse].
- السطور 41-45: علاقة واحد إلى عدة @OneToMany مع @Entity PersonneActivite. يتم الإشارة إلى شخص واحد (One) من خلال عدة (Many) أسطر في جدول الربط [personne_activite] الممثلة بواسطة @Entity PersonneActivite. سيتم وضع هذه الكائنات PersonneActivite في نوع Set<PersonneActivite> حيث PersonneActivite هو نوع سنقوم بتعريفه لاحقًا.
- السطر 44: العلاقة "واحد إلى عدة" المحددة هنا هي العلاقة العكسية لعلاقة رئيسية محددة على الحقل personne للكيان @Entity PersonneActivite (الكلمة الرئيسية mappedBy). لدينا تسلسل Personne -> Activite عند الحذف: سيؤدي حذف شخص p إلى حذف العناصر الدائمة من النوع PersonneActivite الموجودة في المجموعة p.activites.
@Entity Adresse هي كما يلي:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// الحقول
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- السطران 28-29: العلاقة @OneToOne هي العكس للعلاقة @OneToOne التي تشير إلى @Entity Personne (السطران 37-38 من Personne).
الكيان @Entity Activite هو التالي
@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {
// الحقول
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// علاقة النشاط (واحد) -> PersonneActivite (كثير)
// عكس العلاقة الموجودة PersonneActivite (العديد) -> النشاط (واحد)
// حذف متسلسل النشاط -> حذف PersonneActivite
@OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();
- السطور 6-9: المفتاح الأساسي للنشاط
- السطور 11-13: رقم إصدار النشاط
- السطران 15-16: اسم النشاط
- الأسطر 18-22: العلاقة «واحد إلى عدة» التي تربط الكيان @Entity Activite بالكيان @Entity PersonneActivite: يتم الإشارة إلى نشاط واحد (One) من خلال عدة (Many) أسطر في جدول الربط [personne_activite] الممثلة بواسطة @Entity PersonneActivite. سيتم وضع هذه الكائنات PersonneActivite في نوع Set<PersonneActivite>.
- السطر 22: العلاقة "واحد إلى عدة" المحددة هنا هي العلاقة العكسية لعلاقة رئيسية محددة على الحقل activite في @Entity PersonneActivite (الكلمة الرئيسية mappedBy). لدينا تسلسل Activite -> PersonneActivite عند عمليات الحذف: سيؤدي حذف الجدول [activite] مننشاط a سيؤدي إلى حذف جدول الربط [personne_activite] للعناصر الدائمة من النوع PersonneActivite الموجودة في المجموعة a.personnes.
@Entity PersonneActivite هي كما يلي:
@Entity
// جدول الربط
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {
@Embeddable
public static class Id implements Serializable {
// مكونات المفتاح المركب
// يشير إلى شخص
@Column(name = "PERSONNE_ID")
private Long personneId;
// يشير إلى نشاط
@Column(name = "ACTIVITE_ID")
private Long activiteId;
// المنشئون
...
// مُستردات ومُعيّنات
...
// toString
public String toString() {
return String.format("[%d,%d]", getPersonneId(), getActiviteId());
}
}
// حقول الفئة Personne_Activite
// مفتاح مركب
@EmbeddedId
private Id id = new Id();
// العلاقة الرئيسية PersonneActivite (متعدد) -> شخص (واحد)
// مُنفَّذة بواسطة المفتاح الأجنبي: personneId (PersonneActivite (العديد) -> شخص (واحد)
// personneId هو في الوقت نفسه عنصر من المفتاح الأساسي المركب
// يجب ألا يدير JPA هذا المفتاح الأجنبي (insertable = false, updatable = false) لأن التطبيق نفسه يقوم بذلك في منشئه
@ManyToOne
@JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
private Personne personne;
// العلاقة الرئيسية PersonneActivite -> النشاط
// مُنفَّذة بواسطة المفتاح الأجنبي: activiteId (PersonneActivite (many) -> نشاط (one)
// activiteId هو في الوقت نفسه عنصر من المفتاح الأساسي المركب
// يجب ألا يدير JPA هذا المفتاح الأجنبي (insertable = false, updatable = false) لأن التطبيق نفسه يقوم بذلك في منشئه
@ManyToOne()
@JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
private Activite activite;
// منشئات
public PersonneActivite() {
}
public PersonneActivite(Personne p, Activite a) {
// يتم تحديد المفاتيح الخارجية بواسطة التطبيق
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// الارتباطات ثنائية الاتجاه
this.setPersonne(p);
this.setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
// مُستردات ومُعيّنات
...
// toString
public String toString() {
return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
}
}
هذه الفئة أكثر تعقيدًا من الفئات السابقة.
- تحتوي الجدول [personne_activite] على صفوف بالشكل [p,a] حيث p هو المفتاح الأساسي لشخص و a هو المفتاح الأساسي لنشاط. يجب أن تحتوي كل جدول على مفتاح أساسي، ولا يستثنى من هذه القاعدة [personne_activite]. حتى الآن، كنا قد حددنا مفاتيح أساسية يتم إنشاؤها ديناميكيًا بواسطة SGBD. يمكننا القيام بذلك هنا أيضًا. سنستخدم تقنية أخرى، وهي تلك التي يحدد فيها التطبيق بنفسه قيم المفتاح الأساسي للجدول. هنا، يشير السطر [p1,a1] إلى أن شخصًا ما p1 يمارس النشاط a1. لا يمكن العثور على هذا السطر نفسه مرة ثانية في الجدول. وبالتالي، فإن الزوج (p,a) هو مرشح جيد ليكون المفتاح الأساسي. نسمي هذا المفتاح الأساسي المركب.
- السطران 30-31: المفتاح الأساسي المركب. التعليق التوضيحي @EmbeddedId (عادةً ما يكون @Id) مشابه للتدوين @Embedded المطبق على الحقل Adresse لشخص ما. في الحالة الأخيرة، كان هذا يعني أن الحقل Adresse كان موضوعًا لفئة خارجية ولكن كان يجب إدراجه في نفس الجدول الخاص بالشخص. هنا المعنى هو نفسه، إلا أنه للإشارة إلى أننا نتعامل مع المفتاح الأساسي، يصبح الترميز @EmbeddedId.
- السطر 31: يتم إنشاء كائن فارغ يمثل المفتاح الأساسي id فور إنشاء الكائن [PersonneActivite]. يتم تعريف الفئة التي تمثل المفتاح الأساسي في الأسطر 7-26، كفئة عامة ثابتة داخلية في فئة [PersonneActivite]. ويفرض Hibernate أن تكون هذه الفئة عامة وثابتة. إذا استبدلنا public static بـ private,، تحدث استثناء ونرى في رسالة الخطأ المرتبطة أن Hibernate حاول تنفيذ الأمر new PersonneActivite$Id. لذلك يجب أن تكون الفئة Id ثابتة وعامة في آن واحد.
- السطر 6: تم إعلان فئة Id للمفتاح الأساسي على أنها @Embeddable. نتذكر أن المفتاح الأساسي id في السطر 31 قد تم إعلانه على أنه @EmbeddedId. لذا يجب أن تحتوي الفئة المقابلة على التعليق التوضيحي @Embeddable.
- لقد ذكرنا أن المفتاح الأساسي للجدول [personne_activite] يتكون من الزوج (p,a) حيث p هو المفتاح الأساسي لشخص ما و a هو المفتاح الأساسي لنشاط ما. نجد العنصرين (p,a) للمفتاح المركب في السطر 11 (personneId) والسطر 15 (activiteId). الأعمدة المرتبطة بهذين الحقلين هي: PERSONNE_ID للشخص، و ACTIVITE_ID للنشاط.
- السطر 31: تم تعريف المفتاح الأساسي مع عموديه (PERSONNE_ID، ACTIVITE_ID). لا توجد أعمدة أخرى في الجدول [personne_activite]. لم يتبق سوى تعريف العلاقات الموجودة بين @Entity PersonneActivite التي نصفها حاليًا و@Entity الأخرى في المخطط العلائقي. تعكس هذه العلاقات قيود المفاتيح الخارجية التي تربط الجدول [personne_activite] بالجداول الأخرى.
- الأسطر 33-39: تحدد المفتاح الأجنبي الذي تمتلكه الجدول [personne_activite] على الجدول [personne]
- السطر 37: العلاقة من النوع @ManyToOne: يتم الإشارة إلى سطر واحد (One) من الجدول [personne] من قبل عدة (Many) أسطر من الجدول [personne_activite].
- السطر 38: يتم تسمية عمود المفتاح الأجنبي. يتم استخدام نفس الاسم الذي أُعطِي لمكون "personne" في المفتاح الأجنبي (السطر 10). وتوجد السمات insertable=false و updatable=false لمنع Hibernate من إدارة المفتاح الأجنبي. فهذا المفتاح هو في الواقع أحد مكونات مفتاح أساسي يتم حسابه بواسطة التطبيق، ولا يجب أن يتدخل Hibernate في ذلك.
- السطور 41-47: تحدد المفتاح الأجنبي الذي تمتلكه الجدول [personne_activite] في الجدول [activite]. التفسيرات هي نفسها التي تم تقديمها سابقًا.
- الأسطر 54-63: منشئ كائن PersonneActivite من شخص p ونشاط a. نتذكر أنه عند إنشاء كائن PersonneActivite، كانت المفتاح الأساسي id في السطر 31 تشير إلى كائن Id فارغ. تحدد السطران 56-57 قيمة لكل حقل (personneId، activiteId) في الكائن Id. هذه القيم هي على التوالي المفاتيح الأساسية للشخص p والنشاط a التي تم تمريرها كمعلمات لمُنشئ الكائن. وبالتالي، أصبح للمفتاح الأساسي id (السطر 31) قيمة الآن.
- السطر 59: يتلقى الحقل personne في السطر 39 القيمة p
- السطر 60: يتلقى الحقل activite في السطر 47 القيمة a
- يتم الآن إنشاء كائن [PersonneActivite] وتهيئته. يتم تحديث العلاقات العكسية التي تربط @Entity Personne (السطر 61) و Activite (السطر 62) بـ @Entity PersonneActivite الذي تم إنشاؤه للتو.
لقد انتهينا من وصف كيانات قاعدة البيانات. نحن في موقف معقد ولكنه للأسف شائع. سنرى أن هناك تكوينًا آخر ممكنًا للطبقة JPA يخفي جزءًا من هذا التعقيد: تصبح جدول الربط ضمنيًا، ويتم إنشاؤه وإدارته بواسطة الطبقة JPA. لقد اخترنا هنا الحل الأكثر تعقيدًا ولكنه يسمح للمخطط العلائقي بالتطور. وبذلك يسمح بإضافة أعمدة إلى جدول الربط، وهو ما لا تسمح به التهيئة التي لا يكون فيها جدول الربط كيانًا صريحًا (@Entity). تنصح [ref1] بالحل الذي ندرسه حاليًا. تم العثور في [ref1] على المعلومات التي سمحت بوضع هذا الحل.
2.5.3. مشروع Eclipse / Hibernate
التنفيذ JPA المستخدم هنا هو تنفيذ Hibernate. مشروع Eclipse للاختبارات هو التالي:

في [1]، مشروع Eclipse، وفي [2] أكواد Java. المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.5.4. إنشاء ملف DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لـ SGBD MySQL5 هو التالي:
alter table jpa07_hb_personne
drop
foreign key FKB5C817D45FE379D0;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B06CD852024;
alter table jpa07_hb_personne_activite
drop
foreign key FKD3E49B0668C7A284;
drop table if exists jpa07_hb_activite;
drop table if exists jpa07_hb_adresse;
drop table if exists jpa07_hb_personne;
drop table if exists jpa07_hb_personne_activite;
create table jpa07_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa07_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa07_hb_personne
add index FKB5C817D45FE379D0 (adresse_id),
add constraint FKB5C817D45FE379D0
foreign key (adresse_id)
references jpa07_hb_adresse (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B06CD852024 (ACTIVITE_ID),
add constraint FKD3E49B06CD852024
foreign key (ACTIVITE_ID)
references jpa07_hb_activite (id);
alter table jpa07_hb_personne_activite
add index FKD3E49B0668C7A284 (PERSONNE_ID),
add constraint FKD3E49B0668C7A284
foreign key (PERSONNE_ID)
references jpa07_hb_personne (id);
- السطور 21-26: الجدول [activite]
- الأسطر 28-39: الجدول [adresse]
- الأسطر 41-51: الجدول [personne]
- الأسطر 53-57: جدول الربط [personne_activite]. تجدر الإشارة إلى المفتاح المركب (السطر 56)
- الأسطر 59-63: المفتاح الأجنبي من الجدول [personne] إلى الجدول [adresse]
- الأسطر 65-69: المفتاح الأجنبي من الجدول [personne_activite] إلى الجدول [activite]
- السطور 71-75: المفتاح الخارجي للجدول [personne_activite] إلى الجدول [personne]
2.5.5. InitDB
رمز [InitDB] هو التالي:
package tests;
...
public class InitDB {
// الثوابت
private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";
private final static String TABLE_PERSONNE = "jpa07_hb_personne";
private final static String TABLE_ACTIVITE = "jpa07_hb_activite";
private final static String TABLE_ADRESSE = "jpa07_hb_adresse";
public static void main(String[] args) throws ParseException {
// سياق الاستمرارية
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
EntityManager em = null;
// يتم استرداد EntityManager من EntityManagerFactory
// السابق
em = emf.createEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// الطلب
Query sql1;
// حذف العناصر من الجدول PERSONNE_ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
sql1.executeUpdate();
// حذف العناصر من الجدول PERSONNE
sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
sql1.executeUpdate();
// حذف عناصر الجدول ACTIVITE
sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
sql1.executeUpdate();
// حذف عناصر الجدول ADRESSE
sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
sql1.executeUpdate();
// إنشاء الأنشطة
Activite act1 = new Activite();
act1.setNom("act1");
Activite act2 = new Activite();
act2.setNom("act2");
Activite act3 = new Activite();
act3.setNom("act3");
// استمرار الأنشطة
em.persist(act1);
em.persist(act2);
em.persist(act3);
// إنشاء أشخاص
Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
// إنشاء عناوين
Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
// الربط بين الأشخاص <--> العناوين
p1.setAdresse(adr1);
adr1.setPersonne(p1);
p2.setAdresse(adr2);
adr2.setPersonne(p2);
p3.setAdresse(adr3);
adr3.setPersonne(p3);
// استمرارية الأشخاص وبالتالي العناوين المرتبطة بهم
em.persist(p1);
em.persist(p2);
em.persist(p3);
// استمرارية العنوان a4 غير المرتبط بشخص
em.persist(adr4);
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// عرض العناوين
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
// الارتباطات بين الشخص <--> النشاط
PersonneActivite p1act1 = new PersonneActivite(p1, act1);
PersonneActivite p1act2 = new PersonneActivite(p1, act2);
PersonneActivite p2act1 = new PersonneActivite(p2, act1);
PersonneActivite p2act3 = new PersonneActivite(p2, act3);
// استمرار الارتباطات بين الشخص <--> النشاط
em.persist(p1act1);
em.persist(p1act2);
em.persist(p2act1);
em.persist(p2act3);
// عرض الأشخاص
System.out.println("[personnes]");
for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
System.out.println(p);
}
// عرض العناوين
System.out.println("[adresses]");
for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
System.out.println(a);
}
System.out.println("[activites]");
for (Object a : em.createQuery("select a from Activite a").getResultList()) {
System.out.println(a);
}
System.out.println("[personnes/activites]");
for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
System.out.println(pa);
}
// نهاية المعاملة
tx.commit();
// نهاية EntityManager
em.close();
// نهاية EntityManagerFactory
emf.close();
// سجل
System.out.println("terminé...");
}
}
- السطور 27-38: يتم إفراغ الجداول [personne_activite] و [personne] و [adresse] و [activite]. تجدر الإشارة إلى أنه يجب البدء بالجداول التي تحتوي على مفاتيح خارجية.
- الأسطر 40-45: يتم إنشاء ثلاث أنشطة act1 و act2 و act3
- الأسطر 47-49: يتم وضعها في سياق الاستمرارية.
- الأسطر 51-53: يتم إنشاء ثلاثة أشخاص p1 و p2 و p3.
- الأسطر 55-58: يتم إنشاء أربعة عناوين من adr1 إلى adr4.
- الأسطر 60-65: يتم ربط العناوين adri بالأشخاص pi. هناك عمليتان يجب القيام بهما في كل مرة لأن العلاقة بين الشخص والعنوان ثنائية الاتجاه.
- الأسطر 67-69: يتم وضع الأشخاص من p1 إلى p3 في سياق الاستمرارية. وبسبب التسلسل الشخص -> العنوان، سيكون هذا هو الحال أيضًا بالنسبة للعناوين من adr1 إلى adr3.
- السطر 71: يتم وضع العنوان الرابع adr4 غير المرتبط بشخص ما بشكل صريح في سياق الاستمرارية.
- الأسطر 73-85: يتم استدعاء سياق الاستمرارية للحصول على قوائم الكيانات من النوع [Personne] و [Adresse] و [Activite]. من المعروف أن هذه الطلبات ستؤدي إلى مزامنة السياق مع قاعدة البيانات: سيتم إدراج الكيانات التي تم إنشاؤها في قاعدة البيانات وستحصل على مفتاحها الأساسي. من المهم فهم ذلك لما سيأتي لاحقًا.
- السطور 87-90: يتم إنشاء 4 ارتباطات بين الشخص <-> النشاط. يشير اسم كل ارتباط إلى الشخص المرتبط بالنشاط. ربما نتذكر أن المفتاح الأساسي للكيان PersonneActivite هو مفتاح مركب يتكون من المفتاح الأساسي لشخص والمفتاح الأساسي لنشاط. ولذلك، فإن هذه العملية ممكنة لأن الكيانين Personne و Activite حصلا على مفاتيحهما الأولية خلال عملية مزامنة سابقة.
- الأسطر 92-95: يتم وضع هذه الارتباطات الأربعة في سياق الاستمرارية.
- السطور 87-86: يتم استدعاء سياق الاستمرارية للحصول على قوائم الكيانات من النوع [Personne] و [Adresse] و [Activite] و [PersonneActivite]. من المعروف أن هذه الطلبات ستؤدي إلى مزامنة السياق مع قاعدة البيانات: سيتم إدراج الكيانات PersonneActivite التي تم إنشاؤها في قاعدة البيانات.
يؤدي تنفيذ [InitDB] مع MySQL5 إلى عرض وحدة التحكم التالية:
قد يكون من المدهش أن نرى في السطور 15-16 أن الأشخاص p1 و p2 لديهم رقم إصدار يساوي 1، وأن الأمر نفسه ينطبق على السطور 24-26 بالنسبة للأنشطة الثلاثة. دعونا نحاول فهم ذلك.
في الأسطر 2-4، تكون أرقام إصدار الأشخاص هي 0، وفي الأسطر 11-13، تكون أرقام إصدار الأنشطة هي 0. تظهر العروض السابقة قبل إنشاء العلاقات بين الشخص <-> النشاط. في الأسطر 87-90 من كود Java، يتم إنشاء علاقات بين الأشخاص p1 و p2 والأنشطة act1، act2، act3. ويتم إنشاؤها باستخدام منشئ @Entity PersonneActivite (انظر الفقرة 2.5.2). ويظهر من قراءة كود هذا المنشئ أنه عندما يرتبط شخص p بنشاط a:
- يتم إضافة النشاط a إلى المجموعة p.activites
- يتم إضافة الشخص p إلى المجموعة a.personnes
وبالتالي، عند كتابة new PersonneActivite(p,a)، يتعرض الشخص p والنشاط a لتعديل في الذاكرة. عند الأسطر 97-113 من [InitDB]، تتم مزامنة سياق الاستمرارية مع قاعدة البيانات، JPA / يكتشف Hibernate أن العناصر الدائمة p1 و p2 و act1 و act2 و act3 قد تم تعديلها. يجب إجراء هذه التعديلات في قاعدة البيانات. يتم تسجيل هذه التعديلات في الواقع في جدول الربط [personne_activite]، لكن JPA / Hibernate يقوم مع ذلك بزيادة رقم إصدار كل عنصر من العناصر الدائمة التي تم تعديلها.
في منظور SQL Explorer، تكون النتائج كما يلي:
![]() |
- [2]: الجداول [jpa07_hb_*]
- [3]: جدول الأشخاص
- [4]: جدول العناوين.
- [5]: جدول الأنشطة
- [6]: جدول ربط الشخص <-> النشاط
2.5.6. Main
تسلسل الفئة [Main] للاختبارات التي نستعرضها باستثناء الاختبار 1 الذي يستخدم كود [InitDB] لتهيئة قاعدة البيانات.
2.5.6.1. Test2
هذا الاختبار هو التالي:
// حذف شخص p1
public static void test2() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف التبعيات على p1: غير ضروري لـ hibernate ولكن
// ضروري لـ TopLink
act1.getPersonnes().remove(p1act1);
act2.getPersonnes().remove(p1act2);
// حذف الشخص p1
em.remove(p1);
// نهاية المعاملة
tx.commit();
// يتم عرض الجداول الجديدة
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- السطر 4: نستخدم سياق الاستمرارية لـ test1، حيث الشخص p1 هو كائن في السياق.
- السطر 13: حذف الشخص p1. بسبب السمة:
- cascadeType.ALL على Adresse، سيتم حذف عنوان الشخص p1
- cascadeType.REMOVE على PersonneActivite، سيتم حذف الأنشطة الخاصة بالشخص p1.
- السطران 10-11: يتم حذف التبعيات التي ترتبط بها الكيانات الأخرى بالشخص p1 الذي سيتم حذفه في السطر 13. يتم ممارسة الأنشطة act1 و act2 بواسطة الشخص p1. تم إنشاء الروابط بواسطة منشئ الكيان PersonneActivite الذي يحمل الرمز التالي:
public PersonneActivite(Personne p, Activite a) {
// يتم تحديد المفاتيح الخارجية بواسطة التطبيق
getId().setPersonneId(p.getId());
getId().setActiviteId(a.getId());
// الارتباطات ثنائية الاتجاه
setPersonne(p);
setActivite(a);
p.getActivites().add(this);
a.getPersonnes().add(this);
}
السطر 9، تتلقى النشاط a عنصرًا إضافيًا من النوع PersonneActivite في مجموعتها personnes. هذا العنصر من النوع (p,a) للإشارة إلى أن الشخص p يمارس النشاط a. في test1 من [Main]، تم إنشاء رابطين (p1,act1) و(p1,act2). تقوم السطور 10 و 11 من test2 بإزالة هذه التبعيات. تجدر الإشارة إلى أن Hibernate يعمل دون إزالة هذه التبعيات على الكائن p1، ولكن Toplink لا يعمل.
- السطور 17-20: يتم عرض جميع الجداول
النتائج هي كما يلي:
- الشخص p1 الموجود في test1 (السطر 3) لم يعد موجودًا في نهاية test2 (السطور 22-23)
- العنوان adr1 للشخص p1 الموجود في test1 (السطر 11) لم يعد كذلك بعد test2 (السطور 29-31)
- الأنشطة (p1,act1) (السطر 16) و (p1,act2) (السطر 18) للشخص p1، الموجودة في test1 لم تعد موجودة بعد انتهاء test2 (السطور 33-34)
2.5.6.2. Test3
هذا الاختبار هو التالي:
// حذف النشاط act1
public static void test3() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// إزالة التبعيات على act1: غير ضروري لـ Hibernate ولكن
// ضروري لـ TopLink
p2.getActivites().remove(p2act1);
// حذف نشاط act1
em.remove(act1);
// نهاية المعاملة
tx.commit();
// عرض الجداول الجديدة
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- السطر 4: يتم استخدام سياق الاستمرارية لـ test2
- السطر 12: حذف النشاط act1. بسبب السمة:
- cascadeType.REMOVE على PersonneActivite، سيتم حذف الأسطر (p, act1) من الجدول [personne_activite].
- السطر 10: قبل إخراج act1 من سياق الاستمرارية، يتم حذف التبعيات التي قد تكون لدى كيانات أخرى على هذا الكائن المستمر. بعد حذف الشخص p1 في الاختبار السابق، فإن الشخص p2 هو الوحيد الذي يمارس النشاط act1.
- الأسطر 13-16: يتم عرض جميع الجداول
النتائج هي كما يلي:
- في test2، النشاط act1 موجود (السطر 6). في test3، لم يعد موجودًا (السطور 21-22)
- في test2، يوجد الرابط (p2,act1) (السطر 14). في test3، لم يعد موجودًا (السطر 28)
2.5.6.3. Test4
هذا الاختبار هو التالي:
// استرداد أنشطة شخص ما
public static void test4() {
// سياق الاستمرارية
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// يتم استرداد الشخص p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
// يتم مسح أنشطته
for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
System.out.println(pa);
}
// نمر عبر العلاقة العكسية لـ p2
p2 = em.find(Personne.class, p2.getId());
System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
// يتم مسح أنشطته
for (PersonneActivite pa : p2.getActivites()) {
System.out.println(pa.getActivite().getNom());
}
// نهاية المعاملة
tx.commit();
}
- يعرض الاختبار 4 أنشطة الشخص p2.
- السطر 4: نبدأ من سياق جديد وفارغ
- الأسطر 12-14: نعرض أسماء الأنشطة التي يمارسها الشخص p2 باستخدام استعلام JPQL.
- تم إجراء ربط بين Activite (a) و PersonneActivite (pa) (ربط a.personnes)
- في صفوف هذا الربط (a,pa)، يتم عرض اسم النشاط (a.nom) للشخص p2 (pa.personne.nom='p2').
- السطور 16-21: يتم إجراء نفس العملية السابقة، ولكن بمساعدة العلاقة OneToMany p2.activites للشخص p2. سيتم إنشاء الاستعلام JPQL بواسطة JPA. ونرى هنا فائدة العلاقة العكسية OneToMany: فهي تتجنب الاستعلام JPQL.
والنتائج هي كما يلي:
2.5.6.4. Test5
هذا الاختبار هو التالي:
// استرداد الأشخاص الذين يقومون بنشاط معين
public static void test5() {
// سياق الاستمرارية
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// طلب أنشطة p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
System.out.println(pa);
}
// المرور عبر العلاقة العكسية لـ act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (PersonneActivite pa : act3.getPersonnes()) {
System.out.println(pa.getPersonne().getNom());
}
// نهاية المعاملة
tx.commit();
}
- يُظهر الاختبار 6 الأشخاص الذين يقومون بالنشاط act3. الإجراء مشابه لإجراء الاختبار 6. نترك للقارئ مهمة الربط بين الرمزين.
والنتائج هي كما يلي:
كان الهدف من الاختبارين 4 و 5 هو إثبات مرة أخرى أن العلاقة العكسية ليست ضرورية أبدًا ويمكن دائمًا استبدالها بطلب JPQL.
2.5.7. تنفيذ JPA / Toplink
نستخدم الآن تطبيق JPA / Toplink:
![]() |
مشروع Eclipse مع Toplink هو نسخة من مشروع Eclipse مع Hibernate:
![]() |
أكواد Java مطابقة لتلك الموجودة في مشروع Hibernate السابق باستثناء بعض التفاصيل التي سنذكرها لاحقًا. البيئة (المكتبات – persistence.xml – قاعدة البيانات – ملفات conf وddl – نصوص ant) هي تلك التي تمت دراستها في الفقرة 2.1.15.2. يوجد مشروع Eclipse [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
تم تعديل الملف <persistence.xml> [2] في نقطة واحدة، وهي الكيانات المعلنة:
<!-- فئات ثابتة -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
<class>entites.PersonneActivite</class>
- السطور 2-5: الكيانات الأربعة المدارة
يؤدي تنفيذ [InitDB] مع SGBD MySQL5 إلى النتائج التالية:
![]() |
في [1]، عرض وحدة التحكم، وفي [2]، الجداول [jpa07_tl] التي تم إنشاؤها، وفي [3] البرامج النصية SQL التي تم إنشاؤها. ومحتواها كما يلي:
create.sql
CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
يتم تنفيذ [InitDB] و [Main] دون أخطاء.
2.6. المثال 6: علاقة متعددة إلى متعددة مع جدول ربط ضمني
نستأنف المثال 4 ولكننا نعالجه الآن باستخدام جدول ربط ضمني تم إنشاؤه بواسطة الطبقة JPA نفسها.
2.6.1. مخطط قاعدة البيانات
![]() |
- في [1]، قاعدة البيانات MySQL5 - في [2]: الجدول [personne] – في [3]: الجدول المرتبط [adresse] – في [4]: الجدول [activite] للأنشطة – في [5]: جدول الربط [personne_activite] الذي يربط بين الأشخاص والأنشطة.
2.6.2. كائنات @Entity التي تمثل قاعدة البيانات
سيتم تمثيل الجداول السابقة بواسطة كائنات @Entity التالية:
- سيمثل @Entity Personne الجدول [personne]
- سيتم تمثيل @Entity Adresse بالجدول [adresse]
- ستمثل @Entity Activite الجدول [activite]
- لم تعد الجدول [personne_activite] ممثلة بواسطة @Entity
العلاقات بين هذه الكيانات هي كما يلي:
- تربط علاقة واحد إلى واحد الكيان Personne بالكيان Adresse: شخص p له عنوان a. الكيان Personne الذي يمتلك المفتاح الأجنبي سيكون له العلاقة الرئيسية، والكيان Adresse له العلاقة العكسية.
- تربط علاقة متعددة الأطراف الكيانين Personne و Activite: شخص ما يمارس عدة أنشطة، ونشاط ما يمارسه عدة أشخاص. سيتم تجسيد هذه العلاقة من خلال تعليق @ManyToMany في كل من الكيانين، حيث يُعلن أحدهما عكس الآخر.
الكيان @Entity Personne هو التالي:
@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver :@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
@Column(length = 30, nullable = false)
private String prenom;
@Column(nullable = false)
@Temporal(TemporalType.DATE)
private Date datenaissance;
@Column(nullable = false)
private boolean marie;
@Column(nullable = false)
private int nbenfants;
// العلاقة الرئيسية Personne (واحدة) -> Adresse (واحدة)
// تم تنفيذها بواسطة المفتاح الأجنبي الشخص (adresse_id) -> العنوان
// إدراج متسلسل شخص -> إدراج عنوان
// تسلسل تحديث شخص -> تحديث عنوان
// التسلسل التسلسلي حذف شخص -> حذف عنوان
// يجب أن يكون لكل شخص عنوان واحد (nullable=false)
// عنوان واحد ينتمي إلى شخص واحد فقط (فريد=صحيح)
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;
// علاقة شخص (متعدد) -> نشاط (متعدد) عبر جدول ربط personne_activite
// personne_activite(PERSONNE_ID) هو مفتاح خارجي على الشخص (id)
// personne_activite(ACTIVITE_ID) هو مفتاح فرعي لـ Activite(id)
// cascade=CascadeType.PERSIST : استمرار وجود شخص واحد يؤدي إلى استمرار أنشطته
@ManyToMany(cascade={CascadeType.PERSIST})
@JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
// المصنعون
public Personne() {
}
نقتصر في تعليقنا على العلاقة @ManyToMany في الأسطر 46-48 التي تربط الكيان @Entity Personne بالكيان @Entity Activite:
- السطر 48: الشخص لديه أنشطة. ويمثل الحقل activites هذه الأنشطة. في الإصدار السابق، كان نوع عناصر المجموعة activites هو PersonneActivite. أما هنا، فهو Activite. وبالتالي، يمكن الوصول مباشرة إلى أنشطة الشخص، بينما في الإصدار السابق كان لا بد من المرور عبر الكيان الوسيط PersonneActivite.
- السطر 46: العلاقة التي تربط @Entity Personne التي ندرسها بـ @Entity Activite من المجموعة activites في السطر 48 هي من النوع متعدد-إلى-متعدد (ManyToMany):
- شخص واحد (One) يمارس عدة أنشطة (Many)
- نشاط واحد (One) يمارسه عدة أشخاص (Many)
- في النهاية، ترتبط الكيانات @Entity Personne و Activite بعلاقة ManyToMany. كما هو الحال في العلاقة OneToOne، هناك تناسق بين الكيانات في هذه العلاقة. يمكننا اختيار @Entity التي ستحتوي على العلاقة الرئيسية وتلك التي ستحتوي على العلاقة العكسية بحرية. هنا، نقرر أن @Entity Personne ستحتوي على العلاقة الرئيسية.
- كما رأينا في المثال السابق، تتطلب العلاقة @ManyToMany جدول ربط. بينما كنا قد حددناها سابقًا باستخدام @Entity، يتم تحديد جدول الربط هنا باستخدام التعليق التوضيحي @JoinTable في السطر 47.
- يُسمي السمة name الجدول.
- تتكون جدول الربط من مفاتيح أجنبية في الجداول التي يربطها. هنا، هناك مفتاحان أجنبيان: أحدهما في الجدول [personne]، والآخر في الجدول [activite]. يتم تعريف أعمدة المفاتيح الأجنبية هذه بواسطة السمتين joinColumns و inverseJoinColumns.
- يحدد التعليق التوضيحي @JoinColumn للسمة joinColumns المفتاح الأجنبي في جدول الكيان @Entity الذي يحتوي على العلاقة الرئيسية @ManyToMany، وهو هنا الجدول [personne]. سيُسمى عمود المفتاح الأجنبي هذا PERSONNE_ID.
- يحدد التعليق التوضيحي @JoinColumn للسمة inverseJoinColumns المفتاح الأجنبي في جدول @Entity الذي يحتوي على العلاقة العكسية @ManyToMany، وهو هنا الجدول [activite]. سيُسمى عمود المفتاح الأجنبي هذا ACTIVITE_ID.
و@Entity Adresse هي كما يلي:
@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {
// الحقول
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false)
private String adr1;
@Column(length = 30)
private String adr2;
@Column(length = 30)
private String adr3;
@Column(length = 5, nullable = false)
private String codePostal;
@Column(length = 20, nullable = false)
private String ville;
@Column(length = 3)
private String cedex;
@Column(length = 20, nullable = false)
private String pays;
@OneToOne(mappedBy = "adresse")
private Personne personne;
- السطران 28-29: العلاقة @OneToOne هي العكس للعلاقة @OneToOne التي تشير إلى @Entity Personne (السطران 37-38 من Personne).
الكيان @Entity Activite هو التالي
@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {
// الحقول
@Id()
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.AUTO)
// toplink sqlserver : @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
@Version
private int version;
@Column(length = 30, nullable = false, unique = true)
private String nom;
// العلاقة العكسية نشاط -> شخص
@ManyToMany(mappedBy = "activites")
private Set<Personne> personnes = new HashSet<Personne>();
...
- السطران 20-21: العلاقة متعددة-إلى-متعددة التي تربط @Entity Activite بـ @Entity Personne. وقد تم تعريف هذه العلاقة مسبقًا في @Entity Personne. لذلك نكتفي هنا بالقول إن العلاقة هي عكس (mappedBy) العلاقة @ManyToMany الموجودة في الحقل activites (mappedBy= "activites ") لـ @Entity Personne.
- تجدر الإشارة إلى أن العلاقة العكسية هي دائماً اختيارية. هنا، نستخدمها للحصول على الأشخاص الذين يمارسون النشاط الحالي. ومجموعة Set<Personne> personnes هي التي ستسمح بالحصول على هؤلاء الأشخاص. لم يتم تحديد طريقة تحميل التبعيات Personne لـ @Entity Activite. لم نحددها أيضًا في المثال السابق. بشكل افتراضي، تكون هذه الطريقة هي fetch=FetchType.LAZY.
لقد انتهينا من وصف كيانات قاعدة البيانات. كان الأمر أبسط مما لو كانت جدول الربط [personne_activite] عبارة عن جدول صريح. قد تنطوي هذه الحلول الأبسط على عيوب مع مرور الوقت: فهي لا تسمح بإضافة أعمدة إلى جدول الربط. ومع ذلك، قد يكون ذلك ضروريًا لتلبية احتياجات جديدة، على سبيل المثال إضافة عمود إلى الجدول [personne_activite] يشير إلى تاريخ تسجيل الشخص في النشاط.
2.6.3. مشروع Eclipse / Hibernate
التنفيذ JPA المستخدم هنا هو تنفيذ Hibernate. مشروع Eclipse للاختبارات هو التالي:
![]() |
في [1]، مشروع Eclipse، وفي [2] أكواد Java. المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.6.4. إنشاء ملف DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لـ SGBD MySQL5 هو التالي:
alter table jpa08_hb_personne
drop
foreign key FKA44B1E555FE379D0;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A5CD852024;
alter table jpa08_hb_personne_activite
drop
foreign key FK5A6A55A568C7A284;
drop table if exists jpa08_hb_activite;
drop table if exists jpa08_hb_adresse;
drop table if exists jpa08_hb_personne;
drop table if exists jpa08_hb_personne_activite;
create table jpa08_hb_activite (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_adresse (
id bigint not null auto_increment,
version integer not null,
adr1 varchar(30) not null,
adr2 varchar(30),
adr3 varchar(30),
codePostal varchar(5) not null,
ville varchar(20) not null,
cedex varchar(3),
pays varchar(20) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne (
id bigint not null auto_increment,
version integer not null,
nom varchar(30) not null unique,
prenom varchar(30) not null,
datenaissance date not null,
marie bit not null,
nbenfants integer not null,
adresse_id bigint not null unique,
primary key (id)
) ENGINE=InnoDB;
create table jpa08_hb_personne_activite (
PERSONNE_ID bigint not null,
ACTIVITE_ID bigint not null,
primary key (PERSONNE_ID, ACTIVITE_ID)
) ENGINE=InnoDB;
alter table jpa08_hb_personne
add index FKA44B1E555FE379D0 (adresse_id),
add constraint FKA44B1E555FE379D0
foreign key (adresse_id)
references jpa08_hb_adresse (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A5CD852024 (ACTIVITE_ID),
add constraint FK5A6A55A5CD852024
foreign key (ACTIVITE_ID)
references jpa08_hb_activite (id);
alter table jpa08_hb_personne_activite
add index FK5A6A55A568C7A284 (PERSONNE_ID),
add constraint FK5A6A55A568C7A284
foreign key (PERSONNE_ID)
references jpa08_hb_personne (id);
هذا الرمز DDL مشابه للرمز الذي تم الحصول عليه باستخدام جدول الربط الصريح ويتوافق مع المخطط المقدم سابقًا:
![]() |
2.6.5. InitDB
لن نعلق كثيرًا على الفئة [InitDB] التي تتطابق مع نسختها السابقة وتعطي نفس النتائج. لنركز فقط على الكود التالي الذي يعرض الارتباط Personne <-> Activite:
// عرض الأشخاص/الأنشطة
System.out.println("[personnes/activites]");
Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
- السطر 3: الأمر JPQL الذي يقوم بالربط. تعيد نتيجة select معرّفات الكيانات Personne و Activite المرتبطة ببعضها البعض عبر جدول الربط. تتكون القائمة التي يعرضها select من أسطر تحتوي على كائنين من النوع Long. لتصفح هذه القائمة، يطلب السطر 3 كائن Iterator من القائمة.
- السطور 4-7: باستخدام الكائن من النوع Iterator السابق، يتم تصفح القائمة.
- السطر 5: كل عنصر في القائمة هو مصفوفة تحتوي على سطر ناتج عن select
- السطر 6: يتم استرداد عناصر السطر الحالي الناتج عن select بإجراء التغييرات المناسبة على الأنواع.
نتيجة [InitDB] هي كما يلي:
2.6.6. Main
تسلسل الفئة [Main] للاختبارات التي نستعرض بعضها.
2.6.6.1. Test3
هذا الاختبار هو التالي:
// حذف النشاط act1
public static void test3() {
// سياق الاستمرارية
EntityManager em = getEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف النشاط act1 من p2
p2.getActivites().remove(act1);
// إزالة act1 من سياق الاستمرارية
em.remove(act1);
// نهاية المعاملات
tx.commit();
// عرض الجداول الجديدة
dumpPersonne();
dumpActivite();
dumpAdresse();
dumpPersonne_Activite();
}
- السطر 11: يتم سحب النشاط act1 من سياق الاستمرارية
- السطر 9: النشاط act1 هو جزء من أنشطة الشخص الوحيد المتبقي في السياق، وهو الشخص p2. السطر 9 يزيل النشاط act1 من أنشطة الشخص p2. نقوم بذلك للحفاظ على اتساق سياق الاستمرارية لأننا نحتفظ به لما بعد ذلك.
والنتائج هي كما يلي:
- النشاط act1 الموجود في السطر 26 في test2 قد اختفى من أنشطة test3 (السطران 40-41)
- الشخص p2 كان لديه في test2 النشاط act1 (السطر 33). عند انتهاء test3، لم يعد لديه (السطر 47)
2.6.6.2. Test6
هذا الاختبار هو التالي:
// تعديل أنشطة شخص ما
public static void test6() {
// سياق الاستمرارية
EntityManager em = getNewEntityManager();
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// استرداد الشخص p2
p2 = em.find(Personne.class, p2.getId());
// استرداد النشاط act2
act2 = em.find(Activite.class, act2.getId());
// p2 لم يعد يمارس سوى النشاط act2
p2.getActivites().clear();
p2.getActivites().add(act2);
// نهاية المعاملة
tx.commit();
// يتم عرض الجداول الجديدة
dumpPersonne();
dumpActivite();
dumpPersonne_Activite();
}
- السطر 4: يتم استخدام سياق استمرارية جديد وفارغ
- السطر 9: يتم جلب الشخص p2 من قاعدة البيانات إلى سياق الاستمرارية
- السطر 11: يتم جلب النشاط act2 من قاعدة البيانات إلى سياق الاستمرارية
- السطر 13: يتم نقل أنشطة الشخص p2 (act3) من قاعدة البيانات إلى السياق (fetchType.LAZY). إن استدعاء [getActivites] هو الذي يتسبب في هذا التحميل. يتم حذف أنشطة p2. لا يتعلق الأمر بحذف فعلي للأنشطة (remove) بل بتعديل حالة الشخص p2. لم يعد يمارس أي أنشطة.
- السطر 14: نضيف إلى الشخص p2 النشاط act2. في النهاية، مجموعة الأنشطة الجديدة للشخص p2 هي المجموعة {act2}.
- السطر 16: نهاية المعاملة. ستقوم المزامنة بمراجعة كائنات السياق (p2، act2، act3) وستكتشف أن حالة p2 قد تغيرت. سيتم تنفيذ الأوامر SQL التي تنقل هذا التغيير إلى قاعدة البيانات.
- الأسطر 18-20: يتم عرض جميع الجداول
النتائج هي كما يلي:
- في نهاية الاختبار 4، كان الشخص p2 يمارس النشاط act3 (السطر 3).
- في نهاية الاختبار 6 (السطر 19)، لم يعد الشخص p2 يمارس النشاط act3 (السطر 3) وأصبح يمارس النشاط act2.
2.6.7. التنفيذ JPA / Toplink
نستخدم الآن تطبيق JPA / Toplink:
![]() |
مشروع Eclipse مع Toplink هو نسخة من مشروع Eclipse مع Hibernate:
![]() |
تم تعديل الملف <persistence.xml> [2] في نقطة واحدة، وهي الكيانات المعلنة:
<!-- المزود -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- الفئات الدائمة -->
<class>entites.Activite</class>
<class>entites.Adresse</class>
<class>entites.Personne</class>
...
- الأسطر 4-6: الكيانات المدارة
يؤدي تنفيذ [InitDB] مع SGBD MySQL5 إلى النتائج التالية:
![]() |
في [1]، عرض وحدة التحكم، وفي [2]، الجداول [jpa07_tl] التي تم إنشاؤها، وفي [3] البرامج النصية SQL التي تم إنشاؤها. ومحتواها كما يلي:
create.sql
CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
يتم تنفيذ [InitDB] و [Main] دون أخطاء.
2.6.8. مشروع Eclipse / Hibernate 2
نقوم بإنشاء مشروع Eclipse مستمد من المشروع السابق عن طريق النسخ:
![]() |
في [1]، مشروع Eclipse، وفي [2] أكواد Java. المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
نقوم بتعديل العلاقة التي تربط Personne بـ Activité بالطريقة التالية:
Personne
// علاقة شخص (متعدد) -> نشاط (متعدد) عبر جدول ربط personne_activite
// personne_activite(PERSONNE_ID) هو مفتاح خارجي على Personne(id)
// personne_activite(ACTIVITE_ID) هو مفتاح خارجي على النشاط (id)
// المزيد من التسلسل على الأنشطة
// @ManyToMany(cascade={CascadeType.PERSIST})
@ManyToMany()
@JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
- السطر 6: العلاقة الرئيسية @ManyToMany لم تعد تحتوي على تسلسل استمرارية شخص -> نشاط (انظر الإصدار القديم السطر 5)
النشاط
// لا توجد علاقة عكسية مع Personne
// @ManyToMany(mappedBy = "activites")
// private Set<Personne> personnes = new HashSet<Personne>();
- السطران 2-3: تم حذف العلاقة العكسية @ManyToMany نشاط -> شخص
نسعى إلى إثبات أن السمات المحذوفة (التسلسل والعلاقة العكسية) ليست ضرورية. التغيير الأول الذي أحدثته هذه التهيئة الجديدة موجود في [InitDB]:
// الارتباطات الأشخاص <--> الأنشطة
p1.getActivites().add(act1);
p1.getActivites().add(act2);
p2.getActivites().add(act1);
p2.getActivites().add(act3);
// استمرارية الأنشطة
em.persist(act1);
em.persist(act2);
em.persist(act3);
// استمرارية الأشخاص
em.persist(p1);
em.persist(p2);
em.persist(p3);
// والعنوان a4 غير المرتبط بشخص
em.persist(adr4);
- الأسطر 7-9: نحن مضطرون إلى وضع الأنشطة act1 إلى act3 بشكل صريح في سياق الاستمرارية. عندما كانت سلسلة الاستمرارية Personne -> نشاط، كانت الأسطر 11-13 تحافظ على كل من الأشخاص من p1 إلى p3 وأنشطة هؤلاء الأشخاص من act1 إلى act3.
يظهر تغيير ثانٍ في [Main]:
// استرجاع الأشخاص الذين يقومون بنشاط معين
public static void test5() {
// سياق الاستمرارية
EntityManager em = getNewEntityManager();
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
// يُطلب نشاط p2
for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
System.out.println(pa);
}
// نهاية المعاملة
tx.commit();
}
- السطور 9-12: الاستعلام JPQL الذي يحصل على الأشخاص الذين يمارسون النشاط act3
- في الإصدار السابق، تم الحصول على نفس النتيجة أيضًا عبر العلاقة العكسية Activite -> Personne التي تم حذفها الآن:
// المرور عبر العلاقة العكسية لـ act3
System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
act3 = em.find(Activite.class, act3.getId());
for (Personne p : act3.getPersonnes()) {
System.out.println(p.getNom());
}
2.6.9. مشروع Eclipse / Toplink 2
نقوم بإنشاء مشروع Eclipse مستمد من مشروع Eclipse / Toplink السابق عن طريق النسخ:
![]() |
في [1]، مشروع Eclipse، وفي [2] أكواد Java. المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
أكواد Java مطابقة لتلك الموجودة في إصدار Hibernate.
2.7. المثال 7: استخدام الاستعلامات المسماة
نختتم هذا العرض الطويل للكيانات JPA الذي بدأناه في الفقرة 2 بمثال أخير يوضح استخدام الاستعلامات JPQL الخارجية في ملف التكوين. هذا المثال مستمد من المصدر التالي:
[ref2]: "Getting started With JPA in Spring 2.0" لمارك فيشر على الرابط
[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].
2.7.1. قاعدة البيانات النموذجية
قاعدة البيانات هي التالية:
![]() |
- في [1]: قائمة بالمطاعم مع أسمائها وعناوينها
- في [2]: جدول عناوين المطاعم، يقتصر على رقم الشارع واسم الشارع. هناك علاقة واحد إلى واحد بين الجدولين restaurant و adresse: لكل مطعم عنوان واحد فقط.
- في [3]: جدول الأطباق مع أسمائها ومؤشر صحيح/خطأ للإشارة إلى ما إذا كان الطبق نباتيًا أم لا
- في [4]: جدول ربط المطاعم/الأطباق: يقدم المطعم عدة أطباق ويمكن تقديم الطبق نفسه في عدة مطاعم. لدينا علاقة متعددة إلى متعددة بين الجدولين restaurant و plat.
2.7.2. كائنات @Entity التي تمثل قاعدة البيانات
سيتم تمثيل الجداول السابقة بواسطة كائنات @Entity التالية:
- سيمثل @Entity Restaurant الجدول [restaurant]
- ستمثل @Entity Adresse الجدول [adresse]
- ستمثل @Entity Plat الجدول [plat]
العلاقات بين هذه الكيانات هي كما يلي:
- تربط علاقة واحد إلى واحد الكيان Restaurant بالكيان Adresse: مطعم r له عنوان a. الكيان Restaurant الذي يمتلك المفتاح الأجنبي سيكون له العلاقة الرئيسية. الكيان Adresse لن يكون له علاقة عكسية.
- تربط علاقة متعددة إلى متعددة الكيانين Restaurant و Plat: يقدم مطعم واحد عدة أطباق ويمكن تقديم الطبق نفسه في عدة مطاعم. سيتم تجسيد هذه العلاقة من خلال تعليق @ManyToMany في الكيان Restaurant. لن يكون للكيان Plat علاقة عكسية.
الكيان @Entity Restaurant هو التالي:
package entites;
...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique = true, length = 30, nullable = false)
private String nom;
@OneToOne(cascade = CascadeType.ALL)
private Adresse adresse;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
@JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
private Set<Plat> plats = new HashSet<Plat>();
// المنشئات
public Restaurant() {
}
public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
}
// المُستردات والمُعيّنات
...
// toString
public String toString() {
String signature = "R[" + getNom() + "," + getAdresse();
for (Plat e : getPlats()) {
signature += "," + e;
}
return signature + "]";
}
}
- السطر 17: العلاقة واحد إلى واحد التي تربط الكيان Restaurant بالكيان Adresse. يتم ترحيل جميع عمليات الاستمرارية الخاصة بمطعم ما إلى عنوانه.
- السطر 20: العلاقة التي تربط @Entity Restaurant بـ @Entity Plat من المجموعة plats في السطر 22 هي من النوع متعدد-إلى-متعدد (ManyToMany):
- مطعم واحد (One) لديه العديد من الأطباق (Many)
- طبق واحد (One) يمكن تقديمه في عدة مطاعم (Many)
- في النهاية، ترتبط الكيانات @Entity Restaurant و Plat بعلاقة ManyToMany. نقرر أن @Entity Restaurant سيكون له العلاقة الرئيسية وأن @Entity Plat لن يكون له علاقة عكسية.
- تتطلب العلاقة @ManyToMany جدول ربط. ويتم تعريف هذا الجدول باستخدام التعليق التوضيحي @JoinTable في السطر 47.
- يُسمي السمة name الجدول.
- تتكون جدول الربط من المفاتيح الخارجية في الجداول التي يربطها. هنا، هناك مفتاحان أجنبيان: أحدهما في الجدول [restaurant]، والآخر في الجدول [plat]. يتم تعريف أعمدة المفاتيح الأجنبية هذه بواسطة السمتين joinColumns و inverseJoinColumns.
- يحدد السمة joinColumns المفتاح الأجنبي في جدول @Entity الذي يحتوي على العلاقة الرئيسية @ManyToMany، وهو هنا الجدول [restaurant]. السمة joinColumns غير موجودة هنا. يحتوي JPA على قيمة افتراضية في هذه الحالة: [table]_[clé_primaire_de_table]، وهنا [jpa10_hb_restaurant_id].
- يحدد التعليق التوضيحي @JoinColumn للسمة inverseJoinColumns المفتاح الأجنبي في جدول @Entity الذي يحتوي على العلاقة العكسية @ManyToMany، وهو هنا الجدول [plat]. سيُسمى عمود المفتاح الأجنبي هذا plat_id.
و@Entity Adresse هي كما يلي:
package entites;
...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(name = "NUMERO_RUE")
private int numeroRue;
@Column(name = "NOM_RUE", length=30, nullable=false)
private String nomRue;
// مُستردات ومُعيّنات
...
// المنشئات
public Adresse(int streetNumber, String streetName){
...
}
public Adresse(){
}
// toString
public String toString(){
return "A["+getNumeroRue()+","+getNomRue()+"]";
}
}
- @Entity Adresse هي كيان لا علاقة مباشرة له بالكيانات الأخرى. لا يمكن الاحتفاظ بها إلا من خلال كيان Restaurant.
- يتم تعريف العنوان بواسطة اسم الشارع (السطر 16) ورقم في الشارع (السطر 13).
الكيان @Entity Plat هو التالي
package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Column(unique=true, length=50, nullable=false)
private String nom;
private boolean vegetarien;
// منشئات
public Plat() {
}
public Plat(String name, boolean vegetarian) {
...
}
// الوصول والضبط
...
// toString
public String toString() {
return "E[" + getNom() + "," + isVegetarien() + "]";
}
}
- الكيان @Entity Plat هو كيان لا علاقة مباشرة له بالكيانات الأخرى. لا يمكن الاحتفاظ به إلا من خلال كيان Restaurant.
- يتم تعريف الطبق من خلال الاسم (السطر 12) ونوعه نباتي أم لا (السطر 14).
2.7.3. مشروع Eclipse / Hibernate
التنفيذ JPA المستخدم هنا هو تنفيذ Hibernate. مشروع Eclipse للاختبارات هو التالي:
![]() |
في [1]، مشروع Eclipse، وفي [2] أكواد Java وتكوين الطبقة JPA. يُلاحظ وجود ملف [orm.xml] لم يسبق رؤيته من قبل. المشروع موجود في [3] في مجلد الأمثلة [4]. سنقوم باستيراده.
2.7.4. إنشاء ملف DDL من قاعدة البيانات
باتباع التعليمات الواردة في الفقرة 2.1.7، فإن ملف DDL الذي تم الحصول عليه لملفي SGBD و MySQL5 هو التالي:
alter table jpa10_hb_restaurant
drop
foreign key FK3E8E4F5D5FE379D0;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D11F0F78A4;
alter table jpa10_hb_restaurant_plat
drop
foreign key FK1D2D06D1AFAC3E44;
drop table if exists jpa10_hb_adresse;
drop table if exists jpa10_hb_plat;
drop table if exists jpa10_hb_restaurant;
drop table if exists jpa10_hb_restaurant_plat;
create table jpa10_hb_adresse (
id bigint not null auto_increment,
NUMERO_RUE integer,
NOM_RUE varchar(30) not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_plat (
id bigint not null auto_increment,
nom varchar(50) not null unique,
vegetarien bit not null,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant (
id bigint not null auto_increment,
nom varchar(30) not null unique,
adresse_id bigint,
primary key (id)
) ENGINE=InnoDB;
create table jpa10_hb_restaurant_plat (
jpa10_hb_restaurant_id bigint not null,
plat_id bigint not null,
primary key (jpa10_hb_restaurant_id, plat_id)
) ENGINE=InnoDB;
alter table jpa10_hb_restaurant
add index FK3E8E4F5D5FE379D0 (adresse_id),
add constraint FK3E8E4F5D5FE379D0
foreign key (adresse_id)
references jpa10_hb_adresse (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D11F0F78A4 (plat_id),
add constraint FK1D2D06D11F0F78A4
foreign key (plat_id)
references jpa10_hb_plat (id);
alter table jpa10_hb_restaurant_plat
add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id),
add constraint FK1D2D06D1AFAC3E44
foreign key (jpa10_hb_restaurant_id)
references jpa10_hb_restaurant (id);
- السطور 21-26: الجدول [adresse]
- الأسطر 28-33: الجدول [plat]
- الأسطر 35-40: الجدول [restaurant]
- الأسطر 42-46: جدول الربط [restaurant_plat]. تجدر الإشارة إلى المفتاح المركب (السطر 45)
- الأسطر 48-52: المفتاح الخارجي للجدول [restaurant] إلى الجدول [adresse]
- الأسطر 54-58: المفتاح الأجنبي من الجدول [restaurant_plat] إلى الجدول [plat]
- السطور 60-64: المفتاح الخارجي للجدول [restaurant_plat] إلى الجدول [restaurant]
هذا DDL يتوافق مع المخطط المقدم سابقًا:
![]() |
في منظور SQL Explorer، تظهر قاعدة البيانات على النحو التالي:
![]() |
- في [1]: الجداول الأربعة للقاعدة
- في [2]: العناوين
- في [3]: الأطباق
- في [4]: المطاعم. يشير [adresse_id] إلى عناوين [2].
- في [5]: جدول الربط [restaurant,plat]. تشير [jpa10_hb_restaurant_id] إلى المطاعم في [4] و [plat_id] والأطباق في [3]. وبالتالي، فإن [1,1] تعني أن مطعم "Burger Barn" يقدم الطبق "CheeseBurger".
للحصول على البيانات المذكورة أعلاه، تم تشغيل البرنامج [QueryDB] من مشروع Eclipse.
2.7.5. استعلامات JPQL باستخدام وحدة تحكم Hibernate
نقوم بإنشاء وحدة تحكم Hibernate مرتبطة بمشروع Eclipse السابق. سنتبع الخطوات التي تم شرحها مرتين من قبل، لا سيما في الفقرة 2.1.12.
![]() |
- في [1] و [2]: تكوين وحدة التحكم Hibernate
![]() |
- في [3]: استعلام JPQL وفي [4] النتيجة.
- في [5]: الأمر المكافئ SQL
نقدم الآن سلسلة من الاستعلامات JPQL. ندعو القارئ إلى تشغيلها واكتشاف الترتيب SQL الذي أنشأه Hibernate لتنفيذها.
الحصول على جميع المطاعم مع أطباقها:
![]() | ![]() |
الحصول على المطاعم التي تقدم طبقًا نباتيًا واحدًا على الأقل:
![]() | ![]() |
الحصول على أسماء المطاعم التي تقدم أطباقاً نباتية فقط:
![]() | ![]() |
الحصول على المطاعم التي تقدم البرغر:
![]() | ![]() |
2.7.6. QueryDB
ننتقل الآن إلى البرنامج [QueryDB] التابع لمشروع Eclipse والذي:
- يملأ قاعدة
- ويصدر عليها عددًا من الاستعلامات JPQL. يتم تسجيل هذه الاستعلامات في ملف [META-INF/orm.xml] الخاص بمشروع Eclipse:
![]() |
يمكن استخدام الملف [orm.xml] لتكوين الطبقة JPA بدلاً من تعليقات Java. وهذا يوفر مرونة في تكوين الطبقة JPA. يمكن تعديلها دون إعادة ترجمة أكواد Java. يمكن استخدام الطريقتين في آن واحد: تعليقات Java وملف [orm.xml]. يتم إجراء تكوين JPA أولاً باستخدام تعليقات Java ثم باستخدام ملف [orm.xml]. لذا، إذا أردنا تعديل تكوين تم إجراؤه بواسطة تعليق Java دون إعادة التحويل البرمجي، يكفي وضع هذا التكوين في [orm.xml]. وسيكون لهذا الملف الكلمة الأخيرة.
في مثالنا، يُستخدم الملف [orm.xml] لتسجيل نصوص الاستعلامات JPQL. ومحتواه كما يلي:
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
<description>Restaurants</description>
<named-query name="supprimer le contenu de la table restaurant">
<query>delete from Restaurant</query>
</named-query>
<named-query name="supprimer le contenu de la table plat">
<query>delete from Plat</query>
</named-query>
<named-query name="obtenir tous les restaurants">
<query>select r from Restaurant r order by r.nom asc</query>
</named-query>
<named-query name="obtenir toutes les adresses">
<query>select a from Adresse a order by a.nomRue asc</query>
</named-query>
<named-query name="obtenir tous les plats">
<query>select p from Plat p order by p.nom asc</query>
</named-query>
<named-query name="obtenir tous les restaurants avec leurs plats">
<query>select r.nom,p.nom from Restaurant r join r.plats p</query>
</named-query>
<named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
<query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
</named-query>
<named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
<query>
select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
p1.vegetarien=false)
</query>
</named-query>
<named-query name="obtenir les restaurants d'une certaine rue">
<query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
</named-query>
<named-query name="obtenir les restaurants qui servent des burgers">
<query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
</named-query>
<named-query name="obtenir les plats du restaurant untel">
<query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
</named-query>
</entity-mappings>
- جذر الملف [orm.xml] هو <entity-mappings> (السطر 2).
- الأسطر 5-7: الاستعلامات المسماة JPQL تخضع لعلامات <named-query name= "... ">نص</namedquery>.
- السمة name للعلامة هي اسم الاستعلام.
- محتوى العلامة texte هو نص الاستعلام.
QueryDB سيقوم بتنفيذ الاستعلامات السابقة. رمزه هو التالي:
package tests;
...
public class QueryDB {
// سياق الاستمرارية
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
private static EntityManager em = emf.createEntityManager();
public static void main(String[] args) {
// بدء المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// حذف العناصر من الجدول [restaurant]
em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
// حذف العناصر من الجدول [plat]
em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
// إنشاء كائنات Address
Adresse adr1 = new Adresse(10, "Main Street");
Adresse adr2 = new Adresse(20, "Main Street");
Adresse adr3 = new Adresse(123, "Dover Street");
// إنشاء كائنات "المدخل"
Plat ent1 = new Plat("Hamburger", false);
Plat ent2 = new Plat("Cheeseburger", false);
Plat ent3 = new Plat("Tofu Stir Fry", true);
Plat ent4 = new Plat("Vegetable Soup", true);
// إنشاء كائنات المطعم
Restaurant restaurant1 = new Restaurant();
restaurant1.setNom("Burger Barn");
restaurant1.setAdresse(adr1);
restaurant1.getPlats().add(ent1);
restaurant1.getPlats().add(ent2);
Restaurant restaurant2 = new Restaurant();
restaurant2.setNom("Veggie Village");
restaurant2.setAdresse(adr2);
restaurant2.getPlats().add(ent3);
restaurant2.getPlats().add(ent4);
Restaurant restaurant3 = new Restaurant();
restaurant3.setNom("Dover Diner");
restaurant3.setAdresse(adr3);
restaurant3.getPlats().add(ent1);
restaurant3.getPlats().add(ent2);
restaurant3.getPlats().add(ent4);
// استمرار كائنات المطعم (والكائنات الأخرى بشكل متسلسل)
em.persist(restaurant1);
em.persist(restaurant2);
em.persist(restaurant3);
// نهاية المعاملة
tx.commit();
// تفريغ قاعدة البيانات
dumpDataBase();
// نهاية EntityManager
em.close();
// نهاية EntityManagerFactory
emf.close();
}
// عرض محتوى قاعدة البيانات
@SuppressWarnings("unchecked")
private static void dumpDataBase() {
// اختبار 2
log("données de la base");
// بداية المعاملة
EntityTransaction tx = em.getTransaction();
tx.begin();
// عرض المطاعم
log("[restaurants]");
for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
System.out.println(restaurant);
}
// عرض العناوين
log("[adresses]");
for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
System.out.println(adresse);
}
// عرض الأطباق
log("[plats]");
for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
System.out.println(plat);
}
// عدد مرات عرض الروابط بين المطاعم والأطباق
log("[restaurants/plats]");
Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
}
log("[Liste des restaurants avec au moins un plat végétarien]");
for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
System.out.println(r);
}
// استعلام
log("[Liste des restaurants avec seulement des plats végétariens]");
for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
System.out.println(r);
}
// استعلام
log("[Liste des restaurants dans Dover Street]");
for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
System.out.println(r);
}
// استعلام
log("[Liste des restaurants ayant un plat de type burger]");
record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
while (record.hasNext()) {
Object[] currentRecord = (Object[]) record.next();
System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
}
// استعلام
log("[Plats de Veggie Village]");
for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
System.out.println(r);
}
// نهاية المعاملة
tx.commit();
}
// السجلات
private static void log(String message) {
System.out.println(" -----------" + message);
}
}
نتيجة تنفيذ [QueryDB] هي كما يلي:
نترك للقارئ مهمة الربط بين الكود والنتائج. ولهذا الغرض، ننصحه بتشغيل الاستعلامات JPQL في وحدة التحكم Hibernate وفحص الكود SQL المرتبط بها.
2.7.7. مشروع Eclipse / Toplink
سيجد القارئ المهتم في الأمثلة القابلة للتنزيل مع هذا البرنامج التعليمي المشروع السابق الذي تم تنفيذه باستخدام Toplink:
![]() |
مشروع Eclipse مع Toplink هو نسخة من مشروع Eclipse مع Hibernate:
![]() |
يعلن الملف <persistence.xml> [2] عن الكيانات المدارة:
<!-- المزود -->
<provider>oracle.toplink.essentials.PersistenceProvider</provider>
<!-- فئات ثابتة -->
<class>entites.Restaurant</class>
<class>entites.Adresse</class>
<class>entites.Plat</class>
...
- الأسطر 4-6: الكيانات المدارة
يتم تنفيذ الاستعلامات JPQL المسجلة في [orm.xml] بشكل صحيح بواسطة Toplink. لهذا الغرض، حرصنا في المشروع السابق على عدم استخدام استعلامات HQL (لغة استعلامات Hibernate) التي تعد في الواقع مجموعة شاملة لـ JPQL والتي لا تقبل JPQL بعض صيغها.
2.8. الخلاصة
ننهي هنا دراستنا للكيانات JPA. لقد استغرق الأمر وقتًا طويلاً، ومع ذلك لم يتم تناول أمور مهمة (للمطور المتقدم). مرة أخرى، يُنصح بقراءة كتاب مرجعي مثل الذي تم استخدامه في هذا البرنامج التعليمي:
[ref1]: Java Persistence with Hibernate، للكاتبين كريستيان باور وغافين كينغ، من دار مانينغ.


















































































































