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]. این ستون را در جدول [CATEGORIES] [4-5] هدف قرار میدهد؛
- هنگامی که یک دستهبندی حذف میشود، تمام محصولات مرتبط با آن نیز حذف میشوند ([6]). این نکته مهم است زیرا در ساخت لایه [DAO] که از پایگاه داده [dbproduitscategories] استفاده میکند، به کار میرود؛
جدول دستهبندی [CATEGORIES] به شرح زیر است:
![]() |
- [ID]: کلید اصلی با افزایش خودکار؛
- [VERSIONING]: شماره نسخهٔ دستهبندی؛
- [NOM]: نام منحصربهفرد دستهبندی؛
4.3. پروژه اکلیپس
![]() |
پروژه [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>
<!-- configuration JDBC of SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- اسپرینگ 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>
- خطوط ۲۸–۳۲: این پروژه بر اساس پروژه [mysql-config-jdbc] ساخته شده است که لایه JDBC را پیکربندی میکند؛
- خطوط ۳۴–۳۷: آرتفکت [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);
}
}
- خط ۱۶: این کلاس یک کلاس پیکربندی Spring است؛
- خط ۱۷: پکیج [spring.jdbc.dao] برای یافتن کامپوننتهای اسپرینگ به غیر از آنهایی که در کلاس [AppConfig] موجود هستند، اسکن خواهد شد. کامپوننتی که لایه [DAO] را پیادهسازی میکند، در آنجا پیدا خواهد شد؛
- خط ۱۸: ما خودمان مدیریت تراکنشها را انجام نمیدهیم بلکه آنها را به Spring JDBC واگذار میکنیم. تنها کاری که باید انجام دهیم این است که متدهایی را که باید در داخل یک تراکنش اجرا شوند با anotation Spring [@Transactional] علامتگذاری کنیم. خط ۱۸ تضمین میکند که این anotation پردازش شده و نادیده گرفته نشود. مدیریت تراکنش توسط یکی از وابستگیهای پروژه، Spring JDBC، که توسط فایل [pom.xml] وارد شده است، انجام میشود؛
- خط ۱۹: بینز (beans) که قبلاً در کلاس [generic.jdbc.config.ConfigJdbc] پروژه [mysql-config-jdbc] تعریف شدهاند، وارد میشوند؛
- خطوط 23–36: منبع داده [tomcat-jdbc] که در مثال [spring-jdbc-02] معرفی شده است؛
- خطوط ۴۰–۴۲: مدیر تراکنش مرتبط با منبع دادهای که قبلاً تعریف شده است. این بین باید با نام [transactionManager] نامگذاری شود، زیرا این نام توسط آنوتیشن [@EnableTransactionManagement] استفاده میشود. هندلر [DataSourceTransactionManager] توسط کتابخانه Spring JDBC (خط ۱۲) فراهم میشود؛
- خطوط ۴۵–۴۸: بین [namedParameterJdbcTemplate] که پیادهسازی لایه [DAO] بر اساس آن خواهد بود. این بین توسط کتابخانه اسپرینگ JDBC (خط ۱۰) فراهم میشود. این بین همچنین به منبع دادهای که قبلاً تعریف شده (خط ۴۷) متصل است؛
- خطوط ۵۱–۵۵: بیان [simpleJdbcInsertProduit] (نام آن میتواند آزادانه انتخاب شود) برای درج یک محصول در جدول [PRODUITS] و بازیابی کلید اصلی تولید شده استفاده خواهد شد. پارامترهای مختلف مورد استفاده به شرح زیر هستند:
- [dataSource]: منبع داده [tomcat-jdbc] از خطوط 24–36؛
- [ConfigJdbc.TAB_PRODUITS]: جدول [PRODUITS]؛
- [ConfigJdbc.TAB_CATEGORIES_ID]: ستون کلید اصلی جدول [PRODUITS]. لطفاً توجه داشته باشید که برای PostgreSQL، نام این ستون باید به حروف کوچک باشد؛
- خطوط ۵۸–۶۲: از bean [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] ارث میبرد و بنابراین یک کلاس بدون بررسی (unchecked) است. این کلاس برای سیگنالدهی به یک فراخوانی با آرگومانهای نادرست به یک متد در لایه [DAO] استفاده خواهد شد. این کلاس [IllegalArgumentException] نامگذاری نشد زیرا این استثنا (exception) از قبل در 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);
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۵: فیلد [id] با ستون [ID]، کلید اصلی جدولها، مرتبط خواهد شد؛
- خط ۶: فیلد [version] با ستون [VERSIONING] در جداول مرتبط خواهد بود؛
- خطوط ۸–۲۶: سازندها و متدهای مختلف برای ایجاد یا مقداردهی اولیه یک شیء [AbstractCoreEntity]؛
- خطوط ۳۵–۴۷: متد [equals] بیان میکند که دو شیء [AbstractCoreEntity] زمانی برابر هستند که فیلد [id] یکسانی داشته باشند. در اینجا باید توجه داشت که اشیاء [AbstractCoreEntity] نمایانگر سطرهای جدول خواهند بود که در آن [id] کلید اصلی است و بنابراین، نمیتوان دو سطر با همان [id] وجود داشته باشد؛
- خطوط ۳۰–۳۳: پیشنهادی برای [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);
}
// گیرنده و تنظیمکننده
...
}
- خط ۶: کلاس [Produit] از کلاس [AbstractCoreEntity] ارث میبرد؛
- ردههای ۸–۱۲: فیلدهای [id, version, nom, idCategorie, prix, description] نگاشتهای ستونهای [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] در جدول [PRODUITS] هستند؛
- خط ۱۲: شیء از نوع [Categorie] با کلید اصلی [idCategorie]. این فیلد ممکن است بسته به شرایط پر شود یا نشود. هنگامی که پر شود، به آن محصول با فرم بلند [LongProduit] گفته میشود؛ در غیر این صورت، به عنوان محصول با فرم کوتاه [ShortProduit] نام برده میشود؛
- خط ۵: یک فیلتر jSON. شایان ذکر است که پروژه [mysql-config-jdbc] شامل یک کتابخانه jSON است. این فیلتر ضروری است زیرا ممکن است فیلد [categorie] پر باشد یا نباشد. در این حالت، نمایش محصول (jSON) متفاوت است. برای رسیدگی به این دو سناریو، فیلتر [jsonFilterProduit] در خط ۵ پیکربندی خواهد شد. یک فیلتر 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] ارث میبرد؛
- ردیف ۱۲: فیلدهای [id, version, nom] نمایانگر تصاویر ستونهای [ID, VERSIONING, NOM] در جدول [CATEGORIES] هستند؛
- خط ۱۳: فیلد [produits] نمایانگر لیست محصولات در دستهبندی است. این فیلد همیشه پر نمیشود. وقتی پر نباشد، به دستهبندی به عنوان نسخه کوتاه [ShortCategorie] اشاره میشود؛ در غیر این صورت، نسخه بلند [LongCategorie] است؛
- خطوط ۳۲–۴۴: متد [addProduit] امکان افزودن یک محصول به دستهبندی (خط ۳۹) و تعیین ویژگیهای دستهبندی آن (idCategorie و 'category') برای محصول افزودهشده را فراهم میکند؛
- خط ۸: یک فیلتر 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);
}
- خط ۷: این یک رابط [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] باشد:
بنابراین ما یک رابط داریم که شامل ۱۹ متد است. بیشتر متدها تکراری هستند. بیایید مثال متد [getShortEntitiesById] را در نظر بگیریم:
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
- خطوط ۱ و ۳: پارامتر، فهرستی از کلیدهای اصلی سوابق است که نسخهٔ کوتاه برای آنها مورد نیاز است. این فهرست در دو شکل مختلف ارائه میشود:
- خط ۱: یک لیست که رابط [Iterable<Long>] را پیادهسازی میکند. نوع [List<Long>] این رابط را پیادهسازی میکند، اما انواع دیگری نیز وجود دارند. اگر ما از [List<Long> ids] استفاده میکردیم، این برای مثالهای ما کافی بود، اما کاربر را مجبور میکرد تا اگر پارامتر او از نوع دقیق مورد انتظار نبود، تبدیلاتی را انجام دهد؛
- خط ۳: متأسفانه، نوع `Long[]` رابط `[Iterable<Long>]` را پیادهسازی نمیکند. در این مورد، ما از نسخه خط ۳ استفاده خواهیم کرد. پارامتر رسمی [Long... ids] (سه نقطه) میتواند مقدار یک آرایه یا یک دنباله از شناسهها را بپذیرد: getShortEntitiesById(id1, id2, ...);
همین رابط، IDao<T>، توسط معماری زیر پیادهسازی خواهد شد:
![]() |
که در آن یک لایه [JPA] (Java Persistence API) بین لایه [DAO] و درایور JDBC از SGBD قرار داده خواهد شد. این امر به ما امکان میدهد تا یک لایه تست مشترک برای هر دو معماری داشته باشیم. در هر دو حالت، لایه [DAO] دو رابط فراهم میکند:
- IDao<Product> برای دسترسی به جدول [PRODUITS]؛
- IDao<Category> برای دسترسی به جدول [CATEGORIES]؛
4.8. پیادهسازی رابط IDao<T>
![]() |
- رابط IDao<Product> توسط کلاس [DaoProduit] پیادهسازی شده است؛
- رابط IDao<Category> توسط کلاس [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);
}
- خط ۱۵: کلاس [AbstractDao] انتزاعی است (کلیدواژه «abstract»). بنابراین نمیتوان آن را نمونهسازی کرد. تنها میتوان از آن ارث بری کرد. این کلاس چندین نقش دارد:
- برای تعریف ماهیت تراکنشی که در آن هر متد اجرا میشود؛
- برای رسیدگی به هرچه بیشتر وظایف مشترک برای دو پیادهسازی از رابطهای `[IDao<Produit>]` و `[IDao<Categorie>]`. این عمدتاً شامل بررسی اعتبار آرگومانها است. نه آرگومانهای `null` و نه لیستهای خالی پذیرفته نخواهند شد؛
- انواع پارامترهای `T... params` و `Iterable<T> params` را به یک نوع واحد استانداردسازی کنید: `List<T> params`؛
- به محض اینکه کار به یکی از این دو رابط اختصاصی شد، آن را به کلاسهای فرزند واگذار کنید؛
به لطف استانداردسازی پارامترها برای متدهای مختلف که توسط کلاس [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();
بیایید به چند متد از کلاس [AbstractDao] نگاهی بیندازیم.
متد [getShortEntitiesById]
این متد برای بازیابی نسخهٔ کوتاهِ اِنتیتههایی طراحی شده است که کلیدهای اصلیشان ارائه شدهاند.
//تزریقها
@Autowired
@Qualifier("maxPreparedStatementParameters")
protected int maxPreparedStatementParameters;
// محلی
protected String simpleClassName = getClass().getSimpleName();
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
...
}
- خطوط ۲–۴: bean [maxPreparedStatementParameters] که در فایل پیکربندی [ConfigJdbc] تعریف شده است، تزریق میشود؛ این فایل لایه JDBC از یک SGBD خاص را پیکربندی میکند:
// حداکثر تعداد پارامترها برای [PreparedStatement]
public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;
@Bean(name = "maxPreparedStatementParameters")
public int maxPreparedStatementParameters() {
return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
- خطوط ۱–۷: دانه [maxPreparedStatementParameters] را تعریف میکنند، که حداکثر تعداد پارامترهایی را که میتوان به نوع [PreparedStatement] ارسال کرد، تعیین میکند. این نیازمندی در مورد SGBD و MySQL که برای نوع [PreparedStatement] ده هزار پارامتر را میپذیرفتند، وجود نداشت. در حین آزمایش با سرورهای SGBD و SQL، یک خطای استثنا (exception) رخ داد که نشان میداد حداکثر تعداد پارامترها برای نوع [PreparedStatement]، ۲۱۰۰ بود. در نتیجه، این عدد به یک پارامتر پیکربندی برای نمونههای مختلف 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) {
...
}
- خط ۷: نام کلاس. این به عنوان پارامتر برای یکی از سازندههای کلاس استثنای [DaoException] استفاده میشود؛
- خط ۱۰: حاشیهنویسی [@Transactional(readOnly = true)] نشان میدهد که متد باید در یک تراکنش فقط-خواندن اجرا شود. با توجه به اینکه این متد تنها عملیات خواندن را انجام میدهد و بنابراین در صورت بروز خطا، چیزی برای بازگشت (roll back) وجود ندارد، ممکن است از مفید بودن چنین تراکنشی پرسش شود. نویسنده کتابخانه [Spring Data] این کار را توصیه میکند و دلیل آن را توضیح میدهد. من از توصیه آنها پیروی کردهام؛
بدنهٔ متد به شرح زیر است:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Iterable<Long> ids) {
// اعتبار آرگومان
List<T> entities = checkNullOrEmptyArgument(true, ids);
if (entities != null) {
return entities;
}
...
}
- خط ۵: اعتبار پارامتر [ids] با روش زیر بررسی میشود:
private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
// عناصر null؟
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;
}
- خط ۱: متد [checkNullOrEmptyArgument] یک متد عمومی است که با نوع <T2> پارامترگذاری شده است. T2 نوع عناصری است که بهعنوان پارامتر دوم به متد ارسال میشوند. این میتواند [Long, String, AbstractCoreEntity] باشد؛
- خط ۱: متد [checkNullOrEmptyArgument] دو پارامتر را میپذیرد:
- [Iterable<T2> elements]: پارامتری که باید آزمایش شود؛
- [checkEmpty]: در صورتی که نیاز باشد بررسی کنیم پارامتر قبلی یک لیست غیرخالی است، روی true تنظیم میشود؛
- خطوط ۴–۶: بررسی میکنیم که پارامتر [elements] برابر null نیست. اگر اینطور نباشد، یک استثنا از نوع [MyIllegalArgumentException] پرتاب میشود؛
- خطوط ۸–۱۵: اگر لیست خالی باشد و قرار بود بررسی کنیم که خالی نیست، یک استثنا از نوع [MyIllegalArgumentException] پرتاب میشود؛
- خط ۱۳: اگر لیست خالی باشد و قرار نبوده بررسی کنیم که خالی نیست، در این صورت یک لیست خالی از عناصر از نوع T بازگردانده میشود. رابط [Iterable<T2>] دارای متدی به نام [iterator()] است که امکان پیمایش بر روی عناصر لیست پیادهسازیکننده این رابط را فراهم میکند. دو متد این ایتراتور مفید هستند:
- [itérateur].hasNext(): اگر لیست همچنان عنصر قابل پردازش داشته باشد مقدار true و در غیر این صورت مقدار false را برمیگرداند؛
- [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;
}
- خط ۷: اگر به این نقطه برسیم، یعنی آرگومان [Iterable<Long> ids] معتبر است؛
- خطوط ۷–۱۴: بعداً خواهیم دید که متد [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</span>**<span style="color: #000000"> پارامتری است که مقدار واقعی آن یک List<Long> خواهد بود. هر عنصر این لیست موضوع یک پارامتر ? در نوع [PreparedStatement] خواهد بود. با این حال، ما بیان کردهایم که این نوع حداکثر تعداد پارامتر را میپذیرد، عددی که توسط فیلد [maxPreparedStatementParameters] کلاس تعیین میشود؛
- خط ۷: لیست اشیاء T که توسط متد [getShortEntitiesById] بازگردانده میشود. این لیست به صورت دستههای [maxPreparedStatementParameters] ساخته خواهد شد؛
- خط ۹: با استفاده از آرگومان [Iterable<Long> ids]، یک نوع [List<Long> listIds] ایجاد میشود. کلاس [Lists] کلاسی از کتابخانه Google Guava است که متدهای استاتیک متعددی برای دستکاری مجموعههای اشیاء فراهم میکند. کتابخانه Google Guava توسط پروژه Maven [mysql-config-jdbc] وارد شده است (pom.xml):
<!-- گوگل گوآوا -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- خط ۱۰: تعداد موجودیتهای T که باید در پایگاه داده جستجو شوند؛
- خطوط ۱۱–۱۳: آنها در گروههایی از عناصر [taille = maxPreparedStatementParameters] جستجو میشوند؛
- خط ۱۲: محاسبهای برای جلوگیری از فراتر رفتن از انتهای لیست [listIds];
- خط ۱۳: اشیاء 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] ندارد. کلاس والد آن این کار را برایش انجام داده است؛
- خط ۱۳: اِنتیتهایی که توسط کلاس فرزند بازگردانده میشوند به فهرست اِنتیتهایی که توسط کلاس والد بازگردانده خواهند شد (خط ۱۶) اضافه میشوند؛
اکنون، بیایید به پیادهسازی متد دیگر کلاس، [getShortEntitiesById]، نگاهی بیندازیم:
@Override
@Transactional(readOnly = true)
public List<T> getShortEntitiesById(Long... ids) {
// اعتبار استدلال
List<T> entities = checkNullOrEmptyArgument(true, ids);
// نتیجه
return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
- خط ۳: نوع آرگومان تغییر کرده است: Long... ids;
- خط ۵: اعتبار این آرگومان بررسی میشود؛
- خط ۷: متد [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> {
....
}
- خط ۲۸: کلاس [DaoCategorie] یک کامپوننت Spring است و به همین دلیل، میتواند به کامپوننتهای دیگر Spring تزریق شود؛
- خط ۲۹: کلاس [DaoCategorie] از کلاس انتزاعی [AbstractDao<Categorie>] ارث میبرد و آن را به پیادهسازی رابط [IDao<Categorie>] تبدیل میکند؛
- خطوط ۳۴–۳۷: تزریق بیَنهایی که در کلاس [AppConfig] که در بخش ۴.۴ توصیف شده، تعریف شدهاند؛
- خطوط ۳۸–۳۹: تزریق مرجعی به کلاس [DaoProduit]، که رابط [IDao<Produit>] را پیادهسازی میکند و دسترسی به دادهها در جدول [PRODUITS] را مدیریت میکند؛
- خطوط ۴۱–۸۹: پیادهسازی رابط [IDao<Categorie>]؛
- خطوط ۹۵–۱۰۱: دو کلاس داخلی که رابط [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);
}
}
- خط ۱: کلاس [ShortCategorieMapper] رابط [RowMapper<Categorie>] را پیادهسازی میکند و به همین دلیل، باید متد [mapRow] را در خطوط ۴ پیادهسازی کند–۵، که هدف آن کپسولهسازی یک خط از خروجی [ResultSet rs] تولید شده توسط فرمان [SELECT] در داخل یک نوع [Categorie] است؛
- خط ۵: این احاطه انجام میشود. باید توجه داشت که نامی که توسط متدهای [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;
}
}
- خط ۴: متد [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;
}
- خط ۱: [List<Categorie> categories] فهرست دستهبندیهایی است که باید فیلتر (یا گروهبندی) شوند؛
- خط ۶: فهرست دستهبندیهایی که باید به فراخواننده بازگردانده شوند؛
- خطوط ۸–۲۱: هر دسته در لیست فیلتر شده پردازش میشود؛
- خطوط ۱۰–۱۶: بررسی میکنیم که آیا دسته فعلی [categorie] از قبل در لیست دستههای [cats] که باید ساخته شود، وجود دارد یا خیر (توجه داشته باشید که دو دسته زمانی برابر در نظر گرفته میشوند که کلید اصلی یکسانی داشته باشند؛ به بند ۴.۶ مراجعه کنید)؛
- خطوط ۱۱–۱۴: اگر این وضعیت از قبل برقرار باشد، آنگاه محصولی که در [categorie] جای گرفته است به لیست محصولات برای [cat] اضافه میشود؛
- خطوط ۱۸–۲۰: اگر دسته فعلی [categorie] از قبل در لیست دستههای [cats] که باید ساخته شود، موجود نباشد، آنگاه به همراه لیست محصولاتش که شامل یک آیتم واحد است، به آن لیست اضافه میشود؛
بیایید موردی را در نظر بگیریم که پرسوجوی Select SQL دستههای بدون هیچ محصول مرتبطی را بازمیگرداند. کدام موجودیت کلاس [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 هستند. این مورد در خطوط ۷–۹ رسیدگی میشود:
- خط ۷: کلید اصلی محصول به صورت یک عدد صحیح بلند بازیابی میشود؛
- خط ۹: بررسی میکنیم که آیا مقدار خوانده شده SQL NULL (rs.wasNull) بوده است. اگر اینطور نباشد، محصول در خط ۶ به لیست اضافه میشود؛ در غیر این صورت، هیچ چیزی اضافه نمیشود و لیست محصولات خالی باقی میماند.
توجه داشته باشید که در همه موارد، یک دستهبندی با فیلد [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);
}
}
- در خط ۴، امضای متد [query] مورد استفاده به شرح زیر است:

