4. مقدمة إلى Spring JDBC
في هذا الفصل، سندرس البنية التالية:
![]() |
وهي نفس البنية التي سبق ذكرها. سنقوم بإدخال تعديلين:
- ستحتوي قاعدة البيانات على جدولين مرتبطين بعلاقة مفتاح أجنبي؛
- سيتم تنفيذ الطبقة [DAO] باستخدام مكتبة [Spring JDBC] التي توفر تسهيلات في إدارة API وJDBC؛
4.1. إعداد بيئة العمل
باستخدام STS، قم باستيراد المشروع [spring-jdbc-04] الموجود في المجلد [<exemples>/spring-database-generic/spring-jdbc]
![]() |
بالإضافة إلى ذلك، يتعين علينا إنشاء قاعدة بيانات جديدة MySQL باستخدام العميل [MyManager] (انظر الفقرة 3.1):
![]() |
- في [3]، تعمل الأمثلة التالية على قاعدة بيانات MySQL تُسمى [dbproduitscategories]؛
![]() |
- إلى [9]، أدخل كلمة مرور المستخدم root (كلمة المرور هذه هي root في هذا المستند)؛
![]() |
![]() |
- في [18]، تم إنشاء قاعدة البيانات [dbproduitscategories] فارغة. يتم إنشاء الجداول وتعبئتها باستخدام البرنامج النصي SQL [19-20]؛
![]() |
- في [21]، انتقل إلى المجلد [<exemples>/spring-database-config/mysql/databases]؛
![]() |
- في [25]، تأكد من أنك في قاعدة البيانات [dbproduitscategories] وليس في قاعدة البيانات [dbproduits]؛
- في [29]، أنشأ البرنامج النصي SQL خمس جداول. لن تُستخدم الجداول [ROLES, USERS, USERS_ROLES] إلا عند تناول مسألة تأمين خدمة الويب التي تم إنشاؤها لعرض قاعدة البيانات [dbproduitscategories] على الويب؛
4.2. قاعدة البيانات [dbproduitscategories]
قاعدة البيانات [dbproduitscategories] هي امتداد لقاعدة البيانات [dbproduits] التي تمت دراستها سابقًا. وفي حين أن المنتج في الجدول [PRODUITS] كان له فئة محددة برقم لا يحمل أي معنى خاص، فإن هذا الرقم هنا سيكون مفتاحًا خارجيًا في الجدول [CATEGORIES].
الجدول [PRODUITS] هو كما يلي:
![]() |
- [ID]: المفتاح الأساسي الذي يتم زيادة قيمته تلقائيًا في الجدول [2]؛
- [NOM]: الاسم الفريد للمنتج [4]؛
- [PRIX]: سعر المنتج؛
- [DESCRIPTION]: وصف المنتج؛
- [VERSIONING] هو رقم إصدار المنتج. إصداره الأولي هو 1 [3]. في كل مرة يتم فيها تعديل المنتج، سيتم زيادة رقم إصداره بواسطة البرنامج الذي يستخدم الجدول؛
- [CATEGORIE_ID]: المفتاح الخارجي في الجدول [CATEGORIES] لتحديد الفئة التي ينتمي إليها المنتج؛
![]() |
- في [1-3]، المفتاح الأجنبي [CATEGORIE_ID] من الجدول [PRODUITS]. وهي تستهدف العمود [ID] في الجدول [CATEGORIES] [4-5]؛
- عند حذف فئة ما، يتم حذف جميع المنتجات المرتبطة بها أيضًا [6]. من المهم ملاحظة هذه النقطة لأنها تُستخدم في إنشاء الطبقة [DAO] التي تستفيد من قاعدة البيانات [dbproduitscategories]؛
جدول الفئات [CATEGORIES] هو كما يلي:
![]() |
- [ID]: المفتاح الأساسي الذي يتم زيادة قيمته تلقائيًا؛
- [VERSIONING]: رقم إصدار الفئة؛
- [NOM]: الاسم الفريد للفئة؛
4.3. مشروع Eclipse
![]() |
يُنفذ المشروع [spring-jdbc-04] البنية التالية:
![]() |
مشروع [spring-jdbc-04] هو مشروع Maven تم تكوينه بواسطة الملف [pom.xml] التالي:
![]() |
<?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>dvp.spring.database</groupId>
<artifactId>spring-jdbc-generic-04</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-04</name>
<description>Demo project for Spring JdbcTemplate</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- البحث عن العنصر الأصلي من المستودع -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- تكوين JDBC لـ SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring JdbcTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- الأسطر 28-32: يعتمد المشروع على مشروع [mysql-config-jdbc] الذي يقوم بتكوين الطبقة JDBC؛
- الأسطر 34-37: يجلب العنصر [spring-boot-starter-jdbc] مكتبات Spring JDBC؛
وفي النهاية، تكون التبعيات كما يلي:
![]() |
4.4. تكوين Spring
![]() |
الفئة [AppConfig] التي تهيئ مشروع Spring هي كما يلي:
package spring.jdbc.config;
import generic.jdbc.config.ConfigJdbc;
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.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {
// مصدر البيانات
@Bean
public DataSource dataSource() {
// مصدر البيانات TomcatJdbc
DataSource dataSource = new DataSource();
// تكوين الوصول JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// الاتصالات المفتوحة في البداية
dataSource.setInitialSize(5);
// النتيجة
return dataSource;
}
// مدير المعاملات
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// JdbcTemplate
@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// إدراج المنتج
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
ConfigJdbc.TAB_PRODUITS_ID);
}
// إضافة فئة
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
ConfigJdbc.TAB_CATEGORIES_ID);
}
}
- السطر 16: الفئة هي فئة تكوين Spring؛
- السطر 17: سيتم فحص الحزمة [spring.jdbc.dao] للبحث عن مكونات Spring أخرى غير تلك الموجودة في الفئة [AppConfig]. وسنجد فيها المكون الذي ينفذ الطبقة [DAO]؛
- السطر 18: لن نقوم بإدارة المعاملات بأنفسنا، بل سنترك هذه المهمة لـ Spring JDBC. كل ما علينا فعله هو وضع علامة على الطرق التي يجب تنفيذها ضمن معاملة باستخدام علامة Spring [@Transactional]. يضمن السطر 18 أن يتم التعامل مع هذا التعليق التوضيحي وعدم تجاهله. تتم إدارة المعاملات بواسطة إحدى تبعيات مشروع Spring JDBC الذي تم استيراده بواسطة الملف [pom.xml]؛
- السطر 19: يتم استيراد الفاصوليا (beans) المُعرَّفة مسبقًا في الفئة [generic.jdbc.config.ConfigJdbc] من مشروع [mysql-config-jdbc]؛
- الأسطر 23-36: مصدر البيانات [tomcat-jdbc] الذي تم إدخاله في المثال [spring-jdbc-02]؛
- الأسطر 40-42: مُدير المعاملات المرتبط بمصدر البيانات المُعرَّف سابقًا. يجب أن يُسمى البين بالضرورة [transactionManager] لأن هذا هو الاسم الذي تستخدمه التعليقة التوضيحية [@EnableTransactionManagement]. يتم توفير مدير المعاملات [DataSourceTransactionManager] بواسطة مكتبة Spring JDBC (السطر 12)؛
- الأسطر 45-48: الكائن [namedParameterJdbcTemplate] الذي سيستند إليه تنفيذ الطبقة [DAO]. يتم توفير هذا الكائن من خلال مكتبة Spring JDBC (السطر 10). ويرتبط هذا الكائن أيضًا بمصدر البيانات المحدد سابقًا (السطر 47)؛
- الأسطر 51-55: سيتم استخدام الكائن [simpleJdbcInsertProduit] (اسم حر) لإدراج منتج في الجدول [PRODUITS] واسترداد المفتاح الأساسي الذي تم إنشاؤه. والمعلمات المختلفة المستخدمة هي كما يلي:
- [dataSource]: مصدر البيانات [tomcat-jdbc] المذكور في الأسطر 24-36؛
- [ConfigJdbc.TAB_PRODUITS]: الجدول [PRODUITS]؛
- [ConfigJdbc.TAB_CATEGORIES_ID]: عمود المفتاح الأساسي للجدول [PRODUITS]. تجدر الإشارة إلى أنه بالنسبة لـ PostgreSQL، يجب أن يكون اسم هذا العمود بأحرف صغيرة؛
- السطور 58-62: سيتم استخدام الكائن [simpleJdbcInsertCategorie] لإدراج فئة في الجدول [CATEGORIES] واسترداد المفتاح الأساسي الذي تم إنشاؤه؛
4.5. استثناءات المشروع
![]() |
لقد سبق أن تناولنا الفئات [UncheckedException, DaoException, ShortException] في المشروع [spring-jdbc-03]. ونضيف فئة جديدة:
package spring.jdbc.infrastructure;
public class MyIllegalArgumentException extends UncheckedException {
private static final long serialVersionUID = 1L;
// الشركات المصنعة
public MyIllegalArgumentException() {
super();
}
public MyIllegalArgumentException(int code, Throwable e, String className) {
super(code, e, className);
}
}
- تشتق الفئة [MyIllegalArgumentException] من الفئة [UncheckedException]، وبالتالي فهي فئة غير خاضعة للتحكم. وستُستخدم للإبلاغ عن استدعاء بأرجومنتات غير صحيحة لأحد أساليب الطبقة [DAO]. لم نسمها [IllegalArgumentException] لأن هذا الاستثناء موجود بالفعل في JDK، وكان ذلك يؤدي أحيانًا إلى قيام المُجمِّع بإنشاء [import] غير صحيح؛
4.6. كيانات المشروع
![]() |
الفئات الموجودة في الحزمة [spring.jdbc.entities] هي تمثيلات لصفوف جداول قاعدة البيانات [dbproduitscategories]. وسنتجاهل في الوقت الحالي تمثيلات جداول [USERS, ROLES, USERS_ROLE].
جميع الكيانات تمتد من الفئة الأم [AbstractCoreEntity]:
package spring.jdbc.entities;
public abstract class AbstractCoreEntity {
// الخصائص
protected Long id;
protected Long version;
// الشركات المصنعة
public AbstractCoreEntity() {
}
public AbstractCoreEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
public AbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
public void setAbstractCoreEntity(AbstractCoreEntity entity) {
this.id = entity.id;
this.version = entity.version;
}
// ------------------------------------------------------------
// إعادة تعريف [equals] و [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
return id != null && other.id != null && id.equals(other.id);
}
// الوصول إلى القيم وتعيينها
...
}
- السطر 5: سيتم ربط الحقل [id] بالعمود [ID]، وهو المفتاح الأساسي للجداول؛
- السطر 6: سيتم ربط الحقل [version] بالعمود [VERSIONING] في الجداول؛
- الأسطر 8-26: منشئات وأساليب مختلفة لإنشاء أو تهيئة كائن [AbstractCoreEntity]؛
- الأسطر 35-47: تنص الطريقة [equals] على أن كائنين [AbstractCoreEntity] متساويان إذا كان لهما نفس الحقل [id]. يجب أن نتذكر هنا أن الكائنات [AbstractCoreEntity] ستكون صورًا لصفوف الجداول حيث يُعد [id] المفتاح الأساسي، وبالتالي لا يمكن أن يكون هناك صفان لهما نفس [id]؛
- الصفوف 30-33: اقتراح لـ [hashCode]؛
ستكون الفئة [Produit] صورة لصف واحد من الجدول [PRODUITS]:
package spring.jdbc.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
// الخصائص
private String nom;
private Long idCategorie;
private double prix;
private String description;
private Categorie categorie;
// منشئات
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
super(id, version);
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// التوقيع
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// دالات الاسترجاع والتعيين
...
}
- السطر 6: الفئة [Produit] توسع نطاق الفئة [AbstractCoreEntity]؛
- الأسطر 8-12: الحقول [id, version, nom, idCategorie, prix, description] هي صور للأعمدة [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] في الجدول [PRODUITS]؛
- السطر 12: الكائن من النوع [Categorie] ذو المفتاح الأساسي [idCategorie]. قد يتم ملء هذا الحقل أو لا، حسب الحالة. وعندما يتم ملؤه، يُشار إليه باسم المنتج ذي النسخة الطويلة [LongProduit]، وإلا يُشار إليه باسم المنتج ذي النسخة القصيرة [ShortProduit]؛
- السطر 5: مرشح jSON. تجدر الإشارة إلى أن المشروع [mysql-config-jdbc] يتضمن مكتبة jSON. وتنبع الحاجة إلى المرشح من حقيقة أن الحقل [categorie] قد يكون مملوءًا أو فارغًا. وفي هذه الحالة، يختلف تمثيل المنتج jSON. ولإدارة هاتين الحالتين، سيتم تكوين المرشح [jsonFilterProduit] في السطر 5. ويتيح المرشح jSON تحديد الحقول التي يجب استبعادها من تمثيل jSON بشكل ديناميكي. وعندما يتبين أن الحقل [categorie] لم يتم ملؤه، سيتم استبعاده من تمثيل المنتج jSON؛
الفئة [Categorie] هي صورة لصف واحد من الجدول [CATEGORIES]:
package spring.jdbc.entities;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
// الخصائص
private String nom;
public List<Produit> produits;
// منشئات
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
super(id, version);
this.nom = nom;
this.produits = produits;
}
// التوقيع
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// الطرق
public void addProduit(Produit produit) {
// إضافة منتج
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// يتم إضافة المنتج
produits.add(produit);
// تحديد فئته
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// دالات القراءة والكتابة
...
}
- السطر 9: الفئة [Categorie] تمتد من الفئة [AbstractCoreEntity]؛
- السطر 12: الحقول [id, version, nom] هي صور للأعمدة [ID, VERSIONING, NOM] في الجدول [CATEGORIES]؛
- السطر 13: يمثل الحقل [produits] قائمة المنتجات في الفئة. لا يتم ملء هذا الحقل دائمًا. وعندما لا يتم ملؤه، يُشار إلى الفئة باسم «القائمة المختصرة» [ShortCategorie]، وإلا يُشار إليها باسم «القائمة الكاملة» [LongCategorie]؛
- السطور 32-44: تتيح الطريقة [addProduit] إضافة منتج إلى الفئة (السطر 39) وتحديد خصائص فئته (idCategorie و«categorie») في المنتج المضاف؛
- السطر 8: مرشح jSON. عندما يتعين على المكتبة jSON تسلسل/إلغاء تسلسل كائن [Categorie]، يجب إرشادها إلى كيفية التعامل مع المرشح المسمى [jsonFilterCategorie]؛
4.7. واجهة Idao<T>
![]() |
![]() |
الواجهة [IDao] للطبقة [DAO] لها التوقيع التالي:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.AbstractCoreEntity;
public interface IDao<T extends AbstractCoreEntity> {
// قائمة بجميع كيانات T
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// الكيانات المحددة - النسخة المختصرة
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// الكيانات المحددة - النسخة الطويلة
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// تحديث عدة كيانات
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// حذف جميع الكيانات
public void deleteAllEntities();
// حذف عدة كيانات
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
- السطر 7: لدينا هنا واجهة [IDao] مُحدَّدة بواسطة نوع T مع شرط: يجب أن يمتد هذا النوع إلى الفئة [AbstractCoreEntity] أو ينفِّذ الواجهة [AbstractCoreEntity]. تُستخدم الكلمة الرئيسية [extends] في كلتا الحالتين. هنا، سيتم إنشاء مثيل لـ T إما من النوع [Produit] أو من النوع [Categorie]. في الواقع، نلاحظ بسرعة أننا نقوم بنفس نوع العمليات (الإدراج، التعديل، الحذف، التحديد) على النوعين [Produit] و [Categorie]. لذا يبدو من المنطقي تجميع هذه الطرق في واجهة عامة؛
- حسب الحالة، يشير المصطلحان [LongEntity] و [ShortEntity] إلى حالات مختلفة:
- عندما يكون T هو النوع [Produit]:
- يكون [ShortEntity] هو المنتج دون ملء حقل [Categorie categorie] الخاص به؛
- [LongEntity] هو المنتج مع ملء حقل [Categorie categorie]؛
- عندما يكون T هو النوع [Categorie]:
- [ShortEntity] هي الفئة التي لم يتم ملء حقلها [List<Produit> produits]؛
- [LongEntity] هو المنتج الذي تم ملء حقل [List<Produit> produits] الخاص به؛
- عندما يكون T هو النوع [Produit]:
وبالتالي، لدينا واجهة غنية تضم 19 طريقة. معظم الطرق موجودة بشكل مكرر. لنأخذ مثال الطريقة [getShortEntitiesById]:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- السطران 1 و3: المعلمة هي قائمة المفاتيح الأولية للكيانات التي نريد الحصول على نسخة مختصرة منها. تُعرض هذه القائمة في شكلين مختلفين:
- السطر 1: قائمة تُنفذ واجهة [Iterable<Long>]. ينفذ النوع [List<Long>] هذه الواجهة، لكن هناك أنواعًا أخرى كثيرة. لو كنا قد استخدمنا [List<Long> ids]، لكان ذلك كافيًا لأمثلتنا، لكننا كنا سنجبر مستخدم أمثلةنا على إجراء تحويلات إذا لم يكن المعلمة من النوع المطلوب بالضبط؛
- السطر 3: للأسف، لا يُنفذ النوع Long[] الواجهة [Iterable<Long>]. في هذه الحالة، سنستخدم النسخة الواردة في السطر 3. يمكن أن يتلقى المعلمة الشكلية [Long... ids] (3 نقاط) القيمة سواء من مصفوفة أو من سلسلة من المعرّفات: getShortEntitiesById(id1, id2, ...)؛
وستتم تنفيذ هذه الواجهة نفسها IDao<T> من خلال البنية التالية:
![]() |
حيث ستتدخل طبقة [JPA] (Java Persistence API) بين الطبقة [DAO] ومحرك JDBC الخاص بـ SGBD. وهذا سيسمح لنا بالحصول على طبقة اختبار مشتركة بين البنيتين. في كلتا الحالتين، ستقدم الطبقة [DAO] واجهتين:
- IDao<المنتج> للوصول إلى الجدول [PRODUITS]؛
- IDao<الفئة> للوصول إلى الجدول [CATEGORIES]؛
4.8. تنفيذ واجهة IDao<T>
![]() |
- يتم تنفيذ الواجهة IDao<المنتج> بواسطة الفئة [DaoProduit]؛
- يتم تنفيذ الواجهة IDao<فئة> بواسطة الفئة [DaoCategorie]؛
تتفرع كل من الفئتين [DaoProduit] و [DaoCategorie] من الفئة المجردة [AbstractDao] التالية:
package spring.jdbc.dao;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;
import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {
// إدخالات
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// محلي
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// صحة الوسيطة
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// الحصول على البيانات على دفعات
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// النتيجة
return entities;
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// صحة الوسيطة
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
// النتيجة
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesByName(String... names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Iterable<Long> ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesById(Long... ids) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(Iterable<String> names) {
...
}
@Override
@Transactional(readOnly = true)
public List<T> getLongEntitiesByName(String... names) {
...
}
@Override
@Transactional
public List<T> saveEntities(Iterable<T> entities) {
...
}
@Override
@Transactional
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
...
}
@Override
public void deleteEntitiesById(Iterable<Long> ids) {
...
}
@Override
public void deleteEntitiesById(Long... ids) {
...
}
@Override
public void deleteEntitiesByName(Iterable<String> names) {
...
}
@Override
public void deleteEntitiesByName(String... names) {
...
}
@Override
public void deleteEntitiesByEntity(Iterable<T> entities) {
...
}
@Override
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
...
}
protected void deleteEntitiesByEntity(List<T> entities) {
...
}
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
// الأساليب الخاصة ----------------------------------------------
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
}
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
// الطرق المحمية ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
}
- السطر 15: الفئة [AbstractDao] هي فئة مجردة (الكلمة المفتاحية abstract). وبهذا الصفة، لا يمكن إنشاء مثيل لها. بل يمكن فقط اشتقاقها. لهذه الفئة عدة أدوار:
- تحديد طبيعة المعاملة التي تجري فيها كل طريقة؛
- جعل أكبر قدر ممكن من العناصر مشتركة بين تنفيذي الواجهتين [IDao<Produit>] و [IDao<Categorie>]. ويتعلق الأمر بشكل أساسي بالتحقق من صحة المعلمات. لن يتم قبول المعلمة null، ولا القوائم الفارغة؛
- توحيد نوع المعلمات T... params و Iterable<T> params في نوع واحد: List<T> params؛
- تفويض المهمة إلى الفئات الفرعية بمجرد أن تصبح هذه المهمة خاصة بإحدى الواجهتين؛
بفضل توحيد معلمات الطرق المختلفة الذي قامت به الفئة [AbstractDao]، لن يتعين على الفئتين الفرعيتين [DaoProduit] و [DaoCategorie] سوى تنفيذ 10 طرق بدلاً من 19:
// الطرق التي تنفذها الفئات الفرعية ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
لنلقِ نظرة على بعض الطرق في الفئة [AbstractDao].
الطريقة [getShortEntitiesById]
تهدف هذه الطريقة إلى الحصول على النسخة المختصرة للكيانات التي يتم توفير مفاتيحها الأساسية.
// عمليات الإدخال
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// محلي
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- الأسطر 2-4: يتم إدخال الكائن [maxPreparedStatementParameters] المُعرَّف في ملف التكوين [ConfigJdbc] الذي يقوم بتكوين الطبقة JDBC لـ SGBD معين:
// الحد الأقصى لعدد معلمات [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- الأسطر 1-7: تحدد المكون [maxPreparedStatementParameters] الذي سيحدد الحد الأقصى لعدد المعلمات التي يمكن تزويدها لنوع [PreparedStatement]. لم تظهر هذه الحاجة مع SGBD وMySQL اللذين قبلا 10000 معلمة لنوع [PreparedStatement]. أثناء الاختبارات التي أُجريت مع خادم SGBD وSQL، أطلق الخادم استثناءً يشير إلى أن الحد الأقصى لعدد المعلمات لنوع [PreparedStatement] هو 2100. ولذلك أصبح هذا الرقم معلمة في تكوين مختلف SGBD. لذا يجب إدراجه في مشروع التكوين [sgbd-config-jdbc] لكل SGBD؛
لنعد إلى كود طريقة [getShortEntitiesById]:
// عمليات الحقن
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// محلي
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- السطر 7: اسم الفئة. يُستخدم كمعلمة لأحد منشئات فئة الاستثناء [DaoException]؛
- السطر 10: يشير التعليق التوضيحي [@Transactional(readOnly = true)] إلى أن الطريقة يجب أن تُنفَّذ ضمن معاملة للقراءة فقط. قد يتساءل المرء عن فائدة مثل هذه المعاملة، حيث إن الطريقة لا تقوم إلا بعمليات قراءة، وبالتالي في حالة الفشل، لا يوجد ما يجب إلغاؤه. وقد نصح بذلك مؤلف المكتبة [Spring Data] وشرح السبب. وقد اتبعت نصيحته؛
ونص الدالة كما يلي:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// صحة الوسيطة
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- السطر 5: يتم التحقق من صحة المعلمة [ids] بواسطة الطريقة التالية:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// عناصر فارغة؟
if (elements == null) {
throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
}
// عناصر فارغة؟
if (!elements.iterator().hasNext()) {
if (checkEmpty) {
throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
simpleClassName);
} else {
return new ArrayList<T>();
}
}
// النتيجة الافتراضية
return null;
}
- السطر 1: الطريقة [checkNullOrEmptyArgument] هي طريقة عامة يتم تحديد معلماتها بواسطة النوع <T2>. T2 هو نوع العناصر التي يتم تمريرها كمعلمة ثانية للطريقة. قد يكون هذا النوع هو [Long, String, AbstractCoreEntity]؛
- السطر 1: تقبل الطريقة [checkNullOrEmptyArgument] معلمتين:
- [Iterable<T2> elements]: المعلمة المراد اختبارها؛
- [checkEmpty]: تُعيَّن إلى «صحيح» إذا كان يجب التحقق من أن المعلمة السابقة هي قائمة غير فارغة؛
- الأسطر 4-6: يتم التحقق من أن المعلمة [elements] ليست null. إذا لم يكن الأمر كذلك، يتم إلقاء استثناء من النوع [MyIllegalArgumentException]؛
- الأسطر 8-15: إذا كانت القائمة فارغة وكان من المفترض التحقق من أنها غير فارغة، يتم إلقاء استثناء من النوع [MyIllegalArgumentException]؛
- السطر 13: إذا كانت القائمة فارغة ولم يكن من الضروري التحقق من أنها غير فارغة، يتم إرجاع قائمة فارغة من العناصر من النوع T. تحتوي الواجهة [Iterable<T2>] على طريقة [iterator()] التي تسمح بالتكرار على عناصر القائمة التي تنفذ الواجهة. هناك طريقتان مفيدتان في هذا المكرر:
- [itérateur].hasNext(): تُرجع القيمة «صحيح» إذا كانت القائمة لا تزال تحتوي على عنصر يمكن استخدامه، و«خطأ» في حالة عدم وجود عنصر؛
- [iterateur].next(): تُرجع العنصر الحالي في القائمة وتتقدم عنصرًا واحدًا؛
- وفي النهاية،
- إذا كانت الحجة [T2... elements] هي null أو فارغة، يتم إلقاء استثناء من النوع [MyIllegalArgumentException]؛
- إذا كانت الحجة [T2... elements] قائمة فارغة وكان ذلك صحيحًا، يتم إرجاع قائمة فارغة من العناصر من النوع T؛
توجد طريقة مماثلة عندما تكون الحجة المراد اختبارها من النوع [T2... elements]:
@SuppressWarnings("unchecked")
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
...
}
لنعد إلى كود الطريقة [getShortEntitiesById]:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// صحة الوسيطة
List<T> entities = checkNullOrEmptyArgument(true, ids);
// الحصول على البيانات على دفعات
entities = new ArrayList<T>();
int taille = maxPreparedStatementParameters;
List<Long> listIds = Lists.newArrayList(ids);
int nbIds = listIds.size();
for (int i = 0; i < nbIds; i += taille) {
int limit = Math.min(nbIds, i + taille);
entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
}
// النتيجة
return entities;
}
- السطر 7: إذا وصلنا إلى هنا، فهذا يعني أن الوسيطة [Iterable<Long> ids] صالحة؛
- الأسطر 7-14: سنرى لاحقًا أن الدالة [getShortEntitiesById] سيتم تنفيذها بواسطة نوع [PreparedStatement] الذي سيكون له كمعلمات قائمة بالمفاتيح الأولية المطلوب البحث عنها. على سبيل المثال:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
:ids هو معلمة ستكون قيمتها الفعلية من النوع List<Long>. سيُستخدم كل عنصر من عناصر هذه القائمة كمعلمة «?» في النوع [PreparedStatement]. وقد ذكرنا سابقًا أن هذا النوع يقبل عددًا أقصى من المعلمات، وهو العدد المحدد بواسطة الحقل [maxPreparedStatementParameters] في الفئة؛
- السطر 7: قائمة الكيانات T التي ستُرجعها الطريقة [getShortEntitiesById]. سيتم إنشاء هذه القائمة على شكل مجموعات من عناصر [maxPreparedStatementParameters]؛
- السطر 9: انطلاقًا من الوسيطة [Iterable<Long> ids]، يتم إنشاء نوع [List<Long> listIds]. الفئة [Lists] هي فئة من مكتبة Google Guava التي توفر العديد من الطرق الثابتة لمعالجة مجموعات الكائنات. تم استيراد مكتبة Google Guava (pom.xml) بواسطة مشروع Maven [mysql-config-jdbc]:
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- السطر 10: عدد الكيانات T المطلوب البحث عنها في قاعدة البيانات؛
- الأسطر 11-13: يتم البحث عنها في مجموعات مكونة من [taille = maxPreparedStatementParameters] عنصر؛
- السطر 12: عملية حسابية لتجنب تجاوز نهاية القائمة [listIds]؛
- السطر 13: يتم الحصول على الكيانات T عن طريق استدعاء [getShortEntitiesById(listIds.subList(i, limit))]. يتم تعريف هذه الطريقة في الفئة بواسطة:
abstract protected List<T> getShortEntitiesById(List<Long> ids);
وبالتالي، فإن الفئة الفرعية هي التي ستقوم بالبحث عن الكيانات T في قاعدة البيانات:
- [DaoProduit] إذا كان T من النوع [Produit]؛
- [DaoCategorie] إذا كان T من النوع [Categorie]؛
وتكمن فائدة هذا العمل الذي تقوم به الفئة الأم في أمرين:
- تكون توقيع الطريقة [getShortEntitiesById] في الفئة الفرعية فريدة: حيث تكون حجتها من النوع [List<Long> ids]؛
- لا يتعين على الفئة الفرعية التعامل مع مشكلة معلمات [maxPreparedStatementParameters] الخاصة بـ [PreparedStatement]. فقد تولت الفئة الأم هذه المهمة نيابة عنها؛
- السطر 13: يتم تجميع الكيانات التي تجلبها الفئة الفرعية في قائمة الكيانات التي ستُرجعها الفئة الأم (السطر 16)؛
الآن، لنلقِ نظرة على تنفيذ الطريقة الأخرى [getShortEntitiesById] الخاصة بالفئة:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// صحة الوسيطة
List<T> entities = checkNullOrEmptyArgument(true, ids);
// النتيجة
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- السطر 3: تغيرت طبيعة الوسيطة: Long... ids؛
- السطر 5: يتم اختبار صحة هذه الحجة؛
- السطر 7: يتم استدعاء الطريقة [getShortEntitiesById] التي وصفناها للتو. وهنا أيضًا، نستعين بالفئة [Lists] من المكتبة [Google Guava]. لاحظ أنه يتعين إجراء تحويل صريح إلى النوع [Iterable<Long>] لمساعدة المُجمِّع على اختيار الطريقة الصحيحة، لأن الطريقة [getShortEntitiesById] لها ثلاث توقيعات في الفئة:
- List<T> getShortEntitiesById(Long... ids) ;
- List<T> getShortEntitiesById(Iterable<Long> ids) ;
- List<T> getShortEntitiesById(List<Long> ids) وهي طريقة مجردة ويتم تنفيذها بواسطة الفئة الفرعية؛
لن نعلق أكثر على الفئة المجردة [AbstractDao]، وهي الفئة الأم للفئتين [DaoProduit] و [DaoCategorie]. سنكتفي بالإشارة إلى أنه من المفيد أحيانًا تجميع السلوكيات المشتركة بين عدة فئات في فئة أم، سواء كانت مجردة أم لا. بعد هذا العمل، لم يتبق للفئات الفرعية سوى تنفيذ الطرق التالية:
// الطرق التي تنفذها الفئات الفرعية ----------------------------------------------
abstract protected List<T> getShortEntitiesById(List<Long> ids);
abstract protected List<T> getShortEntitiesByName(List<String> names);
abstract protected List<T> getLongEntitiesById(List<Long> ids);
abstract protected List<T> getLongEntitiesByName(List<String> names);
abstract protected List<T> saveEntities(List<T> entities);
abstract protected void deleteEntitiesById(List<Long> ids);
abstract protected void deleteEntitiesByName(List<String> names);
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllShortEntities();
@Override
@Transactional(readOnly = true)
public abstract List<T> getAllLongEntities();
@Override
public abstract void deleteAllEntities();
يُظهر الكود الوارد في الفقرة 4.8 الأنواع المختلفة من المعاملات المستخدمة لكل طريقة. تجدر الإشارة إلى بعض النقاط:
- الطرق التي تقرأ قاعدة البيانات مُعلَّمة بـ [@Transactional(readOnly = true)]؛
- الطرق التي تُعدّل قاعدة البيانات مُعلَّمة بـ [@Transactional]؛
- الطرق التي تحمل الرمز [delete] غير مُعلَّمة، وبالتالي لا تتم ضمن معاملة. والفكرة هي أنه في حالة فشل عملية حذف ما، فمن غير المرجح أن يرغب المستخدم في إلغاء جميع العمليات التي نجحت قبل ذلك؛
4.9. الفئة [DaoCategorie]
![]() |
![]() |
تنفذ الفئة [DaoCategorie] الواجهة [IDao<Categorie>] التي تضمنالوصول إلى بيانات الجدول [CATEGORIES] في قاعدة البيانات MySQL [dbproduitscategories]. وهيكلها الأساسي هو كما يلي:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
// الثوابت
// عمليات الحقن
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
@Autowired
private IDao<Produit> daoProduit;
@Override
public List<Categorie> getAllShortEntities() {
...
}
@Override
public List<Categorie> getAllLongEntities() {
...
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
...
}
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
...
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
...
}
// --------------------- أدوات التعيين
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}
class LongCategorieMapper implements RowMapper<Categorie> {
....
}
- السطر 28: الفئة [DaoCategorie] هي مكون Spring، وبهذه الصفة يمكن حقنها في مكونات Spring أخرى؛
- السطر 29: الفئة [DaoCategorie] تمتد من الفئة المجردة [AbstractDao<Categorie>]، مما يجعلها تنفيذًا للواجهة [IDao<Categorie>]؛
- الأسطر 34-37: حقن حبات (beans) محددة في الفئة [AppConfig] الموصوفة في الفقرة 4.4؛
- الأسطر 38-39: حقن مرجع إلى الفئة [DaoProduit] التي تنفذ الواجهة [IDao<Produit>] التي تدير الوصول إلى بيانات الجدول [PRODUITS]؛
- الأسطر 41-89: تنفيذ الواجهة [IDao<Categorie>]؛
- الأسطر 95-101: فئتان داخليتان تنفذان الواجهة [RowMapper<T>]؛
دعونا ندرس الطرق واحدة تلو الأخرى.
4.9.1. الطريقة [getAllShortEntities]
تُرجع الطريقة [getAllShortEntities] جميع الفئات الموجودة في الجدول [CATEGORIES] في صيغتها المختصرة:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
تعتمد جميع الطرق على الكائن [namedParameterJdbcTemplate] المُعرَّف في ملف تكوين Spring والمُقدَّم من مكتبة Spring JDBC. وتوجد طرق عديدة لهذا الكائن. والطريقة المستخدمة أعلاه هي التالية:
![]()
- [sql] هي الأمر SQL المطلوب تنفيذه؛
- [rowMapper] هي مثيل للواجهة [RowMapper<T>] التالية:

