11. [Cours]: إدارة قواعد البيانات العلائقية باستخدام Spring Data
الكلمات المفتاحية: بنية متعددة الطبقات، Spring، حقن التبعيات، API JPA (Java Persistence API)، Spring Data.
سنقوم بتنفيذ الطبقة [DAO] من TD باستخدام [Spring Data]، وهو فرع من نظام Spring. يعتمد [Spring Data] على طبقة JPA (Java Persistence API) التي تسمح للطبقة [DAO] بمعالجة الكائنات بدلاً من الأوامر SQL. وفي النهاية، لا تدرك الطبقة [DAO] أنها تتواصل مع قاعدة بيانات. فهي لا تعرف سوى واجهة الطبقة [Spring Data].
![]() |
سنكتشف أولاً [Spring Data] من خلال مثالين.
11.1. Support
![]() |
- في [1]، يحتوي المجلد [support / chap-11] على ثلاثة مشاريع Eclipse؛
- في [2]، البرنامج النصي SQL الذي يسمح بإنشاء قاعدة البيانات النموذجية لهذا الفصل؛
11.2. مثال 1
يوجد على موقع Spring العديد من البرامج التعليمية للبدء مع Spring [http://spring.io/guides]. سنستخدم أحدها لتقديم Spring Data. ونستخدم لهذا الغرض Spring Tool Suite (STS).
![]() |
- في [1]، نقوم باستيراد أحد البرامج التعليمية من [spring.io/guides]؛
![]() |
- في [2]، نختار البرنامج التعليمي [Accessing Data Jpa] الذي يوضح كيفية الوصول إلى قاعدة بيانات باستخدام Spring Data؛
- في [3]، يتم اختيار مشروع تم تكوينه بواسطة Maven؛
- في [4]، يمكن تقديم البرنامج التعليمي في شكلين: [initial] وهي نسخة فارغة يتم ملؤها باتباع البرنامج التعليمي أو [complete] وهي النسخة النهائية من البرنامج التعليمي. نختار هذه الأخيرة؛
- في [5]، يمكننا اختيار عرض البرنامج التعليمي في متصفح؛
- في [6]، المشروع النهائي.
11.2.1. تكوين Maven للمشروع
يتم تكوين تبعيات Maven للمشروع في الملف [pom.xml]:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- استخدام UTF-8 لكل شيء -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- الأسطر 5-9: تحدد مشروع Maven الأصلي. وهو الذي يحدد معظم تبعيات المشروع. قد تكون هذه التبعيات كافية، وفي هذه الحالة لا نضيف المزيد، أو قد لا تكون كافية، وفي هذه الحالة نضيف التبعيات الناقصة؛
- الأسطر 12-15: تحدد تبعية لـ [spring-boot-starter-data-jpa]. يحتوي هذا الأرتيفاكت على فئات Spring Data؛
- الأسطر 16-19: تحدد تبعية لـ SGBD H2 التي تسمح بإنشاء وإدارة قواعد البيانات في الذاكرة.
لنلقِ نظرة على الفئات التي توفرها هذه التبعيات:
![]() | ![]() | ![]() |
وهي عديدة جدًا:
- بعضها ينتمي إلى نظام Spring (تلك التي تبدأ بـ spring)؛
- وبعضها الآخر ينتمي إلى نظام Hibernate (hibernate، jboss) الذي نستخدم هنا تطبيقه JPA؛
- وبعضها الآخر عبارة عن مكتبات اختبار (junit، hamcrest)؛
- وبعضها الآخر عبارة عن مكتبات سجلات (log4j، logback، slf4j)؛
سنحتفظ بها جميعًا. بالنسبة لتطبيق في مرحلة الإنتاج، يجب الاحتفاظ فقط بتلك الضرورية.
في السطر 26 من الملف [pom.xml]، نجد السطر:
<start-class>hello.Application</start-class>
يرتبط هذا السطر بالسطور التالية:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
الأسطر 6-9، يسمح المكون الإضافي [spring-boot-maven-plugin] بإنشاء ملف jar القابل للتنفيذ للتطبيق. تشير السطر 26 من الملف [pom.xml] إلى الفئة القابلة للتنفيذ لهذا الملف jar.
11.2.2. الطبقة [JPA]
يتم الوصول إلى قاعدة البيانات من خلال طبقة [JPA]، Java Persistence API:
![]() |
![]() |
التطبيق بسيط ويدير العملاء [Customer]. الفئة [Customer] هي جزء من الطبقة [JPA] وهي كما يلي:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
يحتوي العميل على معرف [id]، واسم أول [firstName]، واسم عائلة [lastName]. تمثل كل مثيل [Customer] سطرًا في جدول قاعدة البيانات.
- السطر 8: تعليق توضيحي JPA الذي يجعل استمرارية مثيلات [Customer] (إنشاء، قراءة، تحديث، حذف) تُدار بواسطة تطبيق JPA. وفقًا لتبعيات Maven، نرى أن التنفيذ JPA / Hibernate هو المستخدم؛
- السطران 11-12: تعليقات توضيحية JPA تربط الحقل [id] بالمفتاح الأساسي لجدول [Customer]. يشير السطر 12 إلى أن التنفيذ JPA سيستخدم طريقة إنشاء المفتاح الأساسي الخاصة بـ SGBD المستخدم، وهو هنا H2؛
لا توجد تعليقات توضيحية أخرى لـ JPA. سيتم عندئذٍ استخدام القيم الافتراضية:
- ستحمل جدول [Customer] اسم الفئة، أي [Customer]؛
- ستحمل أعمدة هذا الجدول أسماء حقول الفئة: [id, firstName, lastName] مع العلم أن الأحرف الكبيرة والصغيرة لا تؤخذ في الاعتبار في اسم عمود الجدول؛
تجدر الإشارة إلى أنه لم يتم تسمية التنفيذ JPA المستخدم في أي وقت من الأوقات.
11.2.3. الطبقة [Spring Data]
تنفذ الفئة [CustomerRepository] طبقة الوصول إلى الجدول [Customer]. وفيما يلي كودها:
![]() |
![]() |
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
وبالتالي فهي واجهة وليست فئة (السطر 7). وهي توسع واجهة [CrudRepository]، وهي واجهة Spring Data (السطر 5). يتم تعيين هذه الواجهة بواسطة نوعين: الأول هو نوع العناصر المدارة، وهو هنا النوع [Customer]، والثاني هو نوع المفتاح الأساسي للعناصر المدارة، وهو هنا النوع [Long]. واجهة [CrudRepository] هي كما يلي:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
تحدد هذه الواجهة العمليات CRUD (إنشاء – قراءة – تحديث – حذف) التي يمكن إجراؤها على نوع JPA T:
- السطر 8: تسمح طريقة save بحفظ كيان T في قاعدة البيانات. وهي تجعل الكيان محفوظًا بالمفتاح الأساسي الذي أعطاه له SGBD. كما تسمح بتحديث كيان T المحدد بمفتاحه الأساسي id. يتم اختيار إحدى العمليتين وفقًا لقيمة المفتاح الأساسي id: إذا كانت قيمته null، تتم عملية الحفظ، وإلا تتم عملية التحديث؛
- السطر 10: نفس الشيء ولكن بالنسبة لقائمة من الكيانات؛
- السطر 12: تسمح الطريقة findOne بالعثور على كيان T محدد بواسطة مفتاحه الأساسي id؛
- السطر 22: تسمح الطريقة delete بحذف كيان T محدد بواسطة مفتاحه الأساسي id؛
- السطور 24-28: متغيرات الطريقة [delete]؛
- السطر 16: تتيح الطريقة [findAll] استرداد جميع الكيانات المخزنة T؛
- السطر 18: نفس الشيء ولكن يقتصر على الكيانات التي تم تمرير قائمة معرفاتها؛
لنعد إلى واجهة [CustomerRepository]:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- السطر 9 يسمح بالعثور على [Customer] من خلال اسمه [lastName]؛
وهذا كل شيء بالنسبة لطبقة [DAO]. لا توجد فئة لتنفيذ الواجهة السابقة. يتم إنشاء هذه الفئة عند التنفيذ بواسطة [Spring Data]. يتم تنفيذ أساليب واجهة [CrudRepository] تلقائيًا. أما بالنسبة للأساليب المضافة في واجهة [CustomerRepository]، فهذا يعتمد على الحالة. لنعد إلى تعريف [Customer]:
private long id;
private String firstName;
private String lastName;
يتم تنفيذ الطريقة الموجودة في السطر 9 تلقائيًا بواسطة [Spring Data] لأنها تشير إلى الحقل [lastName] (السطر 3) في [Customer]. عندما يصادف Spring Data طريقة [findBySomething] في الواجهة المطلوب تنفيذها، فإنه ينفذها من خلال الاستعلام JPQL (لغة استعلامات Java Persistence) التالي:
لذلك يجب أن يحتوي النوع T على حقل باسم [something]. وبالتالي فإن الطريقة
بواسطة كود يشبه ما يلي:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
حيث يشير [em] إلى سياق الاستمرارية JPA. وهذا ممكن فقط إذا كانت الفئة [Customer] تحتوي على حقل باسم [lastName]، وهو ما يحدث بالفعل.
في الختام، في الحالات البسيطة، يتيح لنا Spring Data تنفيذ الطبقة [DAO] بواجهة بسيطة.
11.2.4. الطبقة [console]
![]() |
![]() |
الفئة [Application] هي كما يلي:
package hello;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
CustomerRepository repository;
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Override
public void run(String... strings) throws Exception {
// حفظ عدد من العملاء
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// استدعاء جميع العملاء
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : repository.findAll()) {
System.out.println(customer);
}
System.out.println();
// استدعاء عميل فردي بواسطة ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// استدعاء العملاء حسب الاسم الأخير
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : repository.findByLastName("Bauer")) {
System.out.println(bauer);
}
}
}
- السطر 9: تنفذ الفئة الواجهة [CommandLineRunner] التي هي واجهة [Spring Boot] (السطر 4). لا تحتوي هذه الواجهة سوى على طريقة واحدة، وهي تلك الموجودة في السطر 19؛
- السطر 8: @SpringBootApplication هو تعليق يجمع عدة تعليقات [Spring Boot]:
- @Configuration: تشير إلى أن الفئة هي فئة تكوين؛
- @EnableAutoConfiguration: يطلب من [Spring Boot] إنشاء عدد معين من الفاصوليا بنفسه بناءً على خصائص متنوعة، ولا سيما محتوى مسار الفئة (Classpath) للمشروع. نظرًا لوجود مكتبات Hibernate في مسار الفئات (Classpath)، سيتم تنفيذ حبة [entityManagerFactory] باستخدام Hibernate. نظرًا لوجود مكتبة SGBD H2 في مسار الفئات (Classpath)، سيتم تنفيذ الكائن (bean) [dataSource] باستخدام H2. في bean [dataSource]، يجب أيضًا تعريف المستخدم وكلمة المرور الخاصة به. هنا سيستخدم Spring Boot المسؤول الافتراضي لـ H2، وهو بدون كلمة مرور. نظرًا لوجود المكتبة [spring-tx] في مسار الفئات (Classpath)، فسيتم استخدام مدير معاملات Spring؛
- @EnableWebMvc: إذا كانت مكتبة [spring-mvc] موجودة في مسار الفئات. في هذه الحالة، يتم إجراء تكوين تلقائي لتطبيق الويب؛
- @ComponentScan: الذي يحدد لـ Spring مكان البحث عن الفاصوليا والتكوينات والخدمات الأخرى. هنا يتم البحث عنها افتراضيًا في الحزمة التي تحتوي على الفئة الموسومة، أي الحزمة [hello]. وبالتالي سيتم العثور على الفئتين [Customer] و [CustomerRepository]. ولأن الأولى تحمل التعليق التوضيحي [@Entity]، فسيتم تصنيفها ككيان يجب أن يديره Hibernate. ولأن الثانية تمتد واجهة [CrudRepository]، فسيتم تسجيلها كحبة Spring؛
- السطران 11-12: يتم حقن bean [CustomerRepository] في كود الفئة الرئيسية؛
- السطر 15: يتم تنفيذ الطريقة الثابتة [run] للفئة [SpringApplication] في مشروع Spring Boot. معلمتها هي الفئة التي تحتوي على تعليق توضيحي [Configuration] أو [EnableAutoConfiguration]. سيتم بعد ذلك تنفيذ كل ما تم شرحه سابقًا. والنتيجة هي سياق تطبيق Spring، أي مجموعة من الفاصوليا التي يديرها Spring؛
العمليات التالية لا تفعل سوى استخدام أساليب bean التي تنفذ واجهة [CustomerRepository]. نتائج وحدة التحكم هي كما يلي:
- الأسطر 1-8: شعار مشروع Spring Boot؛
- السطر 9: يتم تنفيذ الفئة [hello.Application]؛
- السطر 10: [AnnotationConfigApplicationContext] هي فئة تنفذ واجهة [ApplicationContext] من Spring. وهي عبارة عن حاوية للبيانات؛
- السطر 11: يتم تنفيذ bean [entityManagerFactory] باستخدام الفئة [LocalContainerEntityManagerFactory]، وهي فئة من Spring؛
- السطر 12: يظهر [Hibernate]. تم اختيار هذا التنفيذ JPA؛
- السطر 19: لهجة Hibernate هي المتغير SQL الذي سيتم استخدامه مع SGBD. هنا، تُظهر لهجة [H2Dialect] أن Hibernate ستعمل مع SGBD و H2؛
- السطران 21-22: تم إنشاء قاعدة البيانات. يتم إنشاء الجدول [CUSTOMER]. وهذا يعني أن Hibernate قد تم تكوينه لإنشاء الجداول من التعريفات JPA، وهنا التعريف JPA للفئة [Customer]؛
- الأسطر 26-30: نتيجة طريقة [findAll] للواجهة؛
- السطر 34: نتيجة الطريقة [findOne] للواجهة؛
- الأسطر 38-39: نتائج الطريقة [findByLastName]؛
- السطور 41 وما يليها: سجلات إغلاق سياق Spring.
11.2.5. التكوين اليدوي لمشروع Spring Data
نقوم بنسخ المشروع السابق في المشروع [gs-accessing-data-jpa-02]:
![]() |
في هذا المشروع الجديد، لن نعتمد على التكوين التلقائي الذي يقوم به Spring Boot. سنقوم بذلك يدويًا. قد يكون ذلك مفيدًا إذا كانت الإعدادات الافتراضية لا تناسبنا.
أولاً، سنقوم بتوضيح التبعيات الضرورية في الملف [pom.xml]:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa-02</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- قاعدة بيانات H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- تومكات JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
</dependencies>
<properties>
<!-- استخدم UTF-8 لكل شيء -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release</url>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- الأسطر 10-14: مشروع Maven الأصلي الذي سنستخدم المكتبات التي يحددها؛
- الأسطر 18-21: Spring Data المستخدم للوصول إلى قاعدة البيانات؛
- الأسطر 23-26: تنفيذ Hibernate لمواصفات JPA؛
- الأسطر 28-31: SGBD H2؛
- الأسطر 33-36: غالبًا ما تُستخدم قواعد البيانات مع مجموعات من الاتصالات المفتوحة التي تمنع تكرار عمليات فتح وإغلاق الاتصالات. هنا، يتم استخدام التنفيذ الخاص بـ [tomcat-jdbc]؛
في المشروع الجديد، لا تتغير الكيان [Customer] والواجهة [CustomerRepository]. سنقوم بتغيير الفئة [Application] التي سيتم تقسيمها إلى فئتين:
- [Config] التي ستكون فئة التكوين:
- [Main] التي ستكون الفئة القابلة للتنفيذ؛
![]() |
أصبحت الفئة القابلة للتنفيذ [Application] كما يلي:
package console;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;
public class Application {
public static void main(String[] args) {
// إنشاء مثيل سياق Spring
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// حفظ عدد من العملاء
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
...
// إغلاق السياق
context.close();
}
}
- السطر 9: لم تعد فئة [Application] تحتوي على تعليقات توضيحية للتكوين؛
- الأسطر 3-7: تجدر الإشارة إلى أنه لم يعد هناك استيراد لحزم [Spring Boot]؛
- السطر 12: يتم إنشاء مثيلات لـ Spring beans. يتم الحصول على سياق Spring الذي يحتوي على مرجع لـ beans التي تم إنشاؤها؛
- السطر 13: نطلب مرجعًا على bean من النوع [CustomerRepository]؛
الفئة [Config] التي تهيئ المشروع هي التالية:
package config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {
// قاعدة البيانات H2
@Bean
public DataSource dataSource() {
// مصدر البيانات TomcatJdbc
DataSource dataSource = new DataSource();
// تكوين الوصول JDBC
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
// اتصال مفتوح في البداية
dataSource.setInitialSize(1);
// النتيجة
return dataSource;
}
// المزود JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// مدير المعاملات
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- السطر 17: تشير التعليقات التوضيحية [@EnableTransactionManagement] إلى أن طرق واجهات [CrudRepository] يجب أن تتم داخل معاملة. وقد تم وضعها في التعليقات لأنها هي الحالة الافتراضية؛
- السطر 18: تسمح التعليقات التوضيحية [@EnableJpaRepositories] بتحديد المجلدات التي توجد فيها واجهات Spring Data [CrudRepository]. ستصبح هذه الواجهات مكونات Spring وستكون متاحة في سياقها؛
- السطر 19: التعليق التوضيحي [@Configuration] يجعل من الفئة [Config] فئة تكوين Spring؛
- السطر 20: تسمح التعليقات التوضيحية [@ComponentScan] بإدراج المجلدات التي يجب البحث فيها عن مكونات Spring. مكونات Spring هي فئات تم تمييزها بعلامات Spring مثل @Service و@Component و@Controller، ... هنا، لا توجد مكونات أخرى غير تلك المحددة داخل الفئة [AppConfig]، ولذلك تم وضع العلامة في التعليقات؛
- الأسطر 24-37: تحدد مصدر البيانات، قاعدة البيانات H2. إن تعليق @Bean في السطر 25 هو الذي يجعل الكائن الذي تم إنشاؤه بواسطة هذه الطريقة مكونًا يديره Spring. يمكن أن يكون اسم الطريقة هنا أي اسم. ومع ذلك، يجب أن يُسمى [dataSource] إذا كان EntityManagerFactory في السطر 51 غائبًا ومُعرَّفًا عن طريق التكوين التلقائي؛
- السطر 30: ستسمى قاعدة البيانات [demo] وسيتم إنشاؤها في مجلد المشروع؛
- الأسطر 40-47: تحدد التنفيذ JPA المستخدم، وهو هنا تنفيذ Hibernate. يمكن أن يكون اسم الطريقة هنا أي اسم؛
- السطر 43: لا توجد سجلات SQL؛
- السطر 44: سيتم إنشاء قاعدة البيانات إذا لم تكن موجودة؛
- الأسطر 50-58: تحدد EntityManagerFactory التي ستدير استمرارية JPA. يجب أن يُسمى الأسلوب بالضرورة [entityManagerFactory]؛
- السطر 51: تتلقى الطريقة معلمتين من النوعين المحددين سابقًا. سيتم بعد ذلك إنشاء هاتين المعلمتين ثم إدخالهما بواسطة Spring كمعلمات للطريقة؛
- السطر 53: يحدد التنفيذ JPA المستخدم؛
- السطر 54: يحدد المجلدات التي توجد فيها كيانات JPA؛
- السطر 55: يحدد مصدر البيانات المراد إدارته؛
- الأسطر 61-66: مدير المعاملات. يجب أن يُسمى الأسلوب بالضرورة [transactionManager]. ويتلقى كمعلمة bean من الأسطر 51-58؛
- السطر 64: يتم ربط مدير المعاملات بـ EntityManagerFactory؛
يمكن تعريف الطرق السابقة بأي ترتيب.
يؤدي تنفيذ المشروع إلى نفس النتائج. يظهر ملف جديد في مجلد المشروع، وهو ملف قاعدة البيانات H2:
![]() |
11.2.6. إنشاء أرشيف قابل للتنفيذ
لإنشاء أرشيف قابل للتنفيذ للمشروع، يمكن اتباع الخطوات التالية:
![]() |
- في [1]: يتم إنشاء تكوين للتنفيذ؛
- في [2]: من النوع [Java Application]
- في [3]: يحدد المشروع المراد تنفيذه (استخدم الزر Browse)؛
- في [4]: يشير إلى الفئة المراد تنفيذها؛
- في [5]: اسم تكوين التنفيذ – يمكن أن يكون أي اسم؛
![]() |
- في [6]: يتم تصدير المشروع؛
- في [7]: في شكل أرشيف JAR قابل للتنفيذ؛
- في [8]: يشير إلى مسار واسم الملف القابل للتنفيذ المراد إنشاؤه؛
- في [9]: اسم تكوين التشغيل الذي تم إنشاؤه في [5]؛
![]() |
- في [10]، الأرشيف الذي تم إنشاؤه؛
بعد ذلك، نفتح وحدة تحكم في المجلد الذي يحتوي على الأرشيف القابل للتنفيذ:
يتم تشغيل الملف المضغوط بالطريقة التالية:
.....\dist>java -jar gs-accessing-data-jpa-02.jar
النتائج التي تم الحصول عليها في وحدة التحكم هي كما يلي:
11.3. مثال 2
11.3.1. مقدمة
سنستأنف مثال جدول المنتجات الذي استخدمناه لتقديم API JDBC وإنشاء البنية التالية:
![]() |
تحتوي قاعدة البيانات [dbintrospringjpa] على جدولين هما [PRODUITS] و [CATEGORIES]. وفيما يلي الجدول [CATEGORIES]:
![]() |
- [ID]: المفتاح الأساسي في الوضع AUTO_INCREMENT؛
- [VERSION]: رقم إصدار السجل؛
- [NOM]: اسم الفئة - فريد؛
الجدول [PRODUITS] هو كما يلي:
![]() |
- [ID]: المفتاح الأساسي في الوضع AUTO_INCREMENT؛
- [VERSION]: رقم إصدار السجل؛
- [NOM]: اسم المنتج - فريد؛
- [ID_CATEGORIE]: رقم فئته - مفتاح خارجي في الحقل [CATEGORIES.ID]؛
- [PRIX]: سعره؛
- [DESCRIPTION]: وصف المنتج؛
المهمة المطلوبة: قم بإنشاء قاعدة البيانات [dbintrospringdata] باستخدام البرنامج النصي SQL [dbintrospringdata.sql] من الدعم:
11.3.2. إنشاء مشروع Maven
لإنشاء هيكل مشروع Spring Data، يمكن اتباع الخطوات التالية:
![]() |
- في [1]، يتم إنشاء مشروع جديد؛
- في [2]: من النوع [Spring Starter Project]؛
- سيكون المشروع الذي تم إنشاؤه مشروع Maven. في [3]، نحدد اسم مجموعة المشروع؛
- في [4]: نحدد اسم الأرتيفاكت (ملف jar هنا) الذي سيتم إنشاؤه عند بناء المشروع؛
- في [5]: اسم Eclipse للمشروع – يمكن أن يكون أي اسم (لا يجب أن يكون مطابقًا لـ [4])؛
- في [7]: يُشار إلى أنه سيتم إنشاء مشروع يحتوي على طبقة [JPA] مع SGBD MySQL. سيتم بعد ذلك تضمين التبعيات اللازمة لمثل هذا المشروع في الملف [pom.xml]؛
![]() |
- في [8]، أدخل اسم مجلد المشروع؛
- في [9]، قم بإنهاء المساعد؛
![]() |
- في [10]: المشروع الذي تم إنشاؤه؛
يضم الملف [pom.xml] التبعيات اللازمة لمشروع JPA:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>intro-spring-data-01</name>
<description>démo spring data avec table de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
<relativePath/> <!-- البحث عن الأصل من المستودع -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>demo.IntroSpringData01Application</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- الأسطر 14-19: مشروع Maven الأصلي - يحدد عددًا كبيرًا من المكتبات مع إصداراتها - يتم استخدام هذه المكتبات كاعتمادات Maven دون تحديد إصداراتها؛
- الأسطر 28-31: التبعية اللازمة لـ JPA – ستشمل [Spring Data]؛
- الأسطر 32-36: التبعية على برنامج التشغيل JDBC من MySQL؛
- الأسطر 37-41: التبعيات الضرورية لاختبارات JUnit المدمجة مع Spring؛
الفئة القابلة للتنفيذ [Application] لا تقوم بأي شيء ولكنها معدة مسبقًا:
package demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class IntroSpringData01Application {
public static void main(String[] args) {
SpringApplication.run(IntroSpringData01Application.class, args);
}
}
- التعليق التوضيحي [@SpringBootApplication] يجعل الفئة فئة تهيئة تلقائية للمشروع؛
فئة الاختبارات [ApplicationTests] لا تقوم بأي شيء ولكنها معدة مسبقًا:
package demo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {
@Test
public void contextLoads() {
}
}
- السطر 9: تسمح التعليقات التوضيحية [@SpringApplicationConfiguration] باستخدام ملف التكوين [Application]. وبالتالي ستستفيد فئة الاختبار من جميع الحبوب التي سيتم تعريفها بواسطة هذا الملف؛
- السطر 8: تسمح التعليقات التوضيحية [@RunWith] بدمج Spring مع JUnit: سيتمكن من تنفيذ الفئة كاختبار JUnit. [@RunWith] هو تعليق توضيحي JUnit (السطر 4) بينما الفئة [SpringJUnit4ClassRunner] هي فئة Spring (السطر 6)؛
الآن بعد أن أصبح لدينا هيكل تطبيق JPA، يمكننا استكماله لكتابة مشروع طبقة الاستمرارية المرتبطة بقاعدة بيانات المنتجات.
11.3.3. مشروع Eclipse
نقوم بتطوير المشروع السابق بالطريقة التالية:
![]() |
- [AppConfig.java]: فئة تكوين مشروع Spring؛
- [Main.java]: الفئة القابلة للتنفيذ للمشروع؛
- [IDao.java]: واجهة طبقة [DAO]؛
- [Dao.java]: فئة تنفيذ الطبقة [DAO]؛
- [AbstractEntity.java]: الفئة الأم للفئتين [Produit] و [Categorie]؛
- [Produit.java]: فئة مرتبطة بصف في الجدول [PRODUITS] في قاعدة البيانات؛
- [Categorie.java]: فئة مرتبطة بصف في الجدول [CATEGORIES] في قاعدة البيانات؛
- [ProduitsRepository]: واجهة Spring Data للوصول إلى الجدول [PRODUITS]؛
- [CategoriesRepository]: واجهة Spring Data للوصول إلى الجدول [CATEGORIES]؛
- [pom.xml]: ملف تكوين مشروع Maven؛
يُنفذ هذا المشروع البنية التالية:
![]() |
لا ترى الطبقة [DAO] سوى الطبقة التي تنفذها [Spring Data].
11.3.4. تكوين Maven
ملف [pom.xml] لمشروع Maven هو التالي:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>intro-spring-data-01</name>
<description>démo spring data avec table de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- MySQL قاعدة البيانات -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- مكتبة jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- اختبار Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
<!-- مكتبة السجلات -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
هذه التهيئة هي التي تم استخدامها وشرحها في الفقرة 11.2.5. نضيف إليها المكتبات التالية:
- الأسطر 42-49: مكتبة jSON المستخدمة بواسطة الطريقة [toString] للفئة [Produit]؛
- الأسطر 51-55: مكتبة [Google Guava] التي توفر طرقًا مساعدة لإدارة مجموعات العناصر. سيتم استخدامها من قبل الفئة [Dao] التي تنفذ الطبقة [DAO]؛
- الأسطر 56-67: المكتبات اللازمة للاختبارات JUnit؛
- الأسطر 69-72: مكتبة سجلات؛
- الأسطر 81-86: مكونات Maven الإضافية اللازمة للمشروع؛
11.3.5. كيانات الطبقة [JPA]
الطبقة
[DAO]
الطبقة
[console]
الطبقة
[JPA]
برنامج التشغيل
[JDBC]
طبقة
[Spring Data]
Spring 4
SGBD
![]() |
11.3.5.1. الفئة [AbstractEntity]
الفئة [AbstractEntity] هي كما يلي:
package spring.data.entities;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@MappedSuperclass
public abstract class AbstractEntity {
// الخصائص
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
protected Long id;
@Version
@Column(name = "VERSION")
protected Long version;
// المنشئات
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// إعادة تعريف [equals] و [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id.longValue() == other.id.longValue();
}
// التوقيع jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// مُستردات ومُعيّنات
....
}
تهدف هذه الفئة إلى توفير فئة أم للكيانات JPA من خلال تجميع الخصائص [id, version] (السطران 19 و22) المشتركة بين الكيانين [Produit] و[Categorie] المرتبطين بقاعدة البيانات. ترتبط هذه الخصائص بأعمدة [ID, VERSION] في الجداول (السطران 18 و21).
- السطر 13: تشير التعليقات التوضيحية [@MappedSuperclass] إلى أن الفئة هي فئة أم للكيانات JPA؛
- السطر 16: يشير التعليق التوضيحي [@Id] إلى أن الحقل [id] (قد يكون له اسم آخر) مرتبط بالمفتاح الأساسي لإحدى الجداول؛
- السطر 17: تحدد التعليقات التوضيحية [@GeneratedValue(strategy=GenerationType.IDENTITY)] طريقة إنشاء المفاتيح الأساسية. سيستخدم الوضع [GenerationType.IDENTITY] مع MySQL الوضع [AUTO_INCREMENT]. مع SGBD آخر، سيستخدم هذا الوضع طريقة أخرى. الفائدة هي أن المطور لا يحتاج إلى القلق بشأن ذلك وأن كوده يظل صالحًا بغض النظر عن SGBD المستخدم؛
- السطر 18: تشير التعليقات التوضيحية [@Column] إلى العمود المرتبط بالحقل. عندما لا تكون هذه التعليقات التوضيحية موجودة، يفترض JPA أن العمود يحمل نفس اسم الحقل. وهذا هو الحال هنا. لذلك كان من الممكن عدم وضع هذه التعليقات التوضيحية؛
- السطر 20: يشير التعليق التوضيحي [@Version] إلى أن الحقل [version] مرتبط بعمود لإدارة الإصدارات. سيقوم التنفيذ JPA بزيادة رقم الإصدار هذا في كل مرة يتم فيها تعديل الكيان. يُستخدم هذا الرقم لمنع التحديث المتزامن للكيان من قبل مستخدمين مختلفين: يقوم مستخدمان U1 و U2 بقراءة الكيان E برقم إصدار يساوي V1. يقوم U1 بتعديل E ويحفظ هذا التعديل في قاعدة البيانات: عندئذٍ يتغير رقم الإصدار إلى V1+1. U2 يقوم بدوره بتعديل E ويحفظ هذا التعديل في قاعدة البيانات: سيتلقى استثناءً لأنه يمتلك إصدارًا (V1) مختلفًا عن الإصدار الموجود في قاعدة البيانات (V1+1)؛
- الأسطر 35-52: إعادة تعريف الطرق [hashCode] و [equals]. بشكل افتراضي، تكون قيمة [obj1.equals(obj2)] هي true إذا كانت [obj1==obj2]، أي إذا كان ob1 و obj2 مؤشرين متساويين. إذا أردنا مقارنة الكائنات المشار إليها بدلاً من المؤشرات نفسها، يجب إعادة تعريف الطريقة [equals] والطريقة [hashCode]. يجب أن تعطي هذه الطريقة نفس القيمة لكائنين تعتبرهما الطريقة [equals] متساويين؛
- الأسطر 42-51: سيُعتبر كائنان من النوع [AbstractEntity] أو مشتقان منه متساويين إذا كانت مفاتيحهما الأساسية [id] متساوية؛
- الأسطر 35-38: تُرجع الطريقة [hashCode] بالفعل نفس القيمة لكائنين [AbstractEntity] متطابقين، وبالتالي لهما نفس المفتاح الأساسي [id]؛
- الأسطر 55-63: تُرجع الطريقة [toString] السلسلة jSon للكائن [this]. إذا كان هذا الكائن يشير إلى فئة فرعية، فستُرجع هذه الطريقة سلسلة jSON الخاصة بالفئة الفرعية. وهذا يعفينا من إنشاء طريقة [toString] في الفئات الفرعية؛
11.3.5.2. الكيان JPA [Produit]
الفئة [Produit] هي كيان JPA مرتبط بصف في الجدول [PRODUITS]:
![]() |
package spring.data.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonFilter;
@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// الخصائص
@Column(name = "NOM")
private String nom;
@Column(name = "CATEGORIE_ID", insertable = false, updatable = false)
private Long idCategorie;
@Column(name = "PRIX")
private double prix;
@Column(name = "DESCRIPTION")
private String description;
// الفئة
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORIE_ID")
private Categorie categorie;
// المنشئات
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// مُستردات ومُعيّنات
...
}
- السطر 12: التعليق التوضيحي [@Entity] يجعل الفئة [Produit] كيانًا تديره الطبقة [JPA]؛
- السطر 13: يشير التعليق التوضيحي [@Table(name = "PRODUITS")] إلى أن الفئة [Produit] هي الصورة الموضوعة لسطر في الجدول [PRODUITS] في قاعدة البيانات؛
- السطر 14: اسم المرشح jSON المراد تطبيقه على الكيان. سنرى أن الخاصية [categorie] في السطر 13 ليست متاحة دائمًا. لذلك يجب استبعادها من تمثيل الكائن jSON. ولهذا الغرض، نحتاج إلى مرشح. لذا، سنحدد في مرشح باسم [jsonFilterCategorie] ما إذا كنا نريد الخاصية [categorie] أم لا؛
- السطر 18: تربط التعليقات التوضيحية [@Column] الحقل [nom] بالعمود [NOM] في الجدول [PRODUITS]. عندما يحمل الحقل نفس اسم العمود المرتبط به، يمكن حذف التعليق التوضيحي [@Column]. وهذا هو الحال هنا؛
- الأسطر 31-33: فئة المنتج؛
- السطر 31: تشير التعليقات التوضيحية [@ManyToOne] إلى أن عمود التعليقات التوضيحية في السطر 32 [@JoinColumn(name = "CATEGORIE_ID")] هو مفتاح خارجي للجدول [PRODUITS] الخاصالكيان [Produit] في الجدول [CATEGORIES] المرتبط بالكيان في السطر 33. يجب أن تشير هذه التعليقات التوضيحية إلى كيان JPA. وبالتالي، يجب أن تكون فئة السطر 33 كيانًا JPA؛
- السطر 31: يطلب التعليق التوضيحي [fetch = FetchType.LAZY] أنه عند استرجاع منتج من الجدول [PRODUITS]، لا يتم استرجاع فئته (السطر 33) على الفور (التحميل المتأخر). ويتم الحصول عليها عند أول استدعاء للطريقة [getCategorie]. هذه الخاصية ليست إلزامية. يحق للتنفيذ JPA المستخدم تجاهله. ولأن الخاصية [categorie] قد تكون موجودة أو غير موجودة، فقد أدخلنا مرشح jSON في السطر 14. لا تدير تطبيقات JPA الحالية (Hibernate، Eclipselink، OpenJPA) هذه التعليقات التوضيحية بنفس الطريقة. يُثري Hibernate الطريقة [getCategorie] الأولية (التي تكتفي بإرجاع الحقل categorie) من خلال استدعاء SGBD لجلب الفئة. ولكي يكون ذلك ممكنًا، يجب أن يكون الاتصال بـ SGBD المستخدم في البداية للحصول على المنتج لا يزال مفتوحًا، وإلا فسيحدث استثناء.
11.3.5.3. الكيان JPA [Categorie]
الفئة [Categorie] هي كيان JPA مرتبط بسطر في الجدول [CATEGORIES]:
![]() |
ورمزها هو:
package spring.data.entities;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import com.fasterxml.jackson.annotation.JsonFilter;
@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// الخصائص
@Column(name = "NOM")
private String nom;
// المنتجات ذات الصلة
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
public Set<Produit> produits = new HashSet<Produit>();
// المنشئات
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// الأساليب
public void addProduit(Produit produit) {
// إضافة المنتج
produits.add(produit);
// تحديد فئته
produit.setCategorie(this);
}
// مُستردات ومُعيّنات
...
}
- السطران 21-22: اسم الفئة؛
- السطران 25-26: منتجات هذه الفئة؛
- السطر 25: التوضيح [@OneToMany] هو العلاقة العكسية للعلاقة [@ManyToOne] التي صادفناها في الكيان [Produit]. يشير السمة [mappedBy = "categorie"] إلى حقل الكيان [Produit] المُعلَّم بالعلاقة العكسية [@ManyToOne]. يطلب السمة [cascade = { CascadeType.ALL }] أن يتم تطبيق العمليات (persist، merge، remove) التي يتم إجراؤها على @Entity [Categorie] بشكل متسلسل على [produits] في السطر 26. يمكن تحديد تسلسلات جزئية باستخدام الثوابت [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE]؛
- السطر 25: يطلب السمة [fetch = FetchType.LAZY] أنه عند استرداد فئة من الجدول [CATEGORIES]، لا يتم استرداد منتجاتها على الفور. سيتم استردادها عند أول استدعاء للطريقة [getProduits]. لا تدير عمليات التنفيذ الحالية لـ JPA (Hibernate، Eclipselink، OpenJPA) هذه التعليقات التوضيحية بنفس الطريقة. يُثري Hibernate الطريقة [getProduits] الأولية (التي تكتفي بإرجاع الحقل produits) باستدعاء SGBD لجلب منتجات الفئة. ولكي يكون ذلك ممكنًا، يجب أن يكون الاتصال بـ SGBD المستخدم في البداية للحصول على الفئة لا يزال مفتوحًا. هذه الخاصية إلزامية. لا يمكن لتنفيذ JPA تجاهله. نظرًا لأن الخاصية [produits] قد تكون مبدئية أو غير مبدئية، فقد أدخلنا مرشح jSON في السطر 17 الذي سيسمح لنا بتحديد ما إذا كنا نريد هذه الخاصية أم لا؛
- السطر 26: النوع [Set] هو واجهة. النوع [HashSet] هو فئة تنفذ هذه الواجهة. وهي تنفذ مجموعة من العناصر تسمى ensemble. لا يمكن أن تحتوي المجموعة على كائنين متطابقين. هنا، الكائنات من النوع [Produit]. وبالتالي، لا يمكن أن تحتوي المجموعة على كائنين متطابقين. نظرًا لأن الطريقة [equals] للفئة الأصلية [AbstractEntity] قد أعيد تعريفها لتشير إلى أن المنتجين متطابقان إذا كان لهما نفس المفتاح الأساسي، فإن الحقل [produits] لن يحتوي على منتجين لهما نفس المفتاح الأساسي؛
- السطور 38-43: تسمح الطريقة [addProduit] بإضافة منتج إلى الفئة؛
11.3.6. الطبقة [Spring Data]
الطبقة
[DAO]
الطبقة
[console]
طبقة
[JPA]
الطيار
[JDBC]
طبقة
[Spring Data]
الربيع 4
SGBD
![]() |
تدير الواجهة [CategoriesRepository] الوصول إلى الجدول [CATEGORIES]:
package spring.data.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import spring.data.entities.Categorie;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// الفئة مع منتجاتها
@Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
public Categorie getCategorieByIdWithProduits(Long id);
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
public Categorie getCategorieByNameWithProduits(String nom);
// فئة بدون منتجاتها محددة باسمها
public Categorie findByNom(String nom);
}
- السطر 8: تم استخدام واجهة [CrudRepository] وشرحها في الفقرة 11.2.3 . نذكر أن:
- النوع الأول للواجهة هو الكيان JPA الذي يُدار للوصول إلى CRUD (findOne، findAll، حفظ، حذف، deleteAll)،
- النوع الثاني هو مفتاح الكيان الأساسي JPA، وهو هنا عدد صحيح [Long]؛
- السطر 12: يتم تنفيذ طريقة السطر 12 بواسطة الاستعلام JPQL (لغة استعلامات Java Persistence) في السطر 11. ويستعلم هذا الاستعلام عن الكيانات JPA. في مثل هذا الاستعلام:
- يتم استبدال الجداول بكياناتها المرتبطة JPA؛
- يتم استبدال الأعمدة بحقول الكيانات JPA المستخدمة في الاستعلام؛
- السطر 11: يعرض الاستعلام JPQL فئة مع منتجاتها. نتذكر أنه في الكيان [Categorie]، كان للحقل [produits] السمة [fetch = FetchType.LAZY] (التحميل المتأخر). في الاستعلام JPQL، يتم فرض تحميل المنتجات باستخدام الكلمة الرئيسية [fetch]. سيتم استبدال المعلمة ?1 في الاستعلام عند التنفيذ بقيمة المعلمة الأولى لطريقة السطر 12، أي بالمعلمة [Long id]؛
- السطران 14-15: طريقة مماثلة لفئة محددة باسمها؛
- السطر 18: سيتم تنفيذ الطريقة [findByNom] تلقائيًا بواسطة [Spring Data] لأن النوع [Category] يحتوي على حقل [nom]؛
تدير الواجهة [ProduitsRepository] الوصول إلى الجدول [PRODUITS]:
package spring.data.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import spring.data.entities.Produit;
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// منتج مع فئته
@Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
public Produit getProduitByIdWithCategorie(Long id);
@Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
public Produit getProduitByNameWithCategorie(String nom);
// منتج بدون فئته محدد باسمه
public Produit findByNom(String nom);
}
التفسيرات هي نفسها كما في واجهة [CategoriesRepository].
سيتم تنفيذ هذه الواجهات بواسطة فئات تم إنشاؤها بواسطة [Spring Data] عند تنفيذ المشروع. تسمى هذه الفئات [proxy]. بشكل افتراضي، يتم تنفيذ أساليب فئة التنفيذ في معاملة. إن حقيقة أن هذه الواجهات تمتد إلى فئة [CrudRepository] تجعلها مكونات Spring.
11.3.7. الطبقة [DAO]
الطبقة
[DAO]
الطبقة
[console]
طبقة
[JPA]
السائق
[JDBC]
طبقة
[Spring Data]
الربيع 4
SGBD
![]() |
واجهة [IDao] للطبقة [DAO] هي كما يلي:
package spring.data.dao;
import java.util.List;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
public interface IDao {
// إدراج قائمة بالمنتجات
public List<Produit> addProduits(List<Produit> produits);
// حذف جميع المنتجات
public void deleteAllProduits();
// تحديث قائمة المنتجات
public List<Produit> updateProduits(List<Produit> produits);
// الحصول على جميع المنتجات
public List<Produit> getAllProduits();
// إدراج قائمة بالفئات
public List<Categorie> addCategories(List<Categorie> categories);
// حذف جميع الفئات
public void deleteAllCategories();
// تحديث قائمة الفئات
public List<Categorie> updateCategories(List<Categorie> categories);
// الحصول على جميع الفئات
public List<Categorie> getAllCategories();
// منتج معين مع أو بدون فئته
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// فئة معينة مع أو بدون منتجاتها
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
تم هنا اعتماد قاعدة مفادها أن أي طريقة تقوم بتعديل الكائنات الموجودة في معلمات الإدخال الخاصة بها يجب أن تعيدها في نتيجتها. تم شرح سبب هذه القاعدة في الفقرة 4.2: فهي تسمح لطبقة وعميلها بأن يكونا في طبقتين منفصلتين JVM وبالتالي العمل بنظام العميل/الخادم.
التنفيذ [Dao] لهذه الواجهة هو كما يلي:
package spring.data.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.google.common.collect.Lists;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class Dao implements IDao {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> addProduits(List<Produit> produits) {
try {
return Lists.newArrayList(produitsRepository.save(produits));
} catch (Exception e) {
throw new DaoException(101, getMessagesForException(e));
}
}
@Override
public void deleteAllProduits() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(102, getMessagesForException(e));
}
}
@Override
public List<Produit> updateProduits(List<Produit> produits) {
try {
return Lists.newArrayList(produitsRepository.save(produits));
} catch (Exception e) {
throw new DaoException(103, getMessagesForException(e));
}
}
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
try {
return Lists.newArrayList(categoriesRepository.save(categories));
} catch (Exception e) {
throw new DaoException(104, getMessagesForException(e));
}
}
@Override
public void deleteAllCategories() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(105, getMessagesForException(e));
}
}
@Override
public List<Categorie> updateCategories(List<Categorie> categories) {
try {
return Lists.newArrayList(categoriesRepository.save(categories));
} catch (Exception e) {
throw new DaoException(106, getMessagesForException(e));
}
}
@Override
public List<Categorie> getAllCategories() {
try {
return Lists.newArrayList(categoriesRepository.findAll());
} catch (Exception e) {
throw new DaoException(107, getMessagesForException(e));
}
}
@Override
public List<Produit> getAllProduits() {
try {
return Lists.newArrayList(produitsRepository.findAll());
} catch (Exception e) {
throw new DaoException(108, getMessagesForException(e));
}
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
try {
return produitsRepository.getProduitByIdWithCategorie(idProduit);
} catch (Exception e) {
throw new DaoException(109, getMessagesForException(e));
}
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
try {
return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
} catch (Exception e) {
throw new DaoException(110, getMessagesForException(e));
}
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
try {
return categoriesRepository.getCategorieByNameWithProduits(nom);
} catch (Exception e) {
throw new DaoException(111, getMessagesForException(e));
}
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
try {
return produitsRepository.getProduitByNameWithCategorie(nom);
} catch (Exception e) {
throw new DaoException(112, getMessagesForException(e));
}
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
try {
return produitsRepository.findOne(idProduit);
} catch (Exception e) {
throw new DaoException(113, getMessagesForException(e));
}
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
try {
return categoriesRepository.findOne(idCategorie);
} catch (Exception e) {
throw new DaoException(114, getMessagesForException(e));
}
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
try {
return produitsRepository.findByNom(nom);
} catch (Exception e) {
throw new DaoException(115, getMessagesForException(e));
}
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
try {
return categoriesRepository.findByNom(nom);
} catch (Exception e) {
throw new DaoException(116, getMessagesForException(e));
}
}
}
- السطر 16: التعليق التوضيحي [@Component] يجعل من الفئة [Dao] مكونًا لـ Spring؛
- الأسطر 19-23: حقن المراجع في واجهتي [CrudRepository] و [Spring Data]. سيتم هذا الحقن عند إنشاء كائنات Spring، وعادةً ما يكون ذلك في بداية تشغيل مشروع Spring؛
- يُلاحظ في السطور 28 و46 أن الطريقة [save] للواجهة [produitsRepository] تُستخدم لإدراج المنتجات وتحديثها على حد سواء. تستخدم [Spring Data] المفتاح الأساسي للمنتج لمعرفة ما إذا كان يجب إجراء إدراج أم تحديث. إذا كان المفتاح الأساسي يساوي [null]، فسيكون الإجراء إدراجًا، وإلا فسيكون تحديثًا؛
- السطر 82: يتم استخدام الطريقة [Lists.newArrayList] من مكتبة Guava للحصول على قائمة بالمنتجات. الطريقة [produitsRepository.findAll()] تُرجع نوع [Iterable<Produit>]؛
- السطر 28: تُرجع الطريقة [produitsRepository.save(produits)] نوعًا [Iterable<Produit>]. وينطبق الأمر نفسه على العمليات الأخرى [save] للفئة؛
في الفئة [Dao] أعلاه، يتم تغليف الاستثناءات التي قد تحدث في النوع [DaoException] التالي:
package spring.data.dao;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
// فئة استثناء لتطبيق الانتخابات
// الاستثناء غير خاضع للتحكم
public class DaoException extends RuntimeException implements Serializable {
// رقم تسلسلي ID
private static final long serialVersionUID = 1L;
// الحقول المحلية
private int code;
private List<String> erreurs;
// المنشئون
public DaoException() {
super();
}
public DaoException(int code, Throwable e) {
// الأصل
super(e);
// محلي
this.code = code;
this.erreurs = getErreursForException(e);
}
public DaoException(int code, String message, Throwable e) {
// الأصل
super(message, e);
// محلي
this.code = code;
this.erreurs = getErreursForException(e);
}
public DaoException(int code, String message) {
// الأصل
super(message);
// محلي
this.code = code;
List<String> erreurs = new ArrayList<>();
erreurs.add(message);
this.erreurs = erreurs;
}
public DaoException(int code, List<String> erreurs) {
// الأصل
super();
// محلي
this.code = code;
this.erreurs = erreurs;
}
// قائمة رسائل الخطأ الخاصة باستثناء
private List<String> getErreursForException(Throwable th) {
// يتم استرداد قائمة رسائل الخطأ الخاصة بالاستثناء
Throwable cause = th;
List<String> erreurs = new ArrayList<>();
while (cause != null) {
// يتم استرداد الرسالة فقط إذا كانت !=null وليست فارغة
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// السبب التالي
cause = cause.getCause();
}
return erreurs;
}
// مُستردات ومُعيّنات
...
}
- السطر 10: الفئة مشتقة من الفئة [RuntimeException] وبالتالي فهي استثناء غير خاضع للرقابة؛
- السطر 16: رمز خطأ؛
- السطر 17: قائمة برسائل الخطأ، تلك المرتبطة بمكدس الاستثناءات التي تسببت في [DaoException]؛
- الأسطر 59-76: تسمح الطريقة الخاصة [getMessagesForException] بالحصول على قائمة رسائل الأخطاء المرتبطة باستثناءات مكدس الاستثناءات. من الممكن بالفعل تكديس الاستثناءات باستخدام منشئات الفئة Exception التالية:
- Exception(String message, Throwable cause): تنشئ استثناءً مع رسالة والاستثناء الذي نريد تغليفه؛
- Exception(Throwable cause): تنشئ استثناءً مع الاستثناء الذي نريد تغليفه؛
النوع [Throwable] هو الفئة الأم للفئة [Exception]. إذا تم تنفيذ المنشئات السابقة بشكل متكرر، فإن الاستثناء النهائي سيحتوي على عدة استثناءات. ويقال عندئذٍ إن لدينا مكدس استثناءات.
- يتم الحصول على السبب الأخير لاستثناء e1 من خلال التعبير [e1.getCause()]؛
- يتم الحصول على السبب قبل الأخير لاستثناء e1 من خلال التعبير [e1.getCause().getCause()]؛
- ونستمر هكذا حتى نحصل على [getCause()==null]؛
11.3.8. تكوين مشروع Spring
![]() |
تقوم الفئة [DaoConfig] بتكوين الطبقة [DAO]:
package spring.data.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {
// الثوابت
final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
final static String USER = "root";
final static String PASSWD = "";
final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };
// مصدر البيانات [tomcat-jdbc]
@Bean
public DataSource dataSource() {
// مصدر البيانات TomcatJdbc
DataSource dataSource = new DataSource();
// تكوين الوصول JDBC
dataSource.setDriverClassName(DRIVER_CLASSNAME);
dataSource.setUsername(USER);
dataSource.setPassword(PASSWD);
dataSource.setUrl(URL);
// اتصال مفتوح في البداية
dataSource.setInitialSize(1);
// النتيجة
return dataSource;
}
// المزود JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(packagesToScan());
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// مدير المعاملات
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
}
تمت مناقشة تكوين مشابه وشرحه في الفقرة 11.2.5. وقد أضفنا إليه التعليقات التوضيحية التالية لـ Spring:
- السطر 17: تُستخدم تعليمة [@EnableJpaRepositories] للإشارة إلى الحزم التي توجد فيها واجهات [CrudRepository] و [Spring Data]؛
- السطر 18: الفئة هي فئة تكوين Spring. هذه المعلومة مهمة. إذا أزلناها، فإن المشروع يعمل. ولكن لاحقًا في الوثيقة، عند إنشاء مشاريع تعتمد على هذا المشروع، فإن بعضها لن يعمل إذا أزيلت التعليقة التوضيحية في السطر 18؛
- السطر 19: تشير التعليقة التوضيحية [@ComponentScan] إلى الحزم التي توجد فيها كائنات Spring. وهي الفئات المُعلَّمة بـ [@Component, @Service, @Controller, ...]. هنا سيتم العثور على مكون Spring [Dao] وإنشاء مثيل له؛
- السطور 73-76: قمنا بتعريف bean يمثل مصفوفة الحزم التي سيتم فحصها للعثور على كيانات JPA. سيسمح ذلك لمشروع يستورد الفئة [DaoConfig] بإعادة تعريف هذا bean وبالتالي تغيير الحزم التي يتم فحصها في السطر 59. سنواجه هذه المشكلة لاحقًا في الوثيقة؛
تقوم الفئة [AppConfig] بتكوين المشروع بأكمله:
package spring.data.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@Import({DaoConfig.class})
public class AppConfig {
// مرشحات jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// أداة التعيين jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// النتيجة
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// مُخطِط jSON
ObjectMapper mapper = new ObjectMapper();
// مرشحات
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// النتيجة
return mapper;
}
}
- السطر 11: الفئة هي فئة تكوين Spring؛
- السطر 12: الذي يستورد الفاصوليا المحددة بواسطة الفئة [DaoConfig] التي رأيناها للتو؛
- تستخدم الطبقة [console] مخططات jSON المحددة هنا؛
- الأسطر 14-64: تحدد خمسة مخططين jSON؛
- الأسطر 15-18: لا يحتوي المرشح jSON [jsonMapper] على أي مرشحات؛
- الأسطر 20-30: يسمح المرشح jSON [jsonMapperCategorieWithProduits] بتسلسل/إلغاء تسلسل كائن [Categorie] مع منتجاته؛
- الأسطر 32-42: يسمح المرشح jSON [jsonMapperProduitWithCategorie] بتسلسل/إلغاء تسلسل كائن [Produit] مع فئته؛
- الأسطر 43-53: يتيح المرشح jSON [jsonMapperCategorieWithoutProduits] تسلسل/إلغاء تسلسل كائن [Categorie] بدون منتجاته؛
- الأسطر 55-64: يسمح المرشح jSON [jsonMapperProduitWithoutCategorie] بتسلسل/إلغاء تسلسل كائن [Produit] بدون فئته؛
تجدر الإشارة إلى أنه عند إنشاء مرشح jSON لكيان T، يجب تكوين ليس فقط مرشح الكيان T، بل أيضًا مرشحات الكيانات Ti التي قد يحتوي عليها.
11.3.9. الطبقة [console]
الطبقة
[DAO]
الطبقة
[console]
طبقة
[JPA]
الطيار
[JDBC]
طبقة
[Spring Data]
Spring 4
SGBD
![]() |
الفئة [Main] هي كما يلي:
package spring.data.console;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
public class Main {
public static void main(String[] args) throws JsonProcessingException {
AnnotationConfigApplicationContext context = null;
try {
// إنشاء مثيل سياق Spring
context = new AnnotationConfigApplicationContext(AppConfig.class);
ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
ObjectMapper.class);
ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
ObjectMapper.class);
ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
ObjectMapper.class);
ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
ObjectMapper.class);
IDao dao = context.getBean(IDao.class);
// --------------------------------------------------------------------------------------
// إفراغ قاعدة البيانات
log("Vidage de la base de données", 1);
// إفراغ الجدول [CATEGORIES] - سيتم إفراغ الجدول [PRODUITS] بشكل متسلسل
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// ملء الجداول
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
dao.addCategories(categories);
// --------------------------------------------------------------------------------------
log("Affichage de la base", 1);
// قائمة الفئات
log("Liste des catégories", 2);
affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
// قائمة المنتجات
log("Liste des produits", 2);
affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
// الفئة 1 مع منتجاتها
Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
log("Catégorie 1 avec ses produits", 2);
affiche(categorie, jsonMapperCategorieWithProduits);
// المنتج [produit14] مع فئته
Produit p = dao.getProduitByNameWithCategorie("produit14");
log("Produit [produit14] avec sa catégorie", 2);
affiche(p, jsonMapperProduitWithCategorie);
// --------------------------------------------------------------------------------------
log("Mise à jour du prix des produits de [categorie1]", 1);
log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
affiche(categorie1, jsonMapperCategorieWithProduits);
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
dao.updateProduits(Lists.newArrayList(produits));
log("Produits de la catégorie [categorie1] après la mise à jour", 2);
affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
// --------------------------------------------------------------------------------------
log("Vidage de la base de données", 1);
// يتم إفراغ الجدول [CATEGORIES] - بشكل متسلسل سيتم إفراغ الجدول [PRODUITS]
dao.deleteAllCategories();
// عرض قاعدة البيانات
log("Liste des categories avant l'ajout", 2);
affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
log("Liste des produits avant l'ajout", 2);
affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// يتم الإدراج
categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
try {
dao.addCategories(Lists.newArrayList(categorie));
} catch (DaoException e) {
System.out.println(e);
}
// التحقق
log("Liste des categories après l'ajout", 2);
affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
log("Liste des produits après l'ajout", 2);
affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
} catch (DaoException e) {
System.out.println(e);
} finally {
if (context != null) {
// انتهى
context.close();
}
}
System.out.println("Travail terminé");
}
// عرض عنصر من النوع T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// عرض قائمة بعناصر من النوع T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// عرض رسالة
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
- السطر 25: إنشاء مثيلات لـ Spring Beans من فئة التكوين [AppConfig]؛
- السطور 26-33: استرداد المراجع على المخططين jSON. نستخدم التوقيع التالي للطريقة [ApplicationContext].getBean:
- [ApplicationContext].getBean(String id, Class classe): التي نستخدمها عندما يكون هناك عدة حبات من النوع [classe]. في هذه الحالة، نحدد معرف الحبة المطلوبة. إذا تم تعريفه باستخدام التعليق التوضيحي [@Bean]، فإن معرّفه هو اسم الطريقة المُعلّقة. وإذا تم تعريفه باستخدام التعليق التوضيحي [@Bean(« identifiant »]، فإن معرّفه هو المعرّف المحدد في التعليق التوضيحي؛
- السطر 34: استرداد مرجع على الطبقة [DAO]؛
- الأسطر 37-39: إفراغ قاعدة البيانات. يتم إفراغ جدول الفئات (السطر 39). لأننا كتبنا:
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
public Set<Produit> produits = new HashSet<Produit>();
عندما يتم حذف فئة، يتم حذف جميع المنتجات المرتبطة بها أيضًا؛
- السطور 43-53: ملء الجدول بفئتين تحتوي كل منهما على 5 منتجات. في السطر 50، سيؤدي إدراج الفئتين في نفس الوقت إلى إدراج منتجاتهما، وذلك أيضًا لأننا كتبنا [cascade = { CascadeType.ALL }]؛
- السطر 58: يتم عرض الفئات. نستخدم أداة التعيين jSON [jsonMapperCategorieWithoutProduits] لعرض الفئات بدون منتجاتها. في الواقع، تعمل الطريقة [dao.getAllCategories()] على عرض الفئات بدون منتجاتها (التحميل المتأخر)؛
- السطر 61: يتم عرض المنتجات بدون فئاتها. في الواقع، تعرض الطريقة [dao.getAllProduits()] المنتجات بدون فئاتها (التحميل المتأخر)؛
- الأسطر 63-65: تعرض الفئة التي تحمل الاسم [categorie1] مع منتجاتها (التحميل الفوري)؛
- الأسطر 67-69: تعرض منتجًا مع فئته؛
- الأسطر 71-81: يتم زيادة جميع أسعار منتجات الفئة [categorie1] بنسبة 10%؛
- الأسطر 91-101: نضيف فئة تحتوي على منتجين يحملان نفس الاسم. لكن في الجدول [PRODUITS]، لدينا قيد فريد على العمود [NOM]. وبالتالي، سيتم رفض إدراج المنتج الثاني وإطلاق استثناء. ومع ذلك، يتم تنفيذ الطريقة [dao.addProduits] في معاملة. لذا، فإن فشل الإدراج الثاني يجب أن يلغي أيضًا إدراج المنتج الأول وكذلك إدراج فئتهما [cat1]. وهذا ما نريد التحقق منه؛
- الأسطر 119-121: طريقة عامة قادرة على عرض السلسلة jSON لأي عنصر من النوع T. يتم التحكم في التسلسل jSON بواسطة أداة التعيين التي يتم تمريرها كمعلمة؛
- الأسطر 124-128: طريقة مماثلة، هذه المرة لقائمة عناصر من النوع T؛
يؤدي تنفيذ الفئة [Main] إلى النتائج التالية (باستثناء سجلات Spring):
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
- الأسطر 4-17: الفئات والمنتجات التي تم إدراجها في الجدول؛
- السطور 18-19: فئة مع منتجاتها؛
- السطور 20-21: منتج مع فئته؛
- الأسطر 22-26: تحديث سعر بعض المنتجات. في السطر 24، نرى أن الأسعار قد ارتفعت بالفعل بنسبة 10٪؛
- السطور 27-36: إضافة الفئة [cat1] مع منتجين يحملان نفس الاسم. نرى أن الجدول هو نفسه قبل (السطور 28-29) وبعد الإضافة (السطور 35-36)، مما يدل على أن جميع عمليات الإدراج في المعاملة قد تم إلغاؤها بالفعل؛
- السطور 31-34: الاستثناء الذي حدث عند إدراج المنتج الثاني وأدى إلى فشل المعاملة بأكملها؛
11.3.10. اختبار الوحدة JUnit
![]() |
![]() |
الفئة [Test01] هي كما يلي:
package spring.data.tests;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// طبقة [DAO]
@Autowired
private IDao dao;
// مرشحات jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// يتم تنظيف قاعدة البيانات قبل كل اختبار
log("Vidage de la base de données", 1);
// يتم إفراغ الجدول [CATEGORIES] - بشكل متسلسل سيتم إفراغ الجدول [PRODUITS]
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// يتم ملء الجداول
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// إضافة الفئة - بشكل متسلسل سيتم إدراج المنتجات أيضًا
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// قائمة الفئات
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// قائمة المنتجات
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// بعض عمليات التحقق
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
...
// -------------- طرق خاصة
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// عرض عنصر من النوع T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// عرض قائمة بعناصر من النوع T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// عرض رسالة
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// العنوان
System.out.println(String.format("%s : ", title));
// الرسائل
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- السطر 27: يتم تكوين الاختبار الفردي بواسطة الفئة [AppConfig] التي سبق عرضها في الفقرة 11.3.8؛
- السطران 32-33: إدخال مرجع على الطبقة [DAO]؛
- الأسطر 36-50: إدخال الخرائط الخمسة jSON؛
- الأسطر 60-71: بعد إفراغ قاعدة البيانات (السطر 57)، يتم ملء قاعدة البيانات بفئتين تحتوي كل منهما على 5 منتجات. يتم تنفيذ هذه الطريقة قبل كل اختبار بسبب التعليق التوضيحي [@Before] في السطر 52؛
- الأسطر 75-93: تعرض محتوى قاعدة البيانات؛
- الأسطر 95-101: يطلب فئة مع منتجاتها، الفئة المحددة باسمها؛
- الأسطر 103-109: تطلب فئة بدون منتجاتها، ويتم تحديد الفئة من خلال اسمها؛
- الأسطر 111-120: يطلب منتجًا مع فئته، ويتم تحديد المنتج برقمه؛
- الأسطر 122-130: يطلب منتجًا بدون فئته، ويتم تحديد المنتج برقمه؛
- الأسطر 133-184: طرق خاصة مشتركة بين الاختبارات المختلفة؛
المهمة المطلوبة: قم بتنفيذ الاختبار. يجب أن ينجح.
11.3.11. إدارة السجلات
يتم تكوين سجلات تطبيق وحدة التحكم أو الاختبار JUnit بواسطة الملف [logback.xml] التالي:
![]() |
يجب أن يكون اسم الملف [logback.xml] وأن يكون موجودًا في مسار فئة المشروع. ولهذا الغرض، تم وضعه هنا في المجلد [src/main/resources] الذي يشكل جزءًا من مسار فئة المشروع. ومحتواه كما يلي:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- يتم تعيين النوع افتراضيًا للمشفرات
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- التحكم في مستوى السجلات -->
<root level="info"> <!-- معلومات، تصحيح أخطاء، تحذير -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- السطر 12: تعرض العلامة [<root level="info">] سجلات المستوى [info]. بدلاً من [info]، يمكن وضع:
- [debug]: هذا هو المستوى الأكثر تفصيلاً للسجلات. يُنصح باستخدامه أثناء مرحلة تصحيح أخطاء المشروع لأن هناك سجلات مهمة جداً حول التبادلات بين العميل والخادم. هذه طريقة لفهم ما يحدث "خلف الكواليس"؛
- [off]: لا توجد سجلات على الإطلاق؛
- [warn]: مستوى سجلات وسيط حيث يعرض Spring حالات شاذة لا تعتبر بالضرورة أخطاء. يجب مراجعتها إذا لم يتم الحصول على النتيجة المتوقعة؛
المهمة المطلوبة: قم بتغيير مستوى السطر 12 إلى [debug] ثم قم بتنفيذ الاختبار الفردي. لاحظ الفرق في السجلات.
11.3.12. إنشاء أرشيف Maven للمشروع
لتثبيت أرشيف المشروع في مستودع Maven المحلي، اتبع الخطوات التالية [1-3]:
![]() |
سيتم إنشاء الأرشيف باستخدام المعرفات الموجودة في الملف [pom.xml]:
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
يمكن العثور على موقع مستودع Maven المحلي في إعدادات Eclipse:
![]() |
ومن ثم يمكن التحقق من التثبيت الصحيح لملف Maven:
![]() |
من الآن فصاعدًا، سيتمكن مشروع Maven محلي آخر من استخدام هذا الأرشيف.





