پارامتر اول یک دستور SQL [Select] پیکربندیشده است. پارامتر دوم یک فرهنگ لغت است که هر پارامتر را با یک مقدار مرتبط میسازد. مورد سوم، نمونهٔ کلاسی است که یک سطر را از نتیجهٔ [ResultSet] از [Select] در یک شیء از نوع T جاسازی میکند؛
- خط ۴: فرمان پیکربندیشده 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 قرار دارند، بازیابی میکند.
- خط ۵: پارامتر دوم متد [query] در این مورد، یک دیکشنری است که کلید «ids» (پارامتر اول) را با لیست [ids] مرتبط میکند، که در خط ۱ به عنوان پارامتر به متد [getShortEntitiesById] ارسال شده بود. کلاس [Collections] متعلق به کتابخانه [Google Guava] است که قبلاً در مورد آن بحث کردهایم. [Collections.singleMap] یک دیکشنری حاوی یک عنصر واحد را بازمیگرداند؛
- خط ۵: کلاسی که مسئول پنهانسازی یک سطر از [ResultSet]—که نتیجه [Select] است—در یک شیء از نوع [Categorie] است، کلاس [ShortCategorieMapper] است که قبلاً بررسی کردهایم؛
اینجا معمولاً جایی است که بیان [maxPreparedStatementParameters] وارد عمل میشود. در واقع، پارامتر [:ids] از دستور SQL—که نمایانگر فهرستی از کلیدهای اصلی است—میتواند از ۱ تا چند هزار پارامتر داشته باشد. برای این تعداد محدودیتی وجود دارد که به هر SGBD بستگی دارد. برای MySQL، ما توانستیم ۱۰٬۰۰۰ پارامتر را بدون خطا ارسال کنیم و فراتر از آن را آزمایش نکردهایم. برای سرور SQL، محدودیت رسمی ۲۱۰۰ است. برای فایربرد، عدد ۱۰۰۰ بیش از حد بود؛ ما این عدد را به ۱۰۰ کاهش دادیم. به طور کلی، ما حداکثر محدودیت این تعداد را در نمونههای مختلف 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);
}
}
خط ۴، پرسوجوی 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);
}
}
خط ۴، دستور 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);
}
}
خط ۴، دستور 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);
}
}
- خط ۴: متد [namedParameterJdbcTemplate.update] مورد استفاده دارای امضای زیر است:
![]()
پارامتر اول یک درخواست بهروزرسانی SQL (INSERT, UPDATE, DELETE) است. پارامتر دوم، دیکشنریای است که مقادیر را به پارامترهای مختلف دستور SQL نگاشت میکند. این متد تعداد ردیفهای بهروزرسانیشده توسط دستور SQL را برمیگرداند.
- خط ۴: دستور 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);
}
}
خط ۴، دستور 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);
}
}
خط ۴، دستور 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);
}
}
- خطوط ۵–۲۳: درج یا بهروزرسانی دستهبندیها؛
- خطوط 26–43: درج یا بهروزرسانی محصولات؛
- خطوط ۳۵–۳۹: این کد هر محصول را به دستهبندی آن متصل میکند. در مرحله قبلی درج دستهبندیها، یک کلید اصلی به آنها اختصاص داده شد که باید در فیلد [idCategorie] (خط ۳۷) وارد شود. علاوه بر این، خطوط ۳۷–۳۸ امکان اصلاح مواردی را فراهم میکنند که در آن فراخواننده هر محصول را به درستی به دستهبندی خود پیوند نداده است. برای اطمینان از صحت این رابطه، باید از متد `[Categorie].add(Product p)` استفاده شود؛ با این حال، هیچ چیزی مانع از آن نمیشود که کاربر بدون استفاده از این متد، یک محصول را مستقیماً به لیست محصولات یک دسته اضافه کند، که این کار با ریسک پر شدن نادرست فیلدهای `[idCategorie, categorie]` برای محصول p همراه است؛
- خط ۴۳: وظیفه پایدارسازی / بهروزرسانی محصولات به نمونه رابط [IDao<Produit>] واگذار شده است. توجه داشته باشید که این نمونه به کلاس [DaoCategorie] تزریق شده است:
@Autowired
private IDao<Produit> daoProduit;
4.9.10.2. افزودن دستهبندیها
دستهبندیها توسط متد خصوصی زیر [insertCategories] در جدول [CATEGORIES] درج میشوند:
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;
}
- خط ۶: از bean با نام [simpleJdbcInsertCategorie] استفاده میشود که توسط خطوط زیر به کلاس تزریق میشود:
@Autowired
private SimpleJdbcInsert simpleJdbcInsertCategorie;
این بی́n در کلاس [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);
}
- خط ۵، کلاس [SimpleJdbcInsert] یک کلاس از کتابخانه Spring JDBC (خط ۱) است:
- پارامتر سازنده [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;
}
- خط ۶: از متد [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])، و مقادیر در دیکشنری، مقادیری هستند که باید در این ستونها درج شوند.
- خط ۸ [insertCategories]: کلید اصلی بازیابیشده در یک دیکشنری ذخیره میشود. ما صبر میکنیم تا مطمئن شویم که همهٔ اشیاء وارد شدهاند، سپس کلیدهای اصلی را به آنها اختصاص میدهیم. این به آن دلیل است که در صورت رخ دادن خطا (exception)، همهٔ عملیات درج (insertions) برگشت داده (rolled back) میشوند و ما میخواهیم اشیاء [categories] در خط 1 نیز بدون تغییر باقی بمانند؛
- خطوط ۱۴–۱۷: اکنون که مطمئن هستیم همه چیز به درستی پیش رفته است، کلیدهای اصلی تولید شده را به دستههای (categories) اختصاص میدهیم؛
- خط ۱۹: ما لیست دستهبندیها را به همراه کلیدهای اصلی آنها بازمیگردانیم؛
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 را اصلاح میکند و این تغییر را در پایگاه داده commit میکند: شماره نسخه سپس به V1+1 تغییر مییابد. به نوبه خود E را تغییر داده و این تغییر را در پایگاه داده commit میکند: یک استثنا پرتاب خواهد کرد زیرا نسخه آن (V1) با نسخه موجود در پایگاه داده (V1+1) متفاوت است.
- خطوط ۲–۲۹: بلوک `try` دارای دو بلوک `catch` است:
- اولی، در خط ۲۵، برای این است که اجازه دهد هر استثنایی از نوع [DaoException] که توسط کد در خط ۱۳ پرتاب میشود، عبور کند؛
- دومی، در خط ۲۷، برای رسیدگی به انواع دیگر استثناها قرار دارد؛
- خط ۳: ما تمام دستهبندیهایی را که باید بهروزرسانی شوند، اسکن میکنیم؛
- خط ۴: دستهٔ جاری با استفاده از متد [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, :name) که مقادیر آنها در فیلدهایی با همین نام در شیء تغییریافته [categorie] قرار دارند. ما با ارسال [new BeanPropertySqlParameterSource(categorie)] به عنوان پارامتر دوم از این ویژگی استفاده میکنیم، که مشخص میکند «مقادیر پارامترها در فیلدهایی با همان نامها در این جاوا بین قرار دارند»؛
نتیجهای که این عملیات در اجرای عادی بازمیگرداند، تعداد خطوط اصلاحشده است، یعنی 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);
}
}
- خط ۹: بررسی میکنیم که آیا اصلاح موفقیتآمیز بوده است؛
- خط ۱۰: اصلاح با شکست مواجه شده است. از آنجایی که عبارت [WHERE] شامل ستونهای [ID] و [VERSIONING] است، ما به دنبال ستونی میگردیم که باعث شکست [WHERE] شده است؛
- خطوط ۱۲–۱۸: بررسی میکنیم که کلید دستهبندی [id] در پایگاه داده وجود دارد. اگر اینطور نباشد، یک [RuntimeException] را با پیام خطای مناسب اجرا میکنیم؛
- خطوط ۱۹–۲۲: رسیدگی به موردی که نسخه نادرست بوده است؛
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);
}
}
- خط ۴: دستور Select SQL برای [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)";
- خط ۴: کلاس [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);
}
}
- خط ۴: دستور 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";
- خط ۴: کلاس [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);
}
}
در خط ۱۸، محصولات مورد نظر برای درج با استفاده از متد خصوصی زیر، [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 است.
- خط ۴: از bean [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);
}
- خطوط ۳–۶: بین [simpleJdbcInsertProduit]
- به منبع داده در پایگاه داده [dbproduitscategories] (خط ۳) و به جدول [ConfigJdbc.TAB_PRODUITS] در آن منبع (خط ۴) متصل است؛
- کلید اصلی این جدول در ستون [ConfigJdbc.TAB_PRODUITS_ID] (خط ۵) ایجاد میشود؛
- مقادیر فقط به ستونهای موجود در [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] (ردیف 6) اختصاص داده میشوند؛
روش [updateProduits] که محصولات را بهروزرسانی میکند (خط ۲۰ در [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] هستند، که امکان استفاده از دستور در خطوط ۶–۷ برای بهروزرسانی محصول فعلی را فراهم میکند.
4.11. لایهٔ تست
![]() |
![]() |
لایهٔ تست شامل سه کلاس تست است:
- [JUnitTestCheckArguments]: تستهای این کلاس متدهای مختلف لایه [DAO] را با آرگومانهای نامعتبر فراخوانی کرده و بررسی میکنند که پاسخ صحیحی بدهند؛
- [JUnitTestDao]: تستهای این کلاس متدهای مختلف لایه [DAO] را فراخوانی کرده و بررسی میکنند که مطابق انتظار عمل میکنند؛
- [JUnitTestPushTheLimits] برای آزمایش لایه [DAO] در نظر گرفته نشده، بلکه برای سنجش عملکرد آن است؛
این لایهٔ آزمون نقش مهمی در این سند ایفا میکند. در واقع، این لایه برای همهٔ پیادهسازیهای رابط [IDao<T>] مشترک است. برای هر SGBD شش مورد وجود دارد (۱ پیادهسازی JDBC، ۳ پیادهسازی JPA، ۱ پیادهسازی Spring MVC، ۱ پیادهسازی امن Spring MVC)، که در مجموع ۳۶ پیادهسازی برای شش پیادهسازی SGBD آزمایششده میشود. مجموعه تست به ما امکان میدهد تا تأیید کنیم که همه پیادهسازیها به یک شکل رفتار میکنند.
4.11.1. آزمون [JUnitTestCheckArguments]
کلاس تست [JUnitTestCheckArguments] دارای ۴۸ متد است که بررسی میکند متدهای لایه [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];
...
}
- خط ۱۹: تست JUnit به عنوان بخشی از یکپارچهسازی با فریمورک Spring اجرا خواهد شد؛
- خط ۱۸: قبل از تستها، بی́nهای تعریفشده در کلاس [AppConfig] پروژه نمونهسازی خواهند شد؛
- خطوط ۲۳–۲۶: تزریق یک نمونه از هر یک از دو رابط در لایه [DAO]؛
- خطوط ۲۹–۴۴: پارامترهای فراخوانی نادرست برای متدها در لایه [DAO]؛
- خط ۲۹: یک نشانگر از نوع [Iterable<String>] به null به عنوان یک لیست از نامها؛
- خط ۳۰: یک لیست خالی از نوع [Iterable<String>] به عنوان یک لیست از نامها؛
- خط ۲۹: یک نشانگر null از نوع String[] به عنوان آرایه نامها؛
- خط ۳۰: یک آرایه خالی از نوع String[] به عنوان فهرستی از نامها؛
- ...
با فیلد [names1]، برای مثال، تست زیر را انجام میدهیم:
@Test(expected = MyIllegalArgumentException.class)
public void getShortProduitsByName1() {
daoProduit.getShortEntitiesByName(names1);
}
- خط ۱: مشخص میکنیم که تست [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);
}
بنابراین ما ۴۸ آزمایش را برای پوشش تمام موارد ممکن انجام میدهیم. ما پیکربندی اجرای به نام [spring-jdbc-generic-04-JUnitTestCheckArguments] [1] را اجرا میکنیم. نتیجه حاصل برای [2] به شرح زیر است:
![]() |
4.11.2. آزمایش [JUnitTestDao]
تست [JUnitTestDao] متدهای لایه [DAO] را با آرگومانهای معتبر فراخوانی میکند و بررسی میکند که این متدها طبق انتظار عمل میکنند. در مجموع ۷۴ تست وجود دارد که درج، انتخاب، بهروزرسانی و حذف موجودیتها، دستهبندیها یا محصولات را بررسی میکنند. در مجموع، بیش از ۱۰۰۰ خط کد وجود دارد. ما تنها به چند مورد از این متدها نگاهی خواهیم انداخت.
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 {
// زمینهٔ بهار
@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);
}
}
...
}
- خطوط ۲۷–۲۸: مانند تست [JUnitTestCheckArguments]، این یک تست یکپارچه با Spring است که توسط کلاس [AppConfig] در پروژه پیکربندی شده است؛
- خطوط ۳۲–۳۳: تزریق کانکست Spring که دسترسی به تمام Beans آن را فراهم میکند؛
- خطوط ۳۵–۳۶: تزریق نمونهٔ رابط [IDao<Produit>] که توسط کلاس مورد آزمایش قرار میگیرد؛
- خطوط ۳۷–۳۸: تزریق نمونهٔ رابط [IDao<Categorie>] که توسط کلاس تست میشود؛
- خطوط ۴۱–۴۲: هنگامی که یک تست به دادههای پایگاه داده نیاز دارد، یک پایگاه داده از دستههای [NB_CATEGORIES] ایجاد میشود که هر کدام حاوی محصولات [NB_PRODUITS] هستند. این امر منجر به ایجاد دستهبندیهای [NB_CATEGORIES] در جدول [CATEGORIES] و محصولات [NB_CATEGORIES] * [NB_PRODUITS] در جدول [PRODUITS] خواهد شد؛
- خطوط ۴۶–۴۷: دو دیکشنری که محصولات و دستهبندیها در آنها ذخیره خواهند شد؛
- خطوط ۴۹–۶۲: متد [clean] قبل از هر آزمون اجرا میشود (خط ۴۹). در خط ۵۴، جدول [CATEGORIES] پاک میشود. در اینجا باید توجه داشت که جدول [PRODUITS] دارای کلید اصلی [CATEGORIE_ID] در ستون ID از جدول [CATEGORIES] است و این به شرح زیر تعریف شده است؛
![]() |
- (ادامه)
- در [1-3]، کلید خارجی [CATEGORIE_ID] از جدول [PRODUITS]. این به ستون [ID] در جدول [CATEGORIES] [4-5] اشاره میکند؛
- هنگامی که یک دستهبندی حذف میشود، تمام محصولات مرتبط با آن نیز حذف میشوند ([6]). این نکته حائز اهمیت است زیرا در ساخت لایه [DAO] که از پایگاه داده [dbproduitscategories] استفاده میکند، به کار میرود؛
بنابراین، هنگامی که محتویات جدول [CATEGORIES] حذف میشوند، محتویات جدول [PRODUITS] نیز حذف خواهند شد.
- خطوط ۵۶–۵۸: دیکشنری دستهبندی پاک میشود؛
- خطوط ۵۹–۶۱: همین کار برای فرهنگ لغت محصول نیز انجام میشود؛
شایان ذکر است که قبل از هر آزمون، پایگاه داده شامل جداول خالی و حافظه شامل فرهنگهای لغت خالی است.
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]، خط ۴، که پایگاه داده را با دادههای آزمایشی پر میکند؛
- [registerCategories]، خط ۴، که فرهنگها را با دادههای بازگشتی از روش [fill] پر میکند. این دو فرهنگ نمایانگر موجودیتهای پایدار هستند؛
- [showDataBase]، خط ۶، که دو جدول [CATEGORIES] و [PRODUITS] را میخواند و آنچه را خوانده بازمیگرداند؛
- [checkShortCategorie]، خط ۱۳، کاتگوری خوانده شده توسط [showDataBase] را بررسی میکند. این خط بررسی میکند که نسخه کوتاه این کاتگوری با آنچه در دیکشنری کاتگوری ثبت شده است مطابقت دارد؛
- [checkShortProduit] در خط 16 همین کار را برای محصولات انجام میدهد؛
- وقتی یک عنصر در یک فرهنگ لغت یافت میشود، از آن فرهنگ لغت حذف میشود. خطوط ۱۹–۲۰ بررسی میکنند که هر دو فرهنگ لغت خالی هستند. اگر هر دو این گزارهها درست باشند، این بدان معناست که:
- تمام مقادیری که توسط [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;
}
- خطوط ۳–۱۲: یک لیست از دستهبندیهای [nbCategories] ساخته میشود که هر کدام حاوی محصولات [nbProduits] هستند؛
- خط ۱۵: این فهرست دستهبندیها ذخیره میشود. ما دیدهایم که متد [daoCategorie.saveEntities] همچنین محصولات مرتبط با دستهبندیها را در صورت لزوم ذخیره میکند؛
- خط ۱۷: لیست ذخیرهشدهٔ دستهبندیها بازگردانده میشود. موجودیتهای ذخیرهشده (دستهبندیها و محصولات) اکنون در فیلد [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 };
}
- خطوط ۴ و ۸: نسخههای کوتاه دستهبندیها و محصولات بازیابی میشوند؛
- خط ۱۱: یک آرایه حاوی دو لیست موجودیتهای بازیابیشده بازگردانده میشود؛
- خطوط ۵ و ۹: فهرست موجودیتها با استفاده از متد خصوصی زیر [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 (خط ۱۰) نمایش داده میشوند. این مپر پارامتر دوم متد [affiche] است (خط ۲). کانکست 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)]
که باعث میشود آنها به صورت beans در هر درخواست به کانکست Spring instance شوند. این جدید است. تمام beansهای Spring که تا کنون دیدهایم singleton بودهاند: تنها یک نمونه ایجاد میشد و هرگاه از کانکست Spring به آن ارجاع داده میشد، همین نمونه بازگردانده میشد. چرا این تغییر؟ در واقع، چهار بیون [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] مپر واحد jSON (که در واقع یک سینگلتون است) را که در خطوط ۲–۵ تعریف شده، پیکربندی میکنند. این مپر باید هر بار که یکی از چهار بیون قبلی فراخوانی میشود، مجدداً پیکربندی شود، نه فقط یک بار هنگام راهاندازی کانکست. اگر تصمیم میگرفتیم چهار مپر jSON متفاوت داشته باشیم، یکی برای هر یک از چهار بین، آنگاه میتوانستند از نوع تکنسخه (singleton) باشند. این کاملاً ممکن بود. در آن صورت، خطوط ۱۰، ۱۹، ۲۹ و ۳۸ را به شکل زیر مینوشتیم:
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());
}
- خطوط ۶–۸: نسخههای کوتاه محصولات و دستهبندیهایی را که از پایگاه داده خوانده شدهاند، بازیابی میکنیم؛
- خطوط ۱۰–۱۱: بررسیهای اولیه؛
- خطوط ۱۲–۱۴: هر دسته که توسط متد [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 آزمایش کرد
}
- خط ۱: [Categorie actual] کاتگوری خواندهشده از پایگاه داده است و باید با کاتگوری در دیکشنری [mapCategories] یکسان باشد؛
- خط ۲: کلید اصلیِ دستهبندیِ خواندهشده بازیابی میشود؛
- خط ۳: دستهبندی ذخیره شده با این کلید اصلی در فرهنگ لغت دستهبندی بازیابی میشود؛
- خط ۴: کلید از فرهنگ لغت حذف میشود تا اطمینان حاصل شود که هیچ دسته دیگری که خوانده میشود از همان کلید استفاده نکند؛
- خط ۵: بررسی میکنیم که دو دستهبندی نام یکسانی داشته باشند؛
نسخهٔ کوتاه محصولات بازگرداندهشده توسط متد [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 آزمایش کرد
}
- خط ۱: [Produit actual] محصول کوتاهی است که از پایگاه داده خوانده شده است؛
- خطوط ۲–۳: محصول با کلید اولیه یکسان از فرهنگ لغت محصولات ذخیرهشده بازیابی میشود؛
- خط ۴: ورودی یافتشده در فرهنگ لغات حذف میشود؛
- خطوط ۵–۸: بررسی میکنیم که دو محصول مقادیر یکسانی در فیلدها داشته باشند؛
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());
}
- خط ۴: پایگاه داده را پر کرده و فهرست دستهبندیها و محصولاتی را که ذخیره شدهاند، بازیابی میکنیم؛
- خط ۷: متد [daoCategorie.getLongEntitiesByName(Iterable<String> names)] را در لایه [DAO] آزمایش میکنیم. ما یک لیست از دو محصول را که با نامهای کاملشان شناسایی شدهاند درخواست میکنیم؛
- خط ۸: ما بررسی میکنیم که فهرست بازگرداندهشده توسط [daoCategorie.getLongEntitiesByName(Iterable<String> names)] واقعاً شامل دو عنصر است؛
- خط ۹: دو عنصری که در خط ۴ ذخیره شدهاند به دیکشنری دستهبندی اضافه میشوند؛
- خطوط ۱۰–۱۲: بررسی میکنیم که دو عنصر خواندهشده، واقعاً همانهایی هستند که ذخیره شدهاند؛
- خط ۱۳: بررسی میکنیم که دیکشنری دستهبندی خالی است، که به این معنی است که همهی دستهبندیهای خواندهشده در دیکشنری یافت شدهاند و دیکشنری حاوی هیچ مقداری نیست که خوانده نشده باشد؛
خط ۱۱: متد [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());
}
- خط ۶ بررسی میکند که فیلد [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] را ۱۰ درصد افزایش میدهد و دو مورد را بررسی میکند:
- که قیمت پایه واقعاً تغییر کرده است؛
- که نسخه محصول بهروزرسانیشده یک واحد افزایش یافته باشد؛
کد موارد زیر را انجام میدهد:
- خط ۴: پایگاه داده را پر میکند؛
- خط ۷: دسته با نام 'categorie[1]' را از پایگاه داده بازیابی میکند؛
- خطوط ۸–۱۳: قیمت تمام محصولات را ۱۰٪ افزایش میدهد (خط ۱۱). علاوه بر این، یک دیکشنری ایجاد میکند که هر محصول را به نسخه آن پیوند میدهد (خطوط ۹ و ۱۲);
- خط ۱۴: متد [daoProduit.saveEntities] فراخوانی میشود. این متد محصولات را بهروزرسانی خواهد کرد؛
- خط ۱۶: محصولات در دستهبندی با نام «categorie[1]» از پایگاه داده بازیابی میشوند؛
- خطوط ۲۰–۲۴: برای تمام محصولات در این دسته، سیستم بررسی میکند که قیمت واقعاً تغییر کرده است (خط ۲۲) و نسخه ۱ واحد افزایش یافته است (خط ۲۳)؛
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());
}
- خط ۶: دو محصول حذف میشوند؛
- خطوط ۸–۹: بررسی میکند که آنها دیگر در پایگاه داده وجود ندارند؛
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());
}
- خط ۴: پایگاه داده را پر کرده و فهرست دستهبندیهای پایدار را بازیابی میکنیم؛
- خط ۷: نسخه کامل دو محصول را که با نامهایشان شناسایی شدهاند، از پایگاه داده بازیابی کنید؛
- خط ۹: محصولات [produit[0,3], produit[1,4]] موجود در فهرست دستهبندیهای خط ۴ به فرهنگ لغت محصولات اضافه میشوند؛
- خط ۱۰: همین دو محصول با استفاده از کلیدهای اصلیشان در پایگاه داده جستجو میشوند؛
- خطوط ۱۱–۱۴: بررسیای انجام میشود تا اطمینان حاصل شود که دادههای خواندهشده با دادههای ذخیرهشده در فرهنگ لغت یکسان هستند؛
متد خصوصی [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
فعلاً در اینجا متوقف میشویم. در حال حاضر ۷۴ تست وجود دارد و میتوانیم تستهای بیشتری اضافه کنیم، زیرا احتمالاً برخی از موارد آزمون را نادیده گرفتهام. اگرچه این تستها جامع نیستند، اما به ما امکان دادهاند تا خطاهای متعددی را شناسایی کنیم، که عموماً موارد حاشیهای (edge cases) هستند که هنگام نوشتن اولیه لایه [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] را میبینیم. ما قبلاً با همه این متدها آشنا شدهایم. تست با یک پایگاه داده شامل ۲۵۰۰ دسته، که هر کدام شامل ۲ محصول هستند (خطوط ۳۲–۳۳)، کار میکند. بنابراین جدول [CATEGORIES] دارای ۲۵۰۰ سطر و جدول [PRODUITS] دارای ۵۰۰۰ سطر خواهد بود. میتوانستیم سطرهای بیشتری را شامل کنیم، اما این تست در حال حاضر تقریباً یک دقیقه برای اجرا طول میکشد. بنابراین ما ارزشهایی را انتخاب کردهایم که برای کاربری که منتظر پایان تست است، معقول باشند.
در مجموع ۱۸ تست وجود دارد. آنها با استفاده از پیکربندی اجرای [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] با ۲۵۰۰ پارامتر (تعداد دستهبندیها) قرار است اجرا شود. در اینجا است که بیون [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]:
- پایگاه داده را پر میکند؛
- سپس ۵۰۰۰ محصول را با استفاده از کلیدهای اصلیشان حذف میکند (بنابراین ما بالقوه یک [PreparedStatement] با ۵۰۰۰ پارامتر داریم)؛
- بررسی میکند که جدول محصولات پس از آن خالی باشد؛
@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
ما به ارائه جزئیات تستهای مختلف نمیپردازیم. ما صرفاً بیان میکنیم که هر یک چه کاری انجام میدهند و چقدر طول میکشند. این مدتزمانها تنها زمانی معنادار هستند که با یکدیگر مقایسه شوند. در واقع، مقادیر آنها به محیط تست مورد استفاده (پیکربندی سختافزاری و نرمافزاری) بستگی دارد. با این حال، هنگامی که در یک محیط یکسان به دست آیند، میتوان آنها را با هم مقایسه کرد.
مدت زمان کل آزمایش: ۵۹٫۹۹۵ ثانیه
نقش | ||
پایگاه داده را با ۲۵۰۰ دستهبندی و ۵۰۰۰ محصول پر میکند | ||
پایگاه داده را پر و سپس اصلاح میکند | ||
پایگاه داده را پر میکند، سپس همهٔ دستهبندیها و محصولاتشان را حذف میکند | ||
پایگاه داده را پر میکند و نسخه طولانی همه دستهبندیها را درخواست میکند | ||
پایگاه داده را پر میکند و ۵۰۰۰ محصول را یکییکی با استفاده از کلیدهای اصلیشان حذف میکند | ||
پایگاه داده را پر میکند و ۵۰۰۰ محصول را یکییکی با استفاده از نامهایشان حذف میکند | ||
پایگاه داده را پر میکند و ۵۰۰۰ محصول را یکییکی با کد محصولشان حذف میکند | ||
پایگاه داده را پر میکند و نسخه کوتاه همه محصولات را بر اساس نامهایشان بازیابی میکند | ||
پایگاه داده را پر میکند و نسخه طولانی تمام محصولات را بر اساس نام بازیابی میکند | ||
پایگاه داده را پر میکند و نسخهٔ کوتاه همهٔ محصولات را با استفاده از کلیدهای اصلیشان بازیابی میکند | ||
پایگاه داده را پر میکند و نسخه طولانی تمام محصولات را با استفاده از کلیدهای اصلی آنها بازیابی میکند | ||
پایگاه داده را پر میکند و سپس همه دستهبندیها (و در نتیجه محصولات مرتبط) را یکییکی با استفاده از نامهایشان حذف میکند | ||
پایگاه داده را پر میکند و سپس همه دستهبندیها (و در نتیجه محصولات مرتبط) را یکییکی با استفاده از کد محصولشان حذف میکند | ||
پایگاه داده را پر میکند و نسخه کوتاه همه دستهبندیها را از طریق نامهایشان بازیابی میکند | ||
پایگاه داده را پر میکند و نسخه کامل تمام دستهبندیها را بر اساس نام بازیابی میکند | ||
پایگاه داده را پر میکند و نسخهٔ کوتاه همهٔ دستهبندیها را از طریق کلیدهای اصلیشان بازیابی میکند | ||
پایگاه داده را پر میکند و نسخه طولانی همه دستهبندیها را از طریق کلیدهای اصلی آنها بازیابی میکند |
این نتایج گاهی اوقات شگفتآور هستند:
- بازیابی نسخه طولانی محصولات (perf09) سریعتر از نسخه کوتاه آنها (perf08) بود، هرچند که نسخه طولانی شامل یک پیوستشدن بین دو جدول است؛
- مدت زمان اولین پر کردن (perf01) به طور قابل توجهی از تمام پر کردنهای بعدی بیشتر است؛
- بهدست آوردن نسخه کوتاه محصولات بر اساس نام (perf08) بیشتر از بهدست آوردن آنها از طریق کلیدهای اصلی (perf10) طول میکشد. این موضوع کاملاً منطقی به نظر میرسد. با این حال، برای نسخههای طولانی، برعکس این موضوع صادق است (perf09, perf11);
بنابراین، ما بر روی این نتایج تمرکز نخواهیم کرد. با این حال، آنها برای مقایسه این راهحل، [Spring JDBC]، با راهحلهای زیر مفید خواهند بود:
- [Spring JDBC] و پنج مورد دیگر SGBD؛
- [Spring JPA]، که در ادامه خواهد آمد؛





