الفكرة هي كما يلي:
- تقوم الطريقة [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) الأمر SQL من النوع [Select]. وهي تتعامل مع أي استثناءات محتملة، بالإضافة إلى فتح/إغلاق الاتصال بـ SGBD. الشيء الوحيد الذي لا يمكنها فعله هوتغليف عناصر [ResultSet] من الكائنات التي تحصل عليها في نوع [Categorie] لأنها لا تعرف الارتباط الموجود بين حقول النوع [Categorie] وأعمدة [Resultset]. وسنرى لاحقًا أن هذه العلاقة يتم إنشاؤها باستخدام تقنية JPA، مما سيجعل تغليف عناصر [ResultSet] في مثيلات من النوع T أمرًا تلقائيًا. في الوقت الحالي، المعلمة الثانية للطريقة [query] هي مثيل لواجهة [RowMapper<T>] قادرة على إجراء هذا التغليف؛
لنعد إلى الكود:
@Override
public List<Categorie> getAllShortEntities() {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
الترتيب SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] هو كما يلي:
public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";
يطلب الاستعلام الأعمدة [ID, VERSIONING, NOM] من عناصر الجدول [CATEGORIES]. سنستخدم بشكل منهجي الصيغة التالية:
SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...
المهم هو تسمية الأعمدة التي تم الحصول عليها بواسطة SELECT باستخدام السمة [as nom_colonne]. هذه هي الطريقة الوحيدة لضمان قابلية النقل بين عمليات SGBD، لأن كل منها تتبع طريقة خاصة بها في تسمية الأعمدة الناتجة عن عملية SELECT، حيث تحمل أعمدة الجداول المختلفة نفس الاسم (مثل ID أو NOM أو VERSIONING في حالتنا هذه). وبذلك نزيل هذا الغموض من خلال تحديد الاسم الذي يجب أن تحمله هذه الأعمدة بأنفسنا.
الفئة الداخلية [ShortCategorieMapper] هي كما يلي:
class ShortCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
}
}
- السطر 1: الفئة [ShortCategorieMapper] تُنفذ الواجهة [RowMapper<Categorie>]، وبناءً على ذلك يجب أن تُنفذ الطريقة [mapRow] الواردة في الأسطر 4-5، والتي تتمثل مهمتها في تغليف سطر من [ResultSet rs] الناتج عن الأمر [SELECT] في نوع [Categorie]؛
- السطر 5: تم إجراء هذا التغليف. تجدر الإشارة إلى أن الاسم المستخدم في الطرق [rs.getType(nom)] هو نفس الاسم المستخدم في سمات [as nom] لأعمدة SELECT؛
وبذلك حصلنا على قائمة الفئات في صيغتها المختصرة دون الحاجة إلى معالجة الاستثناءات أو إدارة الاتصال. وهذا هو الفائدة من مكتبة Spring JDBC التي تتولى إدارة كل ما يمكن تجريده في إدارة عناصر الجدول، وتترك للمطور ما لا يمكن تجريده.
4.9.2. الطريقة [getAllLongEntities]
تُرجع الطريقة [getAllLongEntities] جميع فئات الجدول [CATEGORIES] بصيغتها الطويلة:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
الترتيب SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] هو كما يلي:
public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";
ويتمثل الهدف في ربط الفئات بمنتجاتها. ويتحقق ذلك عن طريق إجراء ربط بين الجدول [CATEGORIES] والجدول [PRODUITS] عبر المفتاح الأجنبي [CATEGORIE_ID] الذييربط الجدول [PRODUITS] بالجدول [CATEGORIES]. تسمح صيغة الاستعلام [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] أيضًا باسترداد الفئات التي لا ترتبط بأي منتجات. في هذه الحالة، يعرض الاستعلام SELECT فئة ومنتجًا مع جميع أعمدة NULL.
الفئة [LongCategorieMapper] هي كما يلي:
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// حالة الفئة التي لا تحتوي على منتجات
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
- السطر 4: يجب أن تُرجع الطريقة [mapRow] كائنًا [Categorie] مع ملء حقل [produits] الخاص به، وذلك استنادًا إلى سطر من كائن [ResultSet] المستمد من الأمر SELECT السابق؛
وفي النهاية، العملية:
[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]
ستُنتج قائمة من النوع:
حيث ستحتوي كل فئة [ci] على حقل [produits] الذي سيكون قائمة منتجات تحتوي على عنصر واحد فقط [produitsij]. لكننا نحتاج إلى القائمة التالية:
حيث تحتوي كل فئة [ci] على حقل [produits] الذي يمثل قائمة المنتجات [produiti1, produiti2, ...]. ويتم الحصول على ذلك عن طريق تمرير قائمة الفئات التي تم الحصول عليها إلى دالة خاصة [filterCategories]:
@Override
public List<Categorie> getAllLongEntities() {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(223, e, simpleClassName);
}
}
الطريقة [filterCategories] هي كما يلي:
private List<Categorie> filterCategories(List<Categorie> categories) {
if (categories.size() == 0) {
return categories;
}
// الفئات المطلوب عرضها
List<Categorie> cats = new ArrayList<Categorie>();
// يتم تصفح قائمة الفئات التي تم الحصول عليها
for (Categorie categorie : categories) {
boolean trouve = false;
for (Categorie cat : cats) {
if (categorie.equals(cat)) {
cat.addProduit(categorie.getProduits().get(0));
trouve = true;
break;
}
}
// هل تم العثور عليها؟
if (!trouve) {
cats.add(categorie);
}
}
// النتيجة
return cats;
}
- السطر 1: [List<Categorie> categories] هي قائمة الفئات المراد تصفية (أو تجميع)؛
- السطر 6: قائمة الفئات التي سيتم إرجاعها إلى المتصل؛
- الأسطر 8-21: تتم معالجة كل فئة من قائمة الفئات المراد تصفية؛
- الأسطر 10-16: يتم البحث عما إذا كانت الفئة الحالية [categorie] موجودة بالفعل في قائمة الفئات [cats] المقرر إنشاؤها (يُذكر أن فئتين تُعتبران متطابقتين إذا كان لهما نفس المفتاح الأساسي، انظر الفقرة 4.6)؛
- الأسطر 11-14: إذا كان الأمر كذلك بالفعل، يُضاف المنتج المُغلف في [categorie] إلى قائمة منتجات [cat]؛
- الأسطر 18-20: إذا لم تكن الفئة الحالية [categorie] موجودة بالفعل في قائمة الفئات [cats] المقرر إنشاؤها، يتم إضافتها إليها مع قائمة منتجاتها التي تحتوي على عنصر واحد؛
لننظر إلى الحالة التي يعرض فيها الأمر SQL Select فئات لا ترتبط بها أي منتجات. ما الكيان الذي يُرجع الفئة [LongCategorieMapper]؟
class LongCategorieMapper implements RowMapper<Categorie> {
@Override
public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
List<Produit> produits = new ArrayList<Produit>();
long idProduit = rs.getLong("p_ID");
// حالة الفئة التي لا تحتوي على منتجات
if (!rs.wasNull()) {
produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
}
categorie.setProduits(produits);
return categorie;
}
}
في الحالة التي يعرض فيها الأمر SQL Select فئة بدون منتجات، تحتوي أعمدة المنتج المعروضة مع الفئة جميعها على القيمة SQL NULL. تتم معالجة هذه الحالة في الأسطر 7-9:
- السطر 7: يتم استرداد المفتاح الأساسي للمنتج كعدد صحيح طويل؛
- السطر 9: يتم التحقق مما إذا كانت القيمة التي تمت قراءتها هي القيمة SQL NULL (rs.wasNull). وإذا لم يكن الأمر كذلك، يُضاف المنتج إلى القائمة الواردة في السطر 6، وإلا فلا يُضاف شيء وتبقى قائمة المنتجات فارغة.
تجدر الإشارة إلى أنه في جميع الحالات، يتم إرجاع فئة تحتوي على حقل [produits] الذي يختلف عن null.
4.9.3. الطريقة [getShortEntitiesById]
الطريقة [getShortEntitiesById] مشابهة للطريقة [getAllShortEntities]، باستثناء أنها لا تُرجع سوى الكيانات التي تُحدد مفاتيحها الأساسية في قائمة:
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
- في السطر 4، توقيع الطريقة [query] المستخدمة هو كما يلي:

المعلمة الأولى هي أمر SQL [Select] معلمات محددة. والمعلمة الثانية هي قاموس يربط كل معلمة بقيمة. المعلمة الثالثة هي مثيل الفئة الذي يقوم بتغليف سطر من [ResultSet] الناتج عن [Select] في كائن من النوع T؛
- السطر 4: الأمر SQL [Select] المُعدُّ هو كما يلي:
public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";
يستخرج هذا الأمر من الجدول [CATEGORIES] الفئات التي توجد مفاتيحها الأساسية في القائمة: ids.
- السطر 5: المعلمة الثانية للطريقة [query] هي هنا قاموس يربط المفتاح 'ids' (المعلمة الأولى) بالقائمة [ids] التي تم تمريرها في السطر 1 كمعلمة إلى الطريقة [getShortEntitiesById]. تنتمي الفئة [Collections] إلى المكتبة [Google Guava] التي سبق أن تحدثنا عنها. تُرجع [Collections.singleMap] قاموسًا مكونًا من عنصر واحد؛
- السطر 5: الفئة المسؤولة عن تغليف سطر من [ResultSet] الناتج عن [Select] في كائن من النوع [Categorie] هي الفئة [ShortCategorieMapper] التي سبق دراستها؛
وهنا بالذات يأتي دور الكائن [maxPreparedStatementParameters]. ففي الواقع، يمكن أن يحتوي المعلمة [:ids] التابعة للأمر SQL — والتي تمثل قائمة بالمفاتيح الأساسية — على ما بين 1 إلى عدة آلاف من المعلمات. وهناك حد أقصى لهذا العدد يعتمد على كل SGBD. بالنسبة لـ MySQL، تمكنا من تمرير 10000 معلمة دون حدوث أخطاء، ولم نختبر ما يزيد عن ذلك. بالنسبة لخادم SQL، فإن الحد الرسمي هو 2100. أما بالنسبة لـ Firebird، فقد كان الرقم 1000 أكثر من اللازم، لذا تم تخفيضه إلى 100. وبشكل عام، لم يتم اختبار الحد الأقصى لهذا العدد بالنسبة لمختلف ملفات SGBD.
4.9.4. طريقة [getLongEntitiesById]
طريقة [getLongEntitiesById] مشابهة لطريقة [getShortEntitiesById]، باستثناء أنها تعرض النسخ الطويلة للفئات:
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
Collections.singletonMap("ids", ids), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
السطر 4، الاستعلام SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] هو كما يلي:
public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";
4.9.5. الطريقة [getShortEntitiesByName]
الطريقة [getShortEntitiesByName] مشابهة للطريقة [getShortEntitiesById]، باستثناء أن البحث عن الفئات يتم عبر أسمائها بدلاً من مفاتيحها الأساسية:
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new ShortCategorieMapper());
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
في السطر 4، يكون الترتيب SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] كما يلي:
public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";
4.9.6. الطريقة [getLongEntitiesByName]
الطريقة [getLongEntitiesByName] مشابهة للطريقة [getShortEntitiesByName]، باستثناء أن الفئات يتم البحث عنها في صيغتها الكاملة:
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
Collections.singletonMap("noms", names), new LongCategorieMapper()));
} catch (Exception e) {
throw new DaoException(215, e, simpleClassName);
}
}
في السطر 4، يكون الترتيب SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] كما يلي:
public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";
4.9.7. الطريقة [deleteAllEntities]
تقوم الطريقة [deleteAllEntities] بحذف جميع الفئات من الجدول [CATEGORIES]:
@Override
public void deleteAllEntities() {
try {
// يتم حذف جميع الفئات وبالتالي جميع المنتجات
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
- السطر 4: الطريقة [namedParameterJdbcTemplate.update] المستخدمة لها التوقيع التالي:
![]()
المعلمة الأولى هي أمر SQL مُهيأ للتحديث (INSERT، UPDATE، DELETE). المعلمة الثانية هي القاموس الذي يربط القيم بمعلمات الأمر SQL المختلفة. تُرجع هذه الطريقة عدد الأسطر التي تم تحديثها بواسطة الأمر SQL.
- السطر 4: الأمر SQL [ConfigJdbc.DELETE_ALLCATEGORIES] هو كما يلي:
public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";
وبالتالي، فهي ليست أمرًا مُحدَّد المعلمات. ولهذا السبب، فإن المعلمة الثانية للطريقة [update] لها القيمة null.
4.9.8. الطريقة [deleteAllEntitiesById]
تقوم الطريقة [deleteAllEntitiesById] بحذف الفئات من الجدول [CATEGORIES] الذي يتم تمرير مفاتيحه الأساسية إليها:
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
السطر 4، الأمر SQL [ConfigJdbc.DELETE_CATEGORIESBYID] هو كما يلي:
public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";
4.9.9. الطريقة [deleteAllEntitiesByName]
تقوم الطريقة [deleteAllEntitiesByName] بحذف الفئات من الجدول [CATEGORIES] التي يتم تمرير أسمائها:
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
} catch (Exception e) {
throw new DaoException(225, e, simpleClassName);
}
}
السطر 4، الأمر SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] هو كما يلي:
public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";
4.9.10. الطريقة [saveEntities]
4.9.10.1. الرمز
توقيع هذه الطريقة هو كما يلي:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
تستقبل الطريقة قائمة بالفئات كمعلمة. وتقوم بالعمليات التالية عليها:
- إذا كانت الفئة تحتوي على مفتاح أساسي null، يتم تنفيذ العملية SQL INSERT، وإلا يتم تنفيذ العملية SQL UPDATE؛
- يتم تكرار هذه العملية لكل منتج من منتجات الفئة؛
تُرجع الطريقة قائمة بالفئات التي تم حفظها أو تحديثها. القائمة المُرجعة هي صورة طبق الأصل للفئات والمنتجات الموجودة في الجداول مع مراعاة الاختلافات في الإصدارات: فهذه الإصدارات لا يتم تعديلها في الكيانات التي تم تحديثها، على الرغم من أنها قد تمت زيادتها في قاعدة البيانات.
وهذه هي الطريقة الأكثر تعقيدًا على الإطلاق. وفيما يلي كودها:
@Override
protected List<Categorie> saveEntities(List<Categorie> entities) {
try {
// --------------------------------------------- الفئات
List<Categorie> insertCategories = new ArrayList<Categorie>();
List<Categorie> updateCategories = new ArrayList<Categorie>();
// يتم مسح الفئات
for (Categorie categorie : entities) {
// إدراج أم تحديث؟
if (categorie.getId() == null) {
insertCategories.add(categorie);
} else {
updateCategories.add(categorie);
}
}
// إدراج الفئات
if (insertCategories.size() > 0) {
insertCategories(insertCategories);
}
// تحديث الفئات
if (updateCategories.size() > 0) {
updateCategories(updateCategories);
}
// --------------------------------------------- المنتجات
// يتم تحديث المنتجات في الفئات
List<Produit> allProduits = new ArrayList<Produit>();
for (Categorie categorie : entities) {
List<Produit> produits = categorie.getProduits();
Long idCategorie = categorie.getId();
if (produits != null) {
// إضافتها إلى قائمة جميع المنتجات
allProduits.addAll(produits);
// يتم مسح المنتجات واحدة تلو الأخرى لربطها بفئاتها
for (Produit produit : produits) {
// ربط المنتج بفئته
produit.setIdCategorie(idCategorie);
produit.setCategorie(categorie);
}
}
}
// إدراج/تحديث المنتجات
daoProduit.saveEntities(allProduits);
// النتيجة
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(207, e, simpleClassName);
}
}
- الأسطر 5-23: إدراج أو تحديث الفئات؛
- الأسطر 26-43: إدراج أو تحديث المنتجات؛
- الأسطر 35-39: يربط هذا الكود كل منتج بفئته. في المرحلة السابقة لإدراج الفئات، تم تخصيص مفتاح أساسي لكل فئة، ويجب إدخاله في الحقل [idCategorie] الخاص بالمنتج (السطر 37). من ناحية أخرى، تتيح السطور 37-38 تصحيح الحالات التي لم يقم فيها المستدعي بربط كل منتج بفئته بشكل صحيح. ولكي تكون هذه العلاقة صحيحة، يجب استخدام الطريقة [Categorie] .add(المنتج p)، ولكن لا شيء يمنع المستخدم من إضافة منتج مباشرةً إلى قائمة منتجات الفئة دون المرور بهذه الطريقة، مع المخاطرة بأن تكون الحقول [idCategorie, categorie] الخاصة بالمنتج p غير مملوءة بشكل صحيح؛
- السطر 43: يتم تفويض مثيل واجهة [IDao<Produit>] بمهمة حفظ / تحديث المنتجات. تجدر الإشارة إلى أن هذا المثيل قد تم إدراجه في الفئة [DaoCategorie]:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. إدراج الفئات
يتم إدراج الفئات في الجدول [CATEGORIES] بواسطة الطريقة الخاصة [insertCategories] التالية:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// الفئات المطلوب إضافتها
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// يتم حفظ المفتاح الأساسي
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// كل شيء هو OK - يتم تعيين المفاتيح الأساسية للفئات الدائمة
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// النتيجة
return categories;
}
- السطر 6: يتم استخدام المكون [simpleJdbcInsertCategorie] الذي تم حقنه في الفئة عبر الأسطر التالية:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
يتم تعريف هذا الكائن في الفئة [AppConfig] الخاصة بالمشروع على النحو التالي:
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
@Bean
public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
.usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
- السطر 5، الفئة [SimpleJdbcInsert] هي فئة من مكتبة Spring JDBC (السطر 1):
- معلمة منشئ [SimpleJdbcInsert] هي مصدر البيانات الذي يتم العمل عليه؛
- تسمح الجملة [withTableName] بتحديد الجدول الذي نريد إدراج عنصر فيه، وهو هنا الجدول [CATEGORIES]؛
- تسمح الجملة [usingGeneratedKeyColumns] بتحديد عمود المفتاح الأساسي الذي تم إنشاؤه تلقائيًا، وهو في هذه الحالة العمود [ID]؛
- تسمح الجملة [usingColumns] بتقييد الإدراج على أعمدة معينة. هنا يتم استبعاد العمود [ID] الذي يتم إنشاؤه تلقائيًا بواسطة SGBD، والعمود [VERSIONING] الذي قيمته الافتراضية هي 1؛
لنعد إلى كود الأسلوب [insertCategories]:
private List<Categorie> insertCategories(List<Categorie> categories) {
Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
try {
// الفئات المراد إضافتها
for (Categorie categorie : categories) {
Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
// يتم حفظ المفتاح الأساسي
mapCategories.put(newId.longValue(), categorie);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// كل شيء هو OK - يتم تعيين المفاتيح الأساسية للفئات الدائمة
for(Long id : mapCategories.keySet()){
Categorie categorie=mapCategories.get(id);
categorie.setId(id);
}
// النتيجة
return categories;
}
- السطر 6: يتم استخدام الأسلوب [simpleJdbcInsertCategorie.executeAndReturnKey]:
![]()
تتوقع الطريقة كمعلمة قاموسًا يربط بين أعمدة الجدول والقيم المراد إدراجها فيها. وتُرجع كنتيجة المفتاح الأساسي في شكل نوع [Number]. تسمح الطريقة [Number.longValue()] بالحصول على المفتاح الأساسي في شكل نوع [Long].
الطريقة [getMapForCategorie] هي الطريقة الخاصة التالية:
private Map<String, ?> getMapForCategorie(Categorie categorie) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
return map;
}
مفاتيح القاموس هي أسماء الأعمدة التي يجب ملؤها [NOM]، وقيم القاموس هي القيم التي يجب إدراجها في هذه الأعمدة.
- السطر 8 [insertCategories]: يتم تخزين المفتاح الأساسي المسترد في قاموس. سننتظر حتى نتأكد من إدراج جميع الكيانات قبل تخصيص مفاتيحها الأساسية لها. في الواقع، في حالة حدوث استثناء، سيتم إلغاء جميع عمليات الإدراج، ونريد عندئذٍ أن تظل الكيانات [categories] في السطر 1 دون تغيير أيضًا؛
- الأسطر 14-17: الآن بعد أن تأكدنا من أن كل شيء سار على ما يرام، نقوم بتعيين المفاتيح الأولية التي تم إنشاؤها للفئات؛
- السطر 19: نُرجع قائمة الفئات مع مفاتيحها الأساسية؛
4.9.10.3. تحديث الفئات
يتم تحديث الفئات باستخدام الطريقة الخاصة التالية [updateCategories]:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// تحديث الفئة في قاعدة البيانات
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// هل نجحنا؟
Long idCategorie = null;
if (nbLignes == 0) {
// لم ننجح - نبحث عن السبب
// نبحث عن الفئة في قاعدة البيانات
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// الفئة غير موجودة
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// الإصدار غير صحيح
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
لا يُسمح بتحديث فئة C1 في قاعدة البيانات باستخدام فئة C2 الموجودة في الذاكرة إلا إذا كانت الفئتان C1 و C2 من نفس الإصدار. يُستخدم رقم الإصدار هذا لمنع التحديث المتزامن للكيان من قِبل مستخدمين مختلفين: يقوم مستخدمان هما U1 وU2 بقراءة الكيان E الذي يحمل رقم إصدار يساوي V1. يقوم المستخدم U1 بتعديل الكيان E وحفظ هذا التعديل في قاعدة البيانات: فيتغير رقم الإصدار عندئذٍ إلى V1+1. U2 يقوم بدوره بتعديل E ويحفظ هذا التعديل في قاعدة البيانات: سيتلقى استثناءً لأنه يمتلك إصدارًا (V1) مختلفًا عن الإصدار الموجود في قاعدة البيانات (V1+1).
- الأسطر 2-29: تحتوي جملة try على جملتي catch:
- الأول، في السطر 25، موجود للسماح بمرور الاستثناء المحتمل من النوع [DaoException] الذي أطلقه الكود في السطر 13؛
- والثاني، في السطر 27، موجود لمعالجة أنواع الاستثناءات الأخرى؛
- السطر 3: يتم مسح جميع الفئات المطلوب تحديثها؛
- السطر 4: يتم تحديث الفئة الحالية باستخدام الطريقة [namedParameterJdbcTemplate.update]:

- لنحلل الأمر:
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES, new BeanPropertySqlParameterSource(categorie));
التسلسل SQL [ConfigJdbc.UPDATE_CATEGORIES] هو كما يلي:
public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";
يحتوي الأمر على ثلاثة معلمات (:id، :version، :nom) توجد قيمها في الحقول التي تحمل نفس الأسماء في الكائن [categorie] المعدل. يتم استخدام هذه الميزة عن طريق تمرير [new BeanPropertySqlParameterSource(categorie)] كمعلمة ثانية، مما يعني أن «قيم المعلمات موجودة في الحقول التي تحمل نفس الأسماء في هذا الكائن Java bean»؛
والنتيجة التي تُنتج عن هذه العملية، عندما تتم بشكل طبيعي، هي عدد الأسطر التي تم تعديلها، أي 0 أو 1.
لنعد إلى الكود الذي ندرسه:
private void updateCategories(List<Categorie> categories) {
try {
for (Categorie categorie : categories) {
// تحديث الفئة في قاعدة البيانات
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
new BeanPropertySqlParameterSource(categorie));
// هل نجحنا؟
Long idCategorie = null;
if (nbLignes == 0) {
// لم ننجح - نبحث عن السبب
// نبحث عن الفئة في قاعدة البيانات
idCategorie = categorie.getId();
List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
if (categoriesInBd.size() == 0) {
// الفئة غير موجودة
throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
idCategorie));
} else {
// الإصدار لم يكن صحيحًا
throw new RuntimeException(String.format(
"Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
- السطر 9: نتحقق مما إذا كان التعديل قد نجح؛
- السطر 10: لم ينجح التعديل. وبما أن الشرط [WHERE] يشمل العمودين [ID] و [VERSIONING]، فإننا نبحث عن العمود الذي تسبب في فشل [WHERE]؛
- الأسطر 12-18: يتم التحقق من وجود المفتاح [id] الخاص بالفئة في قاعدة البيانات. إذا لم يكن موجودًا، يتم تشغيل [RuntimeException] مع رسالة خطأ مناسبة؛
- الأسطر 19-22: تعالج الحالة التي تكون فيها الإصدارة هي غير الصحيحة؛
4.10. الفئة [DaoProduit]
![]() |
![]() |
تقوم الفئة [DaoProduit] بتنفيذ الواجهة [IDao<Produit>] التي تضمنالوصول إلى بيانات الجدول [PRODUITS] في قاعدة البيانات MySQL [dbproduitscategories]. وهيكلها الأساسي هو كما يلي:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
// عمليات الإدراج
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
@Override
public List<Produit> getAllShortEntities() {
...
}
@Override
public List<Produit> getAllLongEntities() {
....
}
@Override
public void deleteAllEntities() {
...
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
....
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
....
}
@Override
protected void deleteEntitiesByName(List<String> names) {
...
}
}
// --------------------- أدوات التعيين
class ShortProduitMapper implements RowMapper<Produit> {
...
}
class LongProduitMapper implements RowMapper<Produit> {
...
}
يشبه الكود إلى حد كبير كود الفئة [DaoCategorie]. لن ندرس سوى بعض الطرق.
4.10.1. الطريقة [getShortEntitiesById]
تقوم الطريقة [getShortEntitiesById] بإرجاع النسخة المختصرة للمنتجات التي يتم تمرير مفاتيحها الأساسية إليها:
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
Collections.singletonMap("ids", ids), new ShortProduitMapper());
} catch (Exception e) {
throw new DaoException(109, e, simpleClassName);
}
}
- السطر 4: الأمر SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] هو كما يلي:
public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
- السطر 4: الفئة [ShortProduitMapper] المكلفة بتضمين [ResultSet] في قائمة المنتجات هي كما يلي:
class ShortProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
}
}
4.10.2. الطريقة [getLongEntitiesByName]
تقوم الطريقة [getShortEntitiesById] بعرض النسخة المطولة للمنتجات التي يتم تمرير أسمائها:
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
Collections.singletonMap("noms", names), new LongProduitMapper());
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
- السطر 4: الأمر SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] هو كما يلي:
public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
- السطر 4: الفئة [LongProduitMapper] المكلفة بتغليف عناصر [ResultSet] في منتجات، النسخة الطويلة، هي كما يلي:
class LongProduitMapper implements RowMapper<Produit> {
@Override
public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
}
}
4.10.3. الطريقة [saveEntities]
تُستخدم الطريقة [saveEntities] بشكل متبادل لإدراج منتجات جديدة (id==null) أو تحديث المنتجات الموجودة (id!=null):
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
try {
// المنتجات المطلوب إدراجها
List<Produit> insertProduits = new ArrayList<Produit>();
// المنتجات المطلوب تحديثها
List<Produit> updateproduits = new ArrayList<Produit>();
// يتم مسح قائمة الكيانات المستلمة
for (Produit produit : entities) {
Long id = produit.getId();
if (id == null) {
insertProduits.add(produit);
} else {
updateproduits.add(produit);
}
}
// الإضافات
insertProduits(insertProduits);
// التعديلات
updateProduits(updateproduits);
// النتيجة
return entities;
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
في السطر 18، يتم إدراج المنتجات باستخدام الطريقة الخاصة التالية [insertProduits]:
private List<Produit> insertProduits(List<Produit> produits) {
Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
try {
// المنتجات المطلوب إضافتها
for (Produit produit : produits) {
Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
// تدوين المفتاح الأساسي
mapProduits.put(newId.longValue(), produit);
}
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// كل شيء هو OK - يتم تعيين المفاتيح الأساسية للمنتجات المحفوظة
for (Long id : mapProduits.keySet()) {
Produit produit = mapProduits.get(id);
produit.setId(id);
}
// النتيجة
return produits;
}
private Map<String, ?> getMapForProduit(Produit produit) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
return map;
}
هذه الطريقة مشابهة للطريقة [insertCategories] التي تمت دراستها في الفقرة 4.9.10.3.
- السطر 4: يتم استخدام المكون [simpleJdbcInsertProduit] الذي تم إدراجه في الفئة:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertProduit;
تم تعريف هذا البين في الفئة [AppConfig] التي تقوم بتكوين المشروع:
@Bean
public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
return new SimpleJdbcInsert(dataSource)
.withTableName(ConfigJdbc.TAB_PRODUITS)
.usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
.usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
- الأسطر 3-6: الكائن [simpleJdbcInsertProduit]
- مرتبط بمصدر بيانات قاعدة البيانات [dbproduitscategories] (السطر 3)، وبالجدول [ConfigJdbc.TAB_PRODUITS] في هذا المصدر (السطر 4)؛
- يتم إنشاء المفتاح الأساسي لهذه الجدولة في العمود [ConfigJdbc.TAB_PRODUITS_ID] (السطر 5)؛
- يتم تعيين القيم فقط في الأعمدة [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (السطر 6)؛
أما الطريقة [updateProduits] التي تقوم بتحديث المنتجات (السطر 20 من [saveEntities]) فهي كما يلي:
private void updateProduits(List<Produit> updateProduits) {
try {
// يتم مسح المنتجات
for (Produit produit : updateProduits) {
// تحديث المنتج في قاعدة البيانات
int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
new BeanPropertySqlParameterSource(produit));
// هل نجحنا؟
Long idProduit = null;
if (nbLignes == 0) {
// لم ينجح الأمر - نبحث عن السبب
// البحث عن المنتج في قاعدة البيانات
idProduit = produit.getId();
List<Produit> produitsInBd = getShortEntitiesById(idProduit);
if (produitsInBd.size() == 0) {
// المنتج غير موجود
throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
idProduit));
} else {
// الإصدار غير صحيح
throw new RuntimeException(String.format(
"Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
}
}
}
} catch (DaoException e) {
throw e;
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
وهي مشابهة لتلك التي تقوم بتحديث الفئات (انظر الفقرة 4.9.10.3). في السطر 23، الأمر SQL [ConfigJdbc.UPDATE_PRODUITS] الذي تم تنفيذه لتحديث المنتجات هو التالي:
public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";
أسماء معلمات [:id,:version,:nom,:prix,:idCategorie,:description] هي أيضًا أسماء حقول الفئة [Produit]، مما يسمح باستخدام التعليمات الواردة في السطرين 6 و7 لتحديث المنتج الحالي.
4.11. طبقة الاختبارات
![]() |
![]() |
تتكون طبقة الاختبارات من ثلاث فئات اختبار:
- [JUnitTestCheckArguments]: تستدعي اختبارات هذه الفئة الطرق المختلفة في الطبقة [DAO] باستخدام معلمات غير صالحة، وتتحقق من استجابتها بشكل صحيح؛
- [JUnitTestDao]: تستدعي اختبارات هذه الفئة الطرق المختلفة في الطبقة [DAO] وتتحقق من أنها تؤدي المهام المتوقعة؛
- لا تهدف [JUnitTestPushTheLimits] إلى اختبار الطبقة [DAO] بل إلى قياس أدائها؛
تلعب طبقة الاختبارات هذه دورًا كبيرًا في هذا المستند. فهي مشتركة بالفعل بين جميع تطبيقات واجهة [IDao<T>]. يوجد ستة منها لكل SGBD (تطبيق واحد لـ JDBC، و3 تطبيقات لـ JPA، تنفيذ واحد لـ Spring MVC، وتنفيذ واحد لـ Spring MVC الآمن)، أي ما مجموعه 36 اختبارًا للستة SGBD التي تم اختبارها. تتيح لنا طبقة الاختبارات التحقق من أن جميع التنفيذات تستجيب بنفس الطريقة.
4.11.1. اختبار [JUnitTestCheckArguments]
تحتوي فئة الاختبار [JUnitTestCheckArguments] على 48 طريقة تختبر استجابة طرق طبقة [DAO] عند استدعائها بوسائط غير صحيحة. وهيكلها الأساسي هو كما يلي:
package spring.jdbc.tests;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestCheckArguments {
// طبقة [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// بيانات محلية
private Iterable<String> names1 = null;
private Iterable<String> names2 = Lists.newArrayList(new String[0]);
private String[] names3 = null;
private String[] names4 = new String[0];
private Iterable<Long> ids1 = null;
private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
private Long[] ids3 = null;
private Long[] ids4 = new Long[0];
private Iterable<Categorie> categories1 = null;
private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
private Categorie[] categories3 = null;
private Categorie[] categories4 = new Categorie[0];
private Iterable<Produit> produits1 = null;
private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
private Produit[] produits3 = null;
private Produit[] produits4 = new Produit[0];
...
}
- السطر 19: سيتم إجراء الاختبار JUnit بالتكامل مع إطار عمل Spring؛
- السطر 18: قبل إجراء الاختبارات، سيتم إنشاء مثيلات للـ beans المُعرَّفة في فئة [AppConfig] الخاصة بالمشروع؛
- الأسطر 23-26: حقن مثيل لكل من واجهتي الطبقة [DAO]؛
- الأسطر 29-44: معلمات استدعاء طرق الطبقة [DAO] غير صحيحة؛
- السطر 29: مؤشر null من النوع [Iterable<String>] كقائمة أسماء؛
- السطر 30: قائمة فارغة من النوع [Iterable<String>] كقائمة أسماء؛
- السطر 29: مؤشر null من النوع String[] كمصفوفة أسماء؛
- السطر 30: مصفوفة فارغة من النوع String[] كمصفوفة أسماء؛
- ...
باستخدام الحقل [names1]، يمكن إجراء الاختبار التالي على سبيل المثال:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- السطر 1: نحدد أن الاختبار [getShortProduitsByName1] يجب أن يواجه استثناءً من النوع [MyIllegalArgumentException]
باستخدام الحقل [names2]، نجري على سبيل المثال الاختبار التالي:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName2() {
daoCategorie.getLongEntitiesByName(names2);
}
باستخدام الحقل [names3]، يمكن إجراء الاختبار التالي على سبيل المثال:
@Test(expected = MyIllegalArgumentException.class)
public void getLongCategoriesByName3() {
daoCategorie.getLongEntitiesByName(names3);
}
باستخدام الحقل [names4]، يمكن إجراء الاختبار التالي على سبيل المثال:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName4() {
daoProduit.getShortEntitiesByName(names4);
}
وبذلك نجري 48 اختبارًا لاختبار جميع الحالات الممكنة. نقوم بتنفيذ تكوين التشغيل المسمى [spring-jdbc-generic-04-JUnitTestCheckArguments] [1]. والنتيجة التي تم الحصول عليها هي التالية [2]:
![]() |
4.11.2. الاختبار [JUnitTestDao]
يستدعي الاختبار [JUnitTestDao] أساليب الطبقة [DAO] باستخدام معلمات صالحة، ويتحقق من أن هذه الأساليب تؤدي المهام المطلوبة منها. ويوجد إجمالاً 74 اختباراً تتحقق من عمليات الإدراج والاختيار والتحديث والحذف للكيانات أو الفئات أو المنتجات. يبلغ إجمالي عدد أسطر الكود أكثر من 1000 سطر. وسندرس فقط بعضًا من هذه الطرق.
4.11.2.1. هيكل الاختبار
تحتوي الفئة [JUnitTestDao] على الهيكل التالي:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao {
// سياق Spring
@Autowired
private ApplicationContext context;
// الطبقة [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// الثوابت
private final int NB_PRODUITS = 5;
private final int NB_CATEGORIES = 2;
// محلي
// محلي
private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
@Before
public void clean() {
// يتم مسح قاعدة البيانات قبل كل اختبار
log("Vidage de la base de données", 1);
// يتم إفراغ الجدول [CATEGORIES] وبالتالي الجدول [PRODUITS]
daoCategorie.deleteAllEntities();
// يتم إفراغ القواميس
for (Long id : mapCategories.keySet()) {
mapCategories.remove(id);
}
for (Long id : mapProduits.keySet()) {
mapProduits.remove(id);
}
}
...
}
- السطران 27-28: كما هو الحال في الاختبار [JUnitTestCheckArguments]، فإننا نتعامل هنا مع اختبار مدمج مع Spring ومُهيأ بواسطة الفئة [AppConfig] الخاصة بالمشروع؛
- السطران 32-33: حقن سياق Spring الذي يتيح الوصول إلى جميع مكوناته (beans)؛
- السطران 35-36: حقن مثيل واجهة [IDao<Produit>] التي تختبرها الفئة؛
- السطران 37-38: حقن مثيل واجهة [IDao<Categorie>] التي تختبرها الفئة؛
- السطران 41-42: عندما يحتاج الاختبار إلى بيانات في قاعدة البيانات، سيتم إنشاء قاعدة بيانات من فئة [NB_CATEGORIES] تحتوي كل منها على منتجات من فئة [NB_PRODUITS]. وبذلك سيكون لدينا [NB_CATEGORIES] فئات في الجدول [CATEGORIES] و [NB_CATEGORIES] * [NB_PRODUITS] منتجات في الجدول [PRODUITS]؛
- السطران 46-47: قاموسان سيتم تخزين المنتجات والفئات فيهما؛
- السطور 49-62: تُنفَّذ الدالة [clean] قبل كل اختبار (السطر 49). في السطر 54، يتم إفراغ الجدول [CATEGORIES]. يجب أن نتذكر هنا أن الجدول [PRODUITS] له مفتاح أساسي [CATEGORIE_ID] في العمود ID من الجدول [CATEGORIES]، وأن هذا المفتاح الأساسي مُعرَّف على النحو التالي؛
![]() |
- (تابع)
- في [1-3]، المفتاح الأجنبي [CATEGORIE_ID] من الجدول [PRODUITS]. وهي تستهدف العمود [ID] في الجدول [CATEGORIES] [4-5]؛
- عند حذف فئة ما، يتم حذف جميع المنتجات المرتبطة بها أيضًا [6]. من المهم ملاحظة هذه النقطة لأنها تُستخدم في إنشاء الطبقة [DAO] التي تستفيد من قاعدة البيانات [dbproduitscategories]؛
لذلك، عند حذف محتوى الجدول [CATEGORIES]، سيتم أيضًا حذف محتوى الجدول [PRODUITS].
- الأسطر 56-58: يتم إفراغ قاموس الفئات؛
- الأسطر 59-61: نفعل الشيء نفسه مع قاموس المنتجات؛
تجدر الإشارة إلى أنه قبل كل اختبار، تكون قواعد البيانات فارغة والقواميس في الذاكرة فارغة.
4.11.2.2. الطريقة [verifyClean]
تتحقق الطريقة [verifyClean] من أن الجداول فارغة بعد تنفيذ الطريقة [clean]:
@Test
public void verifyClean() {
log("verifyClean", 1);
List<Categorie> categories = daoCategorie.getAllShortEntities();
Assert.assertEquals(0, categories.size());
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.2.3. الطريقة [fillDataBase]
تتحقق هذه الطريقة من ملء قاعدة البيانات بشكل صحيح ببيانات الاختبار:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// يتم ملء قاعدة البيانات والقواميس
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// العرض
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// بعض عمليات التحقق
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// يجب أن تكون القواميس قد استنفدت
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
يستخدم هذا الاختبار عدة طرق خاصة:
- [fill] السطر 4 الذي يملأ قاعدة البيانات ببيانات الاختبار؛
- [registerCategories] السطر 4 الذي يملأ القواميس بالبيانات التي ترجعها الطريقة [fill]. يمثل هذان القاموسان الكيانات الدائمة؛
- [showDataBase]، السطر 6، الذي يقرأ الجدولين [CATEGORIES] و [PRODUITS] ويعرض ما قرأه؛
- [checkShortCategorie] السطر 13 يتحقق من الفئة التي قرأتها [showDataBase]. ويتحقق من أن النسخة المختصرة لهذه الفئة تتطابق مع ما تم تسجيله في قاموس الفئات؛
- [checkShortProduit] السطر 16 يقوم بنفس الشيء بالنسبة للمنتجات؛
- عند العثور على كيان ما في أحد القواميس، يتم حذفه من القاموس. تتحقق السطران 19-20 من أن القاموسين فارغان. إذا كانت هاتان الحالتان صحيحتين، فهذا يعني أن:
- أن جميع القيم التي قرأتها [showDataBase] قد تم العثور عليها بالفعل في القواميس؛
- أن القاموسين لا يحتويان على كيانات أخرى غير تلك التي تمت قراءتها؛
الطريقة الخاصة [fill] هي كما يلي:
private List<Categorie> fill(int nbCategories, int nbProduits) {
// ملء الجداول
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// إضافة الفئة - بشكل متسلسل، سيتم أيضًا إدراج المنتجات
// إدراجها
categories = daoCategorie.saveEntities(categories);
// النتيجة
return categories;
}
- الأسطر 3-12: يتم إنشاء قائمة من [nbCategories] فئة، تحتوي كل منها على [nbProduits] منتج؛
- السطر 15: يتم حفظ قائمة الفئات هذه. وقد رأينا أن الطريقة [daoCategorie.saveEntities] تحفظ أيضًا المنتجات التابعة للفئات عندما تحتوي على منتجات؛
- السطر 17: يتم إرجاع قائمة الفئات التي تم حفظها. أصبحت الكيانات المحفوظة (الفئات والمنتجات) تمتلك الآن مفتاحًا أساسيًا في حقلها [id]؛
ستقوم الطريقة الخاصة [registerCategories] بإدراج هذه الكيانات في القاموسين:
private void registerCategories(List<Categorie> categories) {
// القواميس
for (Categorie categorie : categories) {
mapCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
mapProduits.put(produit.getId(), produit);
}
}
}
مفتاح الوصول لكل قاموس هو المفتاح الأساسي للكيانات.
وبعد ذلك، سيتم قراءة قاعدة البيانات التي تم ملؤها مسبقًا وعرضها بواسطة الطريقة الخاصة التالية [showDataBase]:
private Object[] showDataBase() throws BeansException, JsonProcessingException {
// قائمة الفئات
log("Liste des catégories", 2);
List<Categorie> categories = daoCategorie.getAllShortEntities();
affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
// قائمة المنتجات
log("Liste des produits", 2);
List<Produit> produits = daoProduit.getAllShortEntities();
affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
// النتيجة
return new Object[] { categories, produits };
}
- السطران 4 و8: يتم استرداد النسخ المختصرة للفئات والمنتجات؛
- السطر 11: يتم إرجاع مصفوفة تحتوي على قائمتي الكيانات المسترجعتين؛
- السطران 5 و9: يتم عرض قوائم الكيانات باستخدام الطريقة الخاصة التالية [affiche]:
// عرض قائمة بالعناصر من النوع T
private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, mapper);
}
}
// عرض عنصر من النوع T
private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
System.out.println(mapper.writeValueAsString(element));
}
يتم عرض الكيانات بواسطة مُعَيِّن jSON (السطر 10). هذا المُعَيِّن هو المعلمة الثانية للطريقة [affiche]، السطر 2. يُعرِّف سياق Spring أربعة مُعَيِّنات jSON في الملف [ConfigJdbc] التابع لتبعية Maven [mysql-config-jdbc]:
// عوامل التصفية jSON -------------------------------------
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongCategorie() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperShortProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
return jsonMapper;
}
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
ObjectMapper jsonMapperLongProduit() {
ObjectMapper jsonMapper = jsonMapper();
jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
return jsonMapper;
}
- تحتوي أدوات التعيين هذه jSON (الأسطر 7-9، 16-18، 26-28، 35-37) على سمة
[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)]
التي تجعلها «بينات» يتم إنشاء مثيل لها عند كل طلب يتم إرساله إلى سياق Spring. هذا أمر جديد. فجميع «بينات» Spring التي رأيناها حتى الآن كانت «سينجلتونات»: أي تم إنشاؤها بنسخة واحدة فقط، وكان يتم إرجاع هذه النسخة في كل مرة يتم فيها طلب مرجع لها من سياق Spring. لماذا هذا التغيير؟ في الواقع، تقوم الفئات الأربع [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] بتكوين المُعَيِّن الوحيد jSON (وهو هنا فعلاً فئة فردية) المُعرَّف في الأسطر 2-5. ويجب إعادة تكوين هذا المُعَيِّن عند كل استدعاء لأي من الفئات الأربع السابقة، وليس مرة واحدة فقط عند تهيئة السياق. لو قررنا استخدام أربعة مُعَيِّنات مختلفة jSON، واحدة لكل من الحبوب الأربعة، لكان من الممكن أن تكون هذه الحبوب كائنات فردية. كان ذلك ممكنًا تمامًا. وكنا سنكتب عندئذٍ في الأسطر 10 و19 و29 و38:
ObjectMapper jsonMapper = new ObjectMapper();
- تُستخدم المُعَدِّلات الأربعة json لتكوين المرشحات jSON للكيانات [Produit] و [Categorie]. وقد كتبنا بالفعل (انظر الفقرتين 4.6 و 4.6) ما يلي:
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {
و
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
يتم التحكم في تمثيل jSON للكيان [Categorie] بواسطة المرشح jSON [jsonFilterCategorie]، أما تمثيلالكيان [produit] بواسطة المرشح jSON [jsonFilterProduit]. تقوم أدوات التعيين الأربعة jSON في سياق Spring بتكوين هذين المرشحين على النحو التالي:
- يقوم المُعَيِّن [jsonMapperShortCategorie] بتكوين المرشحين jSON و[jsonFilterCategorie] لإصدار مختصر من الفئة: لن يتم تضمين الحقل [produits] في تمثيل الفئة jSON؛
- يقوم المُعِد [jsonMapperLongCategorie] بتكوين المرشح jSON [jsonFilterCategorie] لإصدار طويل من الفئة: سيتم تضمين الحقل [produits] في تمثيل الفئة jSON؛
- يقوم المُعِد [jsonMapperShortProduit] بتكوين المرشح jSON و [jsonFilterProduit] لإصدار مختصر للمنتج: لن يتم تضمين الحقل [categorie] في تمثيل المنتج jSON؛
- يقوم المُعِد [jsonMapperLongProduit] بتكوين المرشح jSON و [jsonFilterProduit] لإصدار طويل من المنتج: سيتم تضمين الحقل [categorie] في تمثيل المنتج jSON؛
لقد انتهينا من الطريقة الخاصة [showDataBase]. لنعد إلى كود الاختبار [fillDataBase]:
@Test
public void fillDataBase() throws BeansException, JsonProcessingException {
// ملء قاعدة البيانات والقواميس
registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
// العرض
Object[] data = showDataBase();
List<Categorie> categories = (List<Categorie>) data[0];
List<Produit> produits = (List<Produit>) data[1];
// بعض عمليات التحقق
Assert.assertEquals(NB_CATEGORIES, categories.size());
Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
for (Categorie categorie : categories) {
checkShortCategorie(categorie);
}
for (Produit produit : produits) {
checkShortProduit(produit);
}
// يجب أن تكون القواميس قد استُنفدت
Assert.assertEquals(0, mapCategories.size());
Assert.assertEquals(0, mapProduits.size());
}
- الأسطر 6-8: نسترد الإصدارات المختصرة للمنتجات والفئات التي تمت قراءتها من قاعدة البيانات؛
- الأسطر 10-11: عمليات التحقق الأولية؛
- الأسطر 12-14: يتم فحص كل فئة يتم إرجاعها بواسطة الدالة [showDataBase] من خلال الدالة الخاصة التالية [checkShortCategorie]:
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
// لا يمكن اختبار الحقل [produits] بشكل قابل للنقل مع تطبيقات jPA
}
- السطر 1: [Categorie actual] هي الفئة التي تم قراءتها من قاعدة البيانات والتي يجب أن تكون مطابقة للفئة الموجودة في القاموس [mapCategories]؛
- السطر 2: يتم استرداد المفتاح الأساسي للفئة التي تمت قراءتها؛
- السطر 3: يتم استرداد الفئة المسجلة بهذا المفتاح الأساسي في قاموس الفئات؛
- السطر 4: يتم حذف المفتاح من القاموس للتأكد من أن أي فئة أخرى يتم قراءتها لا تستخدم هذا المفتاح نفسه؛
- السطر 5: يتم التحقق من أن الفئتين تحملان الاسم نفسه؛
يتم التحقق من النسخة المختصرة للمنتجات التي تُرجعها الطريقة [showDataBase] بواسطة الطريقة الخاصة التالية [checkShortProduit]:
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
// لا يمكن اختبار الحقل [categorie] بشكل قابل للنقل باستخدام تطبيقات jPA
}
- السطر 1: [Produit actual] هو المنتج المختصر الذي تمت قراءته من قاعدة البيانات؛
- السطران 2-3: يتم استرداد المنتج ذي المفتاح الأساسي نفسه من قاموس المنتجات الدائمة؛
- السطر 4: يتم حذف الإدخال الذي تم العثور عليه من القاموس؛
- الأسطر 5-8: يتم التحقق من أن المنتجين لهما نفس قيم الحقول؛
4.11.2.4. الطريقة [getLongCategoriesByName3]
هذا الاختبار هو التالي:
@Test
public void getLongCategoriesByName3() {
// ملء القاعدة
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// اختبار
log("getLongCategoriesByName3", 1);
List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
Assert.assertEquals(2, categories2.size());
registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
for (Categorie categorie : categories) {
checkLongCategorie(categorie);
}
Assert.assertEquals(0, mapCategories.size());
}
- السطر 4: يتم ملء قاعدة البيانات واسترداد قائمة الفئات والمنتجات المحفوظة؛
- السطر 7: نختبر الطريقة [daoCategorie.getLongEntitiesByName(Iterable<String> names)] في الطبقة [DAO]. نطلب قائمة بمنتجين محددين بأسمائهما الكاملة؛
- السطر 8: نتحقق من أن القائمة التي تم إرجاعها بواسطة [daoCategorie.getLongEntitiesByName(Iterable<String> names)] تحتوي بالفعل على عنصرين؛
- السطر 9: يتم إدراج العنصرين اللذين تم حفظهما في السطر 4 في قاموس الفئات؛
- الأسطر 10-12: يتم التحقق من أن العنصرين اللذين تمت قراءتهما هما بالفعل العنصران اللذان تم حفظهما؛
- السطر 13: يتم التحقق من أن قاموس الفئات فارغ، مما يعني في الوقت نفسه أن جميع الفئات التي تمت قراءتها قد تم العثور عليها في القاموس وأنه لا يحتوي على أي قيم لم تتم قراءتها؛
السطر 11، تتحقق الطريقة [checkLongCategorie] من النسخة المطولة لإحدى الفئات:
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = mapCategories.get(actual.getId());
mapCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
- السطر 6 يتحقق من أن الحقل [produits] الخاص بالفئة ليس null. في الواقع، يؤدي قراءة فئة بتنسيقها الكامل دائمًا إلى إرجاعها بحقل [produits] وليس null. إذا لم تحتوي الفئة على أي منتجات، فإن الحقل [produits] يكون قائمة فارغة ولكنها موجودة؛
4.11.2.5. الطريقة [updateDataBase1]
@Test
public void updateDataBase1() {
// التعبئة
fill(NB_CATEGORIES, NB_PRODUITS);
// اختبار
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
List<Produit> produits = categorie1.getProduits();
Map<Produit, Long> versions = new HashMap<Produit, Long>();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
versions.put(produit, produit.getVersion());
}
daoProduit.saveEntities(produits);
// مراجعة
List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
.getProduits();
Assert.assertEquals(produits.size(), produitsInBd.size());
// التحقق
for (Produit produit2 : produitsInBd) {
Produit produit = findProduitByName(produit2.getNom(), produits);
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
}
}
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
تقوم الطريقة [updateDataBase1] بزيادة سعر منتجات الفئة المسماة categorie[1] بنسبة 10% وتتحقق من أمرين:
- أن السعر الأساسي قد تغير بالفعل؛
- أن إصدار المنتج المحدث قد زاد بمقدار 1؛
يقوم الكود بما يلي:
- السطر 4: ملء قاعدة البيانات؛
- السطر 7: استرداد الفئة المسماة «categorie[1]» من قاعدة البيانات؛
- الأسطر 8-13: زيادة سعر جميع منتجاتها بنسبة 10% (السطر 11). بالإضافة إلى ذلك، يتم إنشاء قاموس يربط كل منتج بإصداره (السطران 9 و12)؛
- السطر 14: يتم استدعاء الأسلوب [daoProduit.saveEntities]. وسيقوم هذا الأسلوب بتحديث المنتجات؛
- السطر 16: يتم استرداد منتجات الفئة المسماة «categorie[1]» من قاعدة البيانات؛
- الأسطر 20-24: بالنسبة لجميع منتجات هذه الفئة، يتم التحقق من أن السعر قد تم تعديله بالفعل (السطر 22) وأن الإصدار قد تمت زيادته بمقدار 1 (السطر 23)؛
4.11.2.6. الطريقة [deleteProduitsByProduit1]
تقوم الطريقة [deleteProduitsByProduit1] بحذف المنتجات من الجدول [PRODUITS]:
@Test
public void deleteProduitsByProduit1() {
// التعبئة
fill(NB_CATEGORIES, NB_PRODUITS);
// حذف
daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
// التحقق
List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
Assert.assertEquals(0, produits.size());
}
- السطر 6: يتم حذف منتجين؛
- السطران 8-9: يتم التحقق من أنهما لم يعودا موجودين في قاعدة البيانات؛
4.11.2.7. الطريقة [getLongProduitsById3]
@Test
public void getLongProduitsById3() {
// التعبئة
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
// اختبار
log("getLongProduitsById3", 1);
List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
Assert.assertEquals(2, produits.size());
registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
for (Produit produit : produits) {
checkLongProduit(produit);
}
Assert.assertEquals(0, mapProduits.size());
}
- السطر 4: يتم ملء قاعدة البيانات واسترداد قائمة الفئات المخزنة؛
- السطر 7: يتم استرداد النسخة الكاملة لمنتجين محددين بأسمائهما من قاعدة البيانات؛
- السطر 9: يتم إدراج المنتجين [produit[0,3], produit[1,4]] الموجودين في قائمة الفئات في السطر 4 في قاموس المنتجات؛
- السطر 10: يتم البحث عن هذين المنتجين في قاعدة البيانات باستخدام مفاتيحهما الأساسية؛
- الأسطر 11-14: يتم التحقق من أن البيانات التي تمت قراءتها مطابقة للبيانات المسجلة في القاموس؛
الطريقة الخاصة [checkLongProduit] هي كما يلي:
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = mapProduits.get(id);
mapProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertNotNull(actual.getCategorie());
}
4.11.2.8. Conclusion
سنتوقف عند هذا الحد. يوجد 74 اختبارًا حتى الآن ويمكن إضافة المزيد لأنني ربما نسيت بعض الحالات التي يجب اختبارها. ورغم أن هذه الاختبارات ليست شاملة، إلا أنها سمحت باكتشاف العديد من الأخطاء، وعادةً ما تكون حالات حدية لم يتم تصورها عند كتابة الطبقة [DAO] في البداية. تعد مرحلة الاختبارات الشاملة أمرًا لا غنى عنه في أي مشروع.
لتنفيذ الاختبار، يمكن استخدام تكوين التشغيل المستورد والمسمى [spring-jdbc-generic-04.JUnitTestDao].
![]() | ![]() |
4.11.3. الاختبار [JUnitTestPushTheLimits]
الاختبار [JUnitTestPushTheLimits] هو اختبار للأداء. نستفيد من حقيقة أن الاختبارات JUnit تعرض وقت تنفيذها لقياس أداء الطبقة [DAO]. ثم تُقارن هذه الأداءات بأداء تطبيقات JPA للطبقة [DAO].
4.11.3.1. Squelette
الهيكل الأساسي لفئة [JUnitTestPushTheLimits] هو كما يلي:
package spring.jdbc.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestPushTheLimits {
// طبقة [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
// الثوابت
private final int NB_CATEGORIES = 2500;
private final int NB_PRODUITS = 2;
// محلي
private Map<Long, Categorie> hCategories;
private Map<Long, Produit> hProduits;
@Before
public void clean() {
// يتم إفراغ الجدول [CATEGORIES]
daoCategorie.deleteAllEntities();
// قواميس
hCategories = new HashMap<Long, Categorie>();
hProduits = new HashMap<Long, Produit>();
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// يتم ملء الجداول
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// إضافة الفئة - سيتم إدراج المنتجات أيضًا بشكل متسلسل
categories = daoCategorie.saveEntities(categories);
// قواميس
for (Categorie categorie : categories) {
hCategories.put(categorie.getId(), categorie);
for (Produit produit : categorie.getProduits()) {
hProduits.put(produit.getId(), produit);
}
}
// النتيجة
return categories;
}
....
// -------------------- طرق خاصة
private void checkLongProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
Assert.assertNotNull(actual.getCategorie());
}
private void checkShortProduit(Produit actual) {
Long id = actual.getId();
Produit expected = hProduits.get(id);
hProduits.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
boolean erreur = false;
try {
actual.getCategorie().getNom();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkShortCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
boolean erreur = false;
try {
actual.getProduits().size();
} catch (Exception e) {
erreur = true;
}
Assert.assertTrue(erreur);
}
private void checkLongCategorie(Categorie actual) {
Long id = actual.getId();
Categorie expected = hCategories.get(actual.getId());
hCategories.remove(id);
Assert.assertEquals(expected.getNom(), actual.getNom());
Assert.assertNotNull(actual.getProduits());
}
}
ونجد هنا الهيكل الأساسي للفئة [JUnitTestDao]. وقد سبق أن تعرفنا على جميع هذه الطرق. يعمل الاختبار على قاعدة بيانات تضم 2500 فئة تحتوي كل منها على منتجين (السطران 32-33). وبالتالي، ستحتوي الجدولة [CATEGORIES] على 2500 سطر، بينما ستحتوي الجدولة [PRODUITS] على 5000 سطر. كان من الممكن إضافة المزيد من الأسطر، لكن مدة الاختبار تستغرق بالفعل ما يقرب من دقيقة واحدة. ولذلك، تم اختيار قيم مقبولة للمستخدم الذي ينتظر انتهاء الاختبار.
يبلغ إجمالي عدد الاختبارات 18 اختبارًا. يتم تنفيذها باستخدام إعدادات التشغيل [1]. تظهر مدد التنفيذ في [2]:
![]() |
4.11.3.2. doNothing [0,114]
لا تقوم الطريقة [doNothing] بأي شيء. فهي تسمح بقياس مدة الطريقة [clean] التي يتم تنفيذها قبل كل اختبار وتقوم بإفراغ قاعدة البيانات. في ما سبق، نرى أن مدة هذه العملية قصيرة نسبيًا مقارنةً بالعمليات الأخرى.
@Test
public void doNothing() {
// تنظيف
}
4.11.3.3. perf01 [4,179]
يُستخدم الاختبار [perf01] لقياس الوقت المستغرق في ملء قاعدة البيانات:
@Test
public void perf01() {
// إدراج
fill(NB_CATEGORIES, NB_PRODUITS);
}
4.11.3.4. perf02 [7,624]
الطريقة [perf02]:
- تملأ قاعدة البيانات؛
- ثم تقوم بتعديل أسماء جميع الفئات وأسعار جميع المنتجات.
@Test
public void perf02() {
// تحديث
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
for (Categorie categorie : categories) {
categorie.setNom(categorie.getNom() + "*");
for (Produit produit : categorie.getProduits()) {
produit.setPrix(produit.getPrix() * 1.1);
}
}
// تحديث
daoCategorie.saveEntities(categories);
}
4.11.3.5. perf03[3,911]
الطريقة [perf03]:
- تملأ قاعدة البيانات
- ثم تحذف جميع الفئات واحدة تلو الأخرى. يتم حذف المنتجات أيضًا بسبب الترابط الموجود بين الجدول [CATEGORIES] والجدول [PRODUITS].
قد يثير الدهشة هنا أن هذه العملية تستغرق وقتًا أقل من العملية [3,911 s] مقارنةً بالعملية [perf01] [4,179 s] التي تقوم بمهام أقل.
@Test
public void perf03() {
// حذف الفئات وحذف المنتجات بشكل متسلسل
daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}
إذا نظرنا إلى كود الطريقة [daoCategorie.deleteEntitiesByEntity]، نلاحظ أنه سيتم تنفيذ عملية [PreparedStatement] التي تحتوي على 2500 معلمة (عدد الفئات). وهنا يأتي دور «البيان» [maxPreparedStatementParameters] الذي سيقوم بتقسيم الأمر SQL إلى عدة أوامر [PreparedStatement] تحتوي على عدد من المعلمات يمكن أن يتحمله «البيان» SGBD المحدد المستخدم.
4.11.3.6. perf04[2,426]
الطريقة [perf04]:
- تملأ قاعدة البيانات؛
- ثم تطلب النسخة المطولة لجميع الفئات؛
@Test
public void perf04() {
// تحديد
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
ids.add(categorie.getId());
}
daoCategorie.getLongEntitiesById(ids);
}
4.11.3.7. perf05 [3,507]
الطريقة [perf05]:
- تملأ قاعدة البيانات؛
- ثم تحذف 5000 منتج عبر مفاتيحها الأساسية (لذلك قد يكون لدينا [PreparedStatement] مع 5000 معلمة)؛
- تتحقق من أن جدول المنتجات أصبح فارغًا بعد ذلك؛
@Test
public void perf05() {
// حذف المنتجات
List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
List<Long> ids = new ArrayList<Long>();
for (Categorie categorie : categories) {
for (Produit p : categorie.getProduits()) {
ids.add(p.getId());
}
}
daoProduit.deleteEntitiesById(ids);
// التحقق
List<Produit> produits = daoProduit.getAllShortEntities();
Assert.assertEquals(0, produits.size());
}
4.11.3.8. Résultats
لن نستمر في عرض الاختبارات المختلفة. سنكتفي ببساطة بذكر ما تفعله كل منها ومدتها. هذه المدد لا تكون ذات أهمية إلا عند مقارنتها ببعضها البعض. فقيمها تعتمد في الواقع على بيئة الاختبار المستخدمة (الأجهزة وتكوين البرامج). ولكن إذا تم الحصول عليها في نفس البيئة، فيمكن مقارنتها.
المدة الإجمالية للاختبار: 59,995 ثانية
الدور | ||
يملأ قاعدة البيانات بـ 2500 فئة و5000 منتج | ||
يملأ قاعدة البيانات ثم يعدلها | ||
يملأ قاعدة البيانات ثم يحذف جميع الفئات والمنتجات التابعة لها | ||
يملأ قاعدة البيانات ويطلب النسخة الكاملة لجميع الفئات | ||
يملأ قاعدة البيانات ويحذف 5000 منتج واحدًا تلو الآخر باستخدام مفاتيحها الأساسية | ||
يملأ قاعدة البيانات ويحذف 5000 منتج واحدًا تلو الآخر باستخدام أسمائها | ||
يملأ قاعدة البيانات ويحذف 5000 منتج واحدًا تلو الآخر باستخدام أرقام المرجع الخاصة بها | ||
يملأ قاعدة البيانات ويطلب النسخة المختصرة لجميع المنتجات عبر أسمائها | ||
يملأ قاعدة البيانات ويطلب النسخة الطويلة لجميع المنتجات عبر أسمائها | ||
يملأ قاعدة البيانات ويطلب النسخة المختصرة لجميع المنتجات عبر مفاتيحها الأساسية | ||
يملأ قاعدة البيانات ويطلب النسخة الطويلة لجميع المنتجات عبر مفاتيحها الأساسية | ||
يملأ قاعدة البيانات ثم يحذف جميع الفئات (وبالتالي المنتجات المرتبطة بها) واحدة تلو الأخرى عبر أسمائها | ||
يملأ قاعدة البيانات ثم يحذف جميع الفئات (وبالتالي المنتجات المرتبطة بها) واحدة تلو الأخرى باستخدام أرقامها المرجعية | ||
يملأ قاعدة البيانات ويطلب النسخة المختصرة لجميع الفئات عبر أسمائها | ||
يملأ قاعدة البيانات ويطلب النسخة الطويلة لجميع الفئات عبر أسمائها | ||
يملأ قاعدة البيانات ويطلب النسخة المختصرة لجميع الفئات عبر مفاتيحها الأساسية | ||
يملأ قاعدة البيانات ويطلب النسخة الطويلة لجميع الفئات عبر مفاتيحها الأساسية |
هذه النتائج قد تكون مفاجئة في بعض الأحيان:
- كان الحصول على النسخة الطويلة للمنتجات (perf09) أسرع من الحصول على النسخة المختصرة (perf08)، على الرغم من أن النسخة الطويلة تتطلب إجراء عملية ربط بين جدولين؛
- تتجاوز مدة عملية التعبئة الأولى (perf01) بشكل واضح مدة جميع عمليات التعبئة الأخرى التي ستليها؛
- يستغرق طلب النسخة المختصرة من المنتجات عبر أسمائها (perf08) وقتًا أطول من طلبها عبر المفاتيح الأولية (perf10). ويبدو هذا منطقيًا إلى حد ما. لكن بالنسبة للنسخ الطويلة، فإن العكس هو الصحيح (perf09، perf11)؛
لذلك لن نتطرق إلى هذه النتائج بالتفصيل. ومع ذلك، ستكون مفيدة لنا لمقارنة هذا الحل [Spring JDBC] بالحلول التالية:
- [Spring JDBC]، والحلول الخمس الأخرى SGBD؛
- [Spring JPA] التي ستتبعها؛





























