3. مقدمهای بر API JDBC
3.1. راهاندازی محیط کار
ما با پایگاه داده MySQL5 کار خواهیم کرد.
شما باید داشته باشید:
- JDK (مجموعه توسعه جاوای) را نصب کرده باشید (بخش 23.1);
- نصب مدیر وابستگی Maven (بخش 23.2)؛
- نصب Spring Tool Suite (IDE) (STS) (بخش 23.3);
- نصب SGBD MySQL5 (بخش 23.4) و کلاینت آن EMS MyManager (بخش 23.5)؛
- کد را از سند [http://tahe.developpez.com/java/spring-database] دانلود کرد؛
از این پس فرض میکنیم که مدیر MySQL5 کاربر root با رمز عبور «root» است. سرویسهای SGBD، MySQL5 و کلاینت آن [MyManager] را راهاندازی کنید. با استفاده از [MyManager]، پایگاه داده [dbproduits] [1-34] را ایجاد میکنیم:
![]() |
- در [3]، پایگاه داده باید [dbproduits] نامگذاری شود؛
![]() |
- به [8-9]، با رمز عبور ریشه (که در تصویر صفحه نمایش بالا نشان داده نشده است) وارد شوید؛
![]() |
- برای [14a]، رمز عبور دوباره root است (که در اسکرینشات نشان داده نشده است)؛
- در [15]، پایگاه داده [dbproduits] ایجاد شده است؛
![]() |
- در [20]، به پایگاه داده انتخابشده توجه کنید. این باید پایگاه داده [dbproduits] باشد؛
![]() |
- به [22]؛ پوشه <examples>/spring-database-config/mysql/databases است، که در آن <examples> پوشهای است که حاوی مثالهای دانلود شده میباشد؛
- در [23]، اسکریپت SQL [dbproduits.sql] را انتخاب کنید. این اسکریپت جدول [PRODUITS] را در پایگاه داده [dbproduits] ایجاد خواهد کرد؛
![]() |
![]() |
- در [30]، جدول [produits] ایجاد شد؛
![]() |
- در [33]، ستونهای جدول [produits]؛
![]() |
- به [34]؛ این جدول در ابتدا خالی است؛
اکنون با استفاده از STS، پروژههای زیر را وارد کنید (مطابق روندی که برای پروژهها در پوشه <examples>/spring-core استفاده شده است):
![]() |
- به داخل [2]؛ پروژه [mysql-config-jdbc] را در پوشه [<exemples>/spring-database-config/mysql/eclipse/mysql-config-jdbc] [1] پیدا خواهید کرد؛
این پروژه لایه JDBC از معماری نشان داده شده در زیر را پیکربندی میکند:
![]() |
سپس سه پروژه زیر را مجدداً وارد کنید:
![]() |
- در [2]؛ پروژهها در پوشههای [<exemples>/spring-database-config/spring-jdbc] و [1] یافت میشوند؛
این سه پروژه، پروژههای Maven هستند که از پروژه Maven [mysql-config-jdbc] استفاده میکنند. این پروژه اخیر، آرتفکت Maven زیر را تولید میکند (به pom.xml مراجعه کنید):
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
همان آرتفکت توسط پروژه [oracle-config-jdbc, db2-config-jdbc, ...] تولید خواهد شد. برای اطمینان از اینکه پروژههای [spring-generic-jdbc-*] که در حال حاضر در STS بارگذاری شدهاند، واقعاً از پروژه [mysql-config-jdbc] استفاده میکنند:
- اطمینان حاصل کنید که پروژهٔ دیگری، [sgbd-config-jdbc]، همزمان بارگذاری نشود. این امر میتواند باعث خطاهایی شود که درک آنها دشوار است؛
- پیکربندی Maven پروژههای بارگذاریشده را به شرح زیر بهروزرسانی کنید:
![]() |
![]() |
برای بررسی پیکربندی خود، پیکربندیهای ساخت [spring-jdbc-generic-01.IntroJdbc01] و [1-3] را اجرا کنید:
![]() |
شما باید خروجی کنسول زیر را مشاهده کنید:
در مثالهای زیر، خواننده میتواند:
- یا مستقیماً با پروژههای بارگذاریشده قبلی کار کند؛
- یا خود پروژهها را بسازد؛
3.2. مراحل کار با یک پایگاه داده
![]() |
در معماری نشان داده شده در بالا، عملیات پایگاه داده توسط برنامه کنسول شامل مراحل زیر است:
- بارگذاری درایور پایگاه داده JDBC؛
- ایجاد اتصال به پایگاه داده؛
- ارسال یک فرمان SQL به پایگاه داده و پردازش نتایج فرمان SQL؛
- بستن اتصال؛
مرحله ۱ تنها یک بار انجام میشود. مراحل ۲ تا ۴ به طور مکرر انجام میشوند. توجه داشته باشید که اتصالات باز باقی نمیمانند؛ بلکه به محض اینکه دیگر مورد نیاز نباشند، بسته میشوند.
3.2.1. مرحله ۱ – بارگذاری درایور JDBC در حافظه
کد
// بارگذاری درایور JDBC
try {
Class.forName(nom de la classe du pilote JDBC);
} catch (ClassNotFoundException e1) {
// رسیدگی به استثنا
}
عملیات در خط ۳ به منظور بارگذاری درایور JDBC از پایگاه داده به حافظه است. این عملیات تنها یک بار نیاز به اجرا دارد. با این حال، تکرار آن باعث خطا نمیشود. کلاس درایور JDBC در مسیر کلاس (classpath) پروژه جستجو میشود. بنابراین، در پروژهٔ Eclipse، فایل [jar] حاوی کلاس درایور JDBC باید در classpath پروژه گنجانده شده باشد.
3.2.2. مرحله ۲ – برقراری ارتباط
پس از اینکه درایور JDBC در جای خود قرار گرفت، به آن دستور داده میشود تا یک اتصال با BD برقرار کند:
کد
package spring.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class IntroJdbc01 {
...
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(url, user, passwd);
...
} catch (SQLException e1) {
// پردازش استثنا
...
} finally {
// بستن اتصال
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e2) {
// پردازش استثنا
...
}
}
}
- خطوط ۳–۷: کلاسهایی که رابط JDBC را پیادهسازی میکنند، همگی در پکیج [java.sql] قرار دارند. علاوه بر این، در صورت بروز خطا، همگی یک استثنای [SQLException] را پرتاب میکنند (خطوط ۱۹ و ۲۷). این استثنا از کلاس [Exception] مشتق شده و به عنوان یک «استثنای کنترلشده» شناخته میشود: شما باید یا از یک بلوک try/catch برای رسیدگی به آن استفاده کنید یا، به عنوان جایگزینی، تصمیم بگیرید که به آن رسیدگی نکنید و با افزودن [throws SQLException] به امضای متد، نشان دهید که متد اجازه میدهد استثنا propagate (انتشار) یابد؛
- خط 17: [DriverManager.getConnection] یک متد استاتیک است که سه پارامتر میگیرد:
- [url]: URL از پایگاه داده. این یک رشتهٔ کاراکتری است که به BD مورد استفاده بستگی دارد. برای MySQL، این شکل را به خود میگیرد [jdbc:mysql://localhost:3306/nom_de_la_bd];
- [user]: مالک اتصال؛
- [passwd]: رمز عبور آنها؛
- سطور ۲۴–۳۰: اتصال باید در داخل بند [finally] بسته شود تا صرفنظر از وقوع خطا یا عدم وقوع آن، بسته شود.
3.2.3. مرحله ۳ – صدور دستورات SQL و [SELECT]
پس از برقراری اتصال، میتوان دستورات SQL را صادر کرد. روش رسیدگی به دستورات خواندن [SELECT] با روشی که برای عملیات بهروزرسانی [UPDATE, INSERT, DELETE] استفاده میشود، متفاوت است. ما با دستورات SQL و [SELECT] شروع میکنیم:
کد
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(url, user, passwd);
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت فقط خواندنی
connexion.setReadOnly(true);
// خواندن جدول [PRODUITS]
ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبیت تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException(connexion,e1);
} finally {
//بلوک `finally` پردازش میشود
doFinally(rs, ps, connexion);
}
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
- خطوط ۸ و ۱۰: باز کردن یک تراکنش (خط ۸) در حالت فقط خواندنی (خط ۱۰). یک تراکنش توالیای از دستورات SQL است که یا همگی موفق میشوند یا همگی ناموفق. بنابراین، در یک تراکنش شامل N دستور SQL، اگر دستور (I+1)ام شکست بخورد، آنگاه I دستور قبلی رولبک (roll back) میشوند. برای یک عملیات خواندن، تراکنش الزامی نیست. با این حال، ایجاد یک تراکنش فقط-خواندن ممکن است به برخی از دستورات SGBD اجازه دهد تا بهینهسازیهای خاصی را انجام دهند؛
- خط ۱۲: استفاده از یک [PreparedStatement]. یک [PreparedStatement] معمولاً دارای پارامترهایی است که با کاراکتر «؟» نشان داده میشوند. در اینجا، هیچکدام را ندارد. یک [PreparedStatement] یک عبارت آماده (prepared statement) است که توسط SGBD تولید میشود. این آمادهسازی هزینه در بر دارد و تنها یک بار انجام میشود. سپس این عبارت آماده توسط SGBD با پارامترهای واقعی متفاوت که جایگزین پارامترهای فرمال «؟» میشوند، اجرا میگردد. توجه داشته باشید که ترجیحاً بهتر است ستونهای مورد نظر را به جای استفاده از نشانه * برای بازیابی همه ستونها، با نام مشخص کنید. با مشخص کردن نام ستونها، میتوان مقادیر آنها را بر اساس موقعیتشان در پرسوجوی SELECT بازیابی کرد؛
- خط ۱۳: اجرای [PreparedStatement]. یک شیء از نوع [ResultSet] بازیابی میشود؛
یک شیء از نوع [ResultSet] یک جدول، یعنی مجموعهای از سطرها و ستونها را نشان میدهد. در هر لحظه، ما فقط به یک سطر از جدول دسترسی داریم که به آن سطر جاری گفته میشود. هنگامی که [ResultSet] برای اولین بار ایجاد میشود، هیچ سطر جاری وجود ندارد. برای بهدست آوردن آن، باید یک عملیات [ResultSet.next()] انجام شود. امضای متد next به شرح زیر است:
این متد سعی میکند به خط بعدی [ResultSet] حرکت کند و در صورت موفقیت، true را بازمیگرداند، و در غیر این صورت، false را. در صورت موفقیت، خط بعدی به خط جاری جدید تبدیل میشود. خط قبلی از بین میرود و قابل بازیابی نیست.
جدول [ResultSet] ستونهایی به نامهای labelCol1، labelCol2، … دارد، همانطور که در پرسوجوی اجراشده [SELECT] مشخص شده است. با پرسوجوی:
SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
- ستون [ID] به ستونی در [ResultSet] به نام [myId] نگاشت خواهد شد؛
- ستون [NOM] در ستونی از جدول [ResultSet] به نام [myNom] قرار داده خواهد شد؛
- ...
در مثال بالا، شناسههای [myCol] به عنوان برچسبهای ستون (column labels) شناخته میشوند. در صورت عدم وجود این برچسبها، نامهای ستون در [ResultSet] به SGBD وابسته خواهند بود. هنگامی که [SELECT] بر روی یک جدول واحد عمل میکند، برچسبهای ستون به طور پیشفرض نامهای ستونی خواهند بود که توسط SELECT درخواست شدهاند. مشکل زمانی پیش میآید که [SELECT] روی چندین جدول عمل میکند و این جدولها دارای نامهای ستون یکسان هستند، همانطور که در مثال زیر نشان داده شده است:
SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
با فرض اینکه جدول [PRODUITS] دارای یک کلید خارجی به جدول [CATEGORIES] باشد، که توسط رابطه [Produits] نشان داده میشود.CATEGORIE_ID --> [CATEGORIES].ID، و اینکه جدولهای [PRODUITS] و [CATEGORIES] هر دو دارای فیلدی به نام [NOM] هستند. در این مورد، نامهای دادهشده در [ResultSet] به ستونهای [PRODUITS.NOM] و [CATEGORIES.NOM] وابسته به SGBD هستند. بنابراین برای قابلیت حمل بین SGBD، باید از برچسبهای ستون در اینجا استفاده شود و کد به صورت زیر نوشته خواهد شد:
SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID
برای دسترسی به فیلدهای مختلف در سطر جاری [ResultSet]، روشهای زیر در دسترس هستند:
برای بازیابی ستونی به نام «labelColi» از سطر جاری، و در نتیجه ستون دارای آن برچسب در [SELECT]. Type به نوع فیلد coli اشاره دارد. میتوان از متدهای زیر [getType] استفاده کرد: getInt, getLong, getString, getDouble, getFloat، getDate، ... به جای استفاده از نام ستون، میتوانید از موقعیت آن در پرسوجوی اجراشده [SELECT] استفاده کنید:
که در آن i نمایه ستون مورد نظر است (i ≥ 1).
- خطوط ۱۵–۱۷: بازیابی مقادیری که در BD خوانده شدهاند؛
- خط ۱۹: تراکنش اعتبارسنجی میشود (که به آن commit نیز گفته میشود). این کار آن را خاتمه داده و منابعی را که SGBD برای آن تخصیص داده بود، آزاد میکند؛
- خط ۲۵: منابع در [finally] آزاد میشوند. این متد [doFinally] زیر را فراخوانی میکند:
private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
//بستن ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
//بستن [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// بستن اتصال
connexion.close();
} catch (SQLException e3) {
// رسیدگی به استثنا
}
}
}
- خطوط ۳–۹: بستهشدن [ResultSet];
- خطوط ۱۱–۱۷: [PreparedStatement] بسته میشود؛
- خطوط ۱۸–۲۷: بستن اتصال؛
با توجه به اینکه اتصال در خطوط ۱۸ تا ۲۵ بسته میشود، بستهشدنها در خطوط ۳ تا ۱۷ اضافی به نظر میرسند. در واقع، در برخی موارد آنها اضافی نیستند و توصیه میشود که آنها را به صورت [http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection] باقی بگذارید.
- خط ۲۲: استثناء توسط متد زیر مدیریت میشود:
private static void doCatchException(Connection connexion, Throwable th) {
// لغو تراکنش
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// مدیریت استثنا
}
}
- خطوط ۴–۶: تراکنش رولبک میشود. این کار آن را خاتمه میدهد و در این صورت SGBD قادر خواهد بود منابع تخصیصیافته به آن را آزاد کند؛
3.2.4. مرحله ۳ – صدور دستورات SQL و [INSERT, UPDATE, DELETE]
دستورهای SQL و [INSERT, UPDATE, DELETE] عملیات بهروزرسانی هستند: آنها پایگاه داده را تغییر میدهند اما هیچ ردیفی بازنمیگردانند. تنها اطلاعاتی که بازگردانده میشود، تعداد ردیفهای تحت تأثیر عملیات بهروزرسانی است.
کد
Connection connexion = null;
PreparedStatement ps = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(url, user, passwd);
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// بهروزرسانی جدول
ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
// دستهبندی ۱
ps.setInt(1, 10);
// اجرا
int nbLignes=ps.executeUpdate();
// تأیید تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException(connexion, e1);
} finally {
// بلوک finally اجرا میشود
doFinally(null, ps, connexion);
}
}
- خط ۹: اتصال هم برای خواندن و هم برای نوشتن استفاده میشود؛
- خط ۱۱: یک [PreparedStatement] با ۱ پارامتر (که با ؟ نشان داده شده است). ممکن است چندین پارامتر وجود داشته باشد. شمارهگذاری آنها از ۱ شروع میشود؛
- خط ۱۳: مقدار آن به پارامتر واحد تخصیص داده میشود. اولین پارامتر [setType] موقعیت پارامتر در داخل [PreparedStatement] (۱، ۲، ...) و دومی مقداری است که به آن تخصیص داده شده است. میتوان از متدهای [setInt, setLong, setFloat, setDouble, setString, setDate, ...] استفاده کرد؛
- خط 15: از متد [executeUpdate] استفاده میشود، نه [executeQuery] که برای دستورات SELECT رزرو شده است. این متد تعداد خطوط تحت تأثیر عملیات را برمیگرداند. این ممکن است 0 باشد.
- خط 17: تراکنش commit میشود؛
3.2.5. مرحله ۴ – بستن اتصال
در یک محیط چندکاربره، یک اتصال باید در اسرع وقت بسته شود، زیرا یک تراکنش SGBD تنها تعداد محدودی اتصال باز را میپذیرد. در مثالهای قبلی، این اتصال در داخل شرط [finally] عملیات SQL بسته شد، تا صرفنظر از اینکه یک استثنا رخ دهد یا خیر، بسته شود.
3.3. پیکربندی لایه JDBC از SGBD MySQL5
ما پروژه [mysql-config-jdbc] را بررسی خواهیم کرد که لایه JDBC را در زیر پیکربندی میکند:
![]() |
3.3.1. پروژه Eclipse
![]() |
3.3.2. پیکربندی Maven
فایل [pom.xml] در پروژه به شرح زیر است:
<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>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration generic jdbc</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!--وابستگیهای متغیر ********************************************** -->
<!-- راننده JDBC برای SGBD -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--وابستگیهای ثابت ********************************************** -->
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- کتابخانه jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- آزمون Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- لاگها -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
این پیکربندی Maven شامل تعدادی آرشیو است که یا توسط پروژه [mysql-config-jdbc] یا توسط پروژههایی که بر روی آن ساخته میشوند، مورد نیاز است:
- خطوط ۴–۶: آرتیفکت Maven تولید شده توسط پروژه. همانطور که قبلاً ذکر شد، تمام پروژههای از نوع [*-config-jdbc] این آرتیفکت یکسان را تولید میکنند. بنابراین، دو پروژه از نوع [*-config-jdbc] نباید همزمان بارگذاری شوند؛
- خطوط ۹–۱۳: پروژهٔ مادر Maven این پروژه. این پروژه نسخههای تعداد زیادی از کتابخانههایی را که توسط اکوسیستم Spring استفاده میشوند، تعریف میکند. این کار از نیاز به مشخص کردن آنها در پروژههایی که از آن مشتق میشوند جلوگیری میکند؛
- خطوط ۱۸–۲۱: کتابخانه درایور JDBC برای SGBD و MySQL5. این تنها کتابخانهای است که پروژه [spring-jdbc-01] به آن نیاز دارد؛
- خطوط ۲۴–۲۷: مصنوع [tomcat-jdbc] آرشیوی را که توسط پروژههای JDBC و [spring-jdbc-02 à 04] مورد نیاز است، فراهم میکند؛
- خطوط ۲۹–۳۶: کتابخانههای لازم برای مدیریت jSON را فراهم میکنند. تقریباً در همه پروژههای این سند استفاده میشوند؛
- خطوط ۳۸–۴۲: Google Guava یک کتابخانه مدیریت مجموعه است. تقریباً در همه پروژههای این سند استفاده میشود؛
- خطوط ۴۳–۵۲: کتابخانههایی که امکان نوشتن تستهایی را با Spring و JUnit فراهم میکنند. تقریباً در همه پروژههای این سند استفاده میشوند؛
- خطوط ۵۴–۵۷: کتابخانههای لاگگیری. در تقریباً همه پروژههای سند استفاده میشوند؛
- خطوط ۶۷–۷۱: افزونه برای نصب محصول پروژه [mysql-config-jdbc] در مخزن محلی Maven؛
3.3.3. کلاس پیکربندی [ConfigJdbc]
![]() |
کلاس [ConfigJdbc] به شرح زیر است:
package generic.jdbc.config;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
public class ConfigJdbc {
// پارامترهای اتصال
public final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "root";
...
// دستورالعملها SQL [jdbc-01, jdbc-02]
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
//public final static String V1_DELETE_PRODUITS = String.format("DELETE FROM %s", TAB_PRODUITS);
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
// دستورالعملها SQL [jdbc-03]
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
public final static String V2_DELETE_ALLPRODUITS = "DELETE FROM PRODUITS";
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_ALLPRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
public final static String V2_SELECT_PRODUIT_BYNAME = "SELECT ID, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE NOM=?";
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
...
}
کلاس [ConfigJdbc] برای پیکربندی لایه JDBC از چهار پروژه [spring-jdbc-01 à 04] استفاده میشود. بیشتر پیکربندی مربوط به پروژه [spring-jdbc-04] است. ما این بخش را هنگام بررسی آن پروژه پوشش خواهیم داد. فقط پیکربندی پروژههای [spring-jdbc-01 à 03] در بالا گنجانده شده است.
- خطوط ۱۴–۱۷: پارامترهای اتصال برای پایگاههای داده MySQL5 و [dbproduits]؛
- خطوط ۲۰–۲۵: دستورات SQL مورد استفاده در پروژههای [spring-jdbc-01 et 02]؛
- خطوط ۲۸–۳۴: دستورات SQL مورد استفاده در پروژه [spring-jdbc-03]؛
این دستورات SQL از جدول [PRODUITS] در پایگاههای داده MySQL5 و [dbproduits] استفاده میکنند که ساختار آن به شرح زیر است:
![]() |
- [ID]: کلید اصلی در حالت AUTO_INCREMENT (اگر کلید اصلی مشخص نشود، SGBD یکی ایجاد میکند)؛
- [NOM]: نام محصول – یکتا؛
- [CATEGORIE]: شماره دستهبندی آن؛
- [PRIX]: قیمت آن؛
- [DESCRIPTION]: توضیحی درباره محصول؛
3.3.4. کلاس [Produit]
![]() |
کلاس [Produit] تصویر یک ردیف در جدول [PRODUITS] است:
package generic.jdbc.entities.dbproduits;
public class Produit {
// fields
private int id;
private String nom;
private int categorie;
private double prix;
private String description;
// سازندهها
public Produit() {
}
public Produit(int id, String nom, int categorie, double prix, String description) {
this.id = id;
this.nom = nom;
this.categorie = categorie;
this.prix = prix;
this.description = description;
}
// گیرنده و تنظیمکننده
...
}
بعداً، برای تعیین اینکه آیا دو محصول برابر هستند یا خیر، باید آنها را مقایسه کنیم. ما میگوییم دو محصول زمانی برابر هستند که همه فیلدهایشان برابر باشند. برای این کار، متد [equals] کلاس [Object] را که کلاس [Produit] از آن مشتق شده است، مجدداً تعریف خواهیم کرد:
// روش برابری
@Override
public boolean equals(Object o) {
// موارد ساده
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Produit p = (Produit) o;
return this == o
|| (this.id == p.id && this.nom.equals(p.getNom()) && this.categorie == p.categorie
&& Math.abs(this.prix - p.prix) < 1e-6 && this.description.equals(p.description));
}
- خط ۳: متد [equals] یک شیء o را دریافت میکند که باید آن را با شیء this مقایسه کند؛
- خطوط ۵–۷: موارد ساده که در آن میتوان فوراً تشخیص داد که این دو شیء برابر نیستند. [Object].getClass() یک نمونه از نوع [Class] را بازمیگرداند، نوعی که کلاس واقعی شیء را نشان میدهد؛
- خط ۸: شیء o به یک محصول p تبدیل میشود؛
- خط ۹: اگر دو مرجع o و p به یک محصول برابر باشند، آنگاه به طور فیزیکی به همان محصول اشاره میکنند؛
- خط ۹: اگر o و p دو مرجع متفاوت به دو محصول با فیلدهای یکسان باشند، میگوییم که آنها برابر هستند. از آنجایی که قیمت از نوع [double] است و هیچ نمایش دقیقی از اعداد حقیقی در محاسبات وجود ندارد، ما دو قیمت را در صورتی یکسان در نظر میگیریم که در فاصله ۱۰⁻⁶ از یکدیگر باشند؛
علاوه بر این، ما متد [hasCode] از کلاس [Object] را مجدداً تعریف خواهیم کرد:
// هَشکد
@Override
public int hashCode() {
return id + 2 * nom.hashCode() + 3 * categorie + 4 * description.hashCode();
}
مقادیر hashCode برای دو محصول باید یکسان باشند اگر متد [equals] این دو محصول را برابر اعلام کرده باشد. این مقدار hashCode برای توزیع اشیاء در کلکسیونهایی مانند دیکشنریها استفاده میشود. همانطور که در بالا نشان داده شده است، اگر دو محصول یکسان باشند، در واقع hashCode یکسانی خواهند داشت.
3.3.5. استثنای [UncheckedException]
![]() |
معماری زیر را در نظر بگیرید:
![]() |
- لایه [JDBC] استثناهایی از نوع [SQLException] را پرتاب میکند. این استثنا باید از طریق لایهها به سمت بالا منتقل شود تا به بالاترین لایه، در این مورد لایه تست، برسد؛
لایه [DAO] میتواند بهسادگی اجازه دهد که [SQLException] به لایه تست منتقل شود. با این حال، از آنجا که این استثنا مدیریت نشده است (مستقیماً از [Exception] ناشی میشود)، این بدان معناست که رابط [IDao] لایه [DAO] به شرح زیر است:
public interface IDao {
// افزودن محصولات
public List<Produit> addProduits(List<Produit> produits) throws SQLException;
// فهرست تمام محصولات
public List<Produit> getAllProduits() throws SQLException;
//یک محصول خاص
public Produit getProduitById(int id) throws SQLException;
public Produit getProduitByName(String name) throws SQLException;
// بهروزرسانی چندین محصول
public int updateProduits(List<Produit> produits) throws SQLException;
// حذف تمام محصولات
public int deleteAllProduits() throws SQLException;
// حذف چند محصول
public int deleteProduits(int[] ids) throws SQLException;
}
و این بسیار آزاردهنده است زیرا ما را از پیادهسازی رابط [IDao] با استفاده از کلاسی که استثناء متفاوتی را پرتاب میکند، باز میدارد. برای دور زدن این مشکل، لایه [DAO] یک استثنای [DaoException] (که از [RuntimeException] مشتق شده است) را که گرفته نشده، پرتاب خواهد کرد، که به ما اجازه میدهد بند [throws] را از امضاهای متد این رابط حذف کنیم. در نتیجه، این رابط میتواند توسط هر کلاسی پیادهسازی شود که همچنین یک استثنای بدونگیرنده (uncaught) پرتاب کند، که ممکن است با استثنای [DaoException] متفاوت باشد. معماری ما اکنون به شرح زیر است:
![]() |
برای آسانتر کردن ایجاد استثناءهای رسیدگینشده برای لایههای مختلف یک برنامه، یک کلاس والد برای آنها ایجاد میکنیم، [UncheckedException]:
![]() |
package generic.jdbc.infrastructure;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
// کلاس استثنای عمومی
// استثناء رسیدگی نشده است
public class UncheckedException extends RuntimeException {
// سریال ID تولید شد
private static final long serialVersionUID = -2924871763340170310L;
// ویژگیها
private int code;
private String trace;
private List<ShortException> exceptions;
// سازندهها
public UncheckedException() {
super();
}
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// محلی
this.code = code;
this.exceptions = getErreursForException(e);
// ردیابی
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
@Override
public String getMessage() {
return this.toString();
}
@Override
public void printStackTrace() {
System.out.println(this);
}
// فهرست پیامهای خطا برای یک استثنا
private List<ShortException> getErreursForException(Throwable th) {
// عناصر پشته استثنا را بازیابی میکند
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
//بازیابی استثنای فعلی
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// استثنای بعدی
cause = cause.getCause();
}
return exceptions;
}
@Override
public String toString() {
ObjectMapper jsonMapper = new ObjectMapper();
try {
return String.format("[code=%s, trace=%s, exceptions=%s", code, trace, jsonMapper.writeValueAsString(exceptions));
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// گیرنده و تنظیمکننده
...
}
- خط ۱۲: این کلاس از [RuntimeException] ارث میبرد و بنابراین یک نوع استثنای بدون بررسی (unchecked) است. از آن برای محصور کردن یک استثنای بررسیشده (checked) (SQLException) در داخل یک نوع استثنای بدون بررسی (UncheckedException) استفاده خواهد شد؛
- برای تمایز بین استثناهای نوع [UncheckedException]، میتوان برای هر یک کدی تعیین کرد که در فیلد خصوصی در خط ۱۸ ذخیره خواهد شد. یک متد جاوا که یک استثنای از نوع [UncheckedException] را رهگیری میکند، از طریق متد [getCode] (از خط ۸۰ به بعد) به این کد خطا دسترسی خواهد داشت؛
- خط ۲۰: پیامهای خطا را از استک استثنای پوشاندهشده ذخیره میکند؛
- خطوط ۲۳–۴۳: روشهای مختلف ساخت یک شی از نوع [UncheckedException]؛
- خطوط ۵۶–۶۷: یک متد خصوصی که امکان ساخت لیست خطاها از خط ۲۰ را از روی یک شیء [Throwable] یا یک نوع مشتقشده، بهویژه نوع [Exception]، فراهم میکند؛
- خطوط ۶۹–۷۸: متد [toString] رشتهای را که نمایانگر استثنا است بازمیگرداند. برای نمایش لیست خطاها در خط ۲۰، از کتابخانه jSON استفاده میکند. این کتابخانه در وابستگیهای Maven پروژه گنجانده شده است:
<!-- کتابخانه jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- خطوط ۴۵–۴۸: متد [getMessage] از کلاس والد [RuntimeException] را مجدداً تعریف کنید. در اینجا، این متد امضای [toString] کلاس را بازمیگرداند؛
- خطوط ۵۰–۵۳: متد [printStackTrace] از کلاس والد [RuntimeException] را مجدداً تعریف کنید. این امضای کلاس [toString] است که نمایش داده خواهد شد؛
کلاس [UncheckedException] در فیلد خط ۲۰، فهرستی از استثناها را که توسط نوع زیر [ShortException] توصیف شدهاند، ذخیره میکند:
package pam.dao.exceptions;
public class ShortException {
// ویژگیها
private String className;
private String errorMessage;
// سازندهها
public ShortException() {
}
public ShortException(String className, String errorMessage) {
this.className = className;
this.errorMessage = errorMessage;
}
// گیرندهها و تنظیمکنندهها
...
}
- خط ۶: نام کلاس استثنایی که رخ داده است؛
- خط ۷: پیام خطای مرتبط؛
بیایید سازندهٔ زیرِ کلاس [UncheckedException] را بررسی کنیم:
public UncheckedException(int code, Throwable e, String simpleClassName) {
super(e);
// محلی
this.code = code;
this.exceptions = getErreursForException(e);
// ردیابی
String fileName = String.format("%s.java", simpleClassName);
StackTraceElement[] traces = e.getStackTrace();
boolean trouve = false;
for (int i = 0; !trouve && i < traces.length; i++) {
StackTraceElement trace = traces[i];
if (fileName.equals(trace.getFileName())) {
this.trace = String.format("[%s,%s,%s]", simpleClassName, trace.getMethodName(), trace.getLineNumber());
trouve = true;
}
}
}
- خط ۱: پارامترها به شرح زیر هستند:
- [code]: یک کد خطا؛
- [e]: استثناءی دربرگرفته شده. [Throwable] کلاس والد کلاس [Exception] است و مستقیماً از کلاس [Object] ارث میبرد. این کلاس والدین تمام کلاسهای C است که میتوان با آن [throw c;] را نوشت، جایی که c یک نمونه از C است؛
- [simpleClassName]: نام کوتاه کلاس کد کاربر که در آن استثنا e شناسایی شد؛
- خط ۴: کد خطا ثبت میشود؛
- خط ۵: لیست برای [ShortException] از [Throwable e] که بهعنوان پارامتر ارسال شده است، ساخته میشود؛
- خطوط ۷–۱۶: ردپاهای استثنا بررسی میشوند. یک استثنای اولیه در نقطهای خاص از کد رخ میدهد و سپس به روشی که آن را فراخوانی کرده است بازگردانده میشود، و به همین ترتیب تا زمانی که یک بلوک try/catch آن را میگیرد. در طول این انتشار، استثنای اولیه ردپاهایی را در آرایه [e.stackTrace] مربوط به استثنای e به جا میگذارد. این ردپاها در خط ۸ از [Throwable e] که بهعنوان پارامتر ارسال شده است، بازیابی میشوند. هر عنصر از نوع [StackTraceElement] یک شیء است که فیلدهای آن شامل موارد زیر است:
- [fileName]: نام فایل جاوا که در آن استثناء رخ داده است؛
- [lineNumber]: شماره خط در آن فایل که استثنا در آن رخ داده است؛
- [methodName]: نام متدی در آن فایل که استثنا در آن رخ داده است؛
- خطوط ۱۰ تا ۱۶ آرایهٔ ردپاها را برای استثناء ارسالشده بهعنوان پارامتر جستجو میکنند و اولین وقوع شرط [trace.fileName==simpleClassName.java] را پیدا میکنند، که در آن [simpleClassName] سومین پارامتر سازنده است. ایده این است که محل وقوع استثنا در کد کاربر ثبت شود. کد کاربر استثنا را به صورت زیر میپیچاند:
- خط ۱۳: یک رشته از نوع [fileName, methodName, lineNumber] ایجاد میشود که مکان وقوع استثنا در کد کاربر را شناسایی میکند؛
اکنون، بیایید کدی را بررسی کنیم که لیست استثناها را از روی پشته استثناهای استثنای [Throwable th] که توسط سازنده قبلی دربرگرفته شده است، ثبت میکند:
// فهرست پیامهای خطا برای یک استثنا
private List<ShortException> getErreursForException(Throwable th) {
// عناصر استک استثنا را بازیابی میکند
Throwable cause = th;
List<ShortException> exceptions = new ArrayList<ShortException>();
while (cause != null) {
//استثناء فعلی را بازیابی میکند
exceptions.add(new ShortException(cause.getClass().getName(), cause.getMessage()));
// استثناء بعدی
cause = cause.getCause();
}
return exceptions;
}
در طول انتشار آن به سمت متدی که از طریق یک بلاک try/catch آن را گرفته است، ممکن است استثنای اولیه e در درون یک استثنای دیگر قرار گرفته باشد. سپس این استثنای دوم است که به سمت متدی که در نهایت آن را میگیرد، منتشر میشود. بنابراین، این استثنا نیز ممکن است تحت پوشش قرار گیرد. در نهایت، هنگامی که یک متد تصمیم میگیرد یک استثنا th را بگیرد و آن را مدیریت کند، استثنای اولیه e را در ته یک پشته از استثناها مدفون خواهد یافت. بنابراین، در مثال بالا، پارامتر [Throwable th] صرفاً نوک کوه یخ در زمینه استثناها است. ویژگی [th.cause] آن، استثناءای را که خود در بر دارد، آشکار میسازد. و به همین ترتیب. هنگامی که یک استثناء e دارای ویژگی [e.getCause()==null] باشد، این بدان معناست که e استثناء اولیه است.
- خط ۸: برای هر استثناء در پشته استثناء [Throwable th]، دو مورد اطلاعات ذخیره میشود:
- [getClass().getName()]: نام کامل استثنا؛
- [getMessage()]: پیام خطای مرتبط؛
3.4. Exemple-01
3.4.1. معماری پروژه
![]() |
در این مثال، یک برنامه کنسول از رابط لایه [JDBC] استفاده میکند.
3.4.2. پروژه اکلیپس
ما یک پروژه Spring/Maven به نام [spring-jdbc-01] ایجاد میکنیم، مطابق با رویهای که در بخش 2.5.2.1 تشریح شده است.
![]() |
این پروژه یک پروژه 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-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-01</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- جستجوی والد از مخزن -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--پیکربندی JDBC از SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- خطوط ۲۸–۳۲: پروژه از آرتیکفکت [generic-config-jdbc] از پروژه [mysql-config-jdbc] که همین حالا بررسی کردیم، استفاده میکند. بنابراین پروژه [spring-jdbc-01] به تمام عناصر پروژه [mysql-config-jdbc] دسترسی دارد؛
این نکتهٔ آخر را میتوان با بررسی وابستگیهای Maven پروژه به دو روش مشاهده کرد:
![]() |
- در [2] میتوانیم ببینیم که پروژه [mysql-config-jdbc] در میان وابستگیهای Maven پروژه فهرست شده است. از آنجایی که این وابستگیها در کلاسپث پروژه قرار دارند، این بدان معناست که پروژه [mysql-config-jdbc] نیز در این کلاسپث قرار دارد و در نتیجه، کلاسها و اینترفیسهای آن در پروژه [spring-jdbc-01] قابل مشاهده هستند؛
پروژه Maven [mysql-config-jdbc] نیازی ندارد که برای استفاده توسط سایر پروژههای Maven در برگه [Package Explorer] موجود باشد. این پروژه تنها باید در مخزن محلی Maven موجود باشد. برخلاف پروژهای مانند IDE در NetBeans، این کار در Eclipse بهصورت خودکار انجام نمیشود. باید بهصورت اجباری انجام شود:
![]() |
ما در بخش 2.3.5 شرایطی را که این نسل را فعال میکنند، بررسی کردیم. پس از انجام این کار، پروژه [mysql-config-jdbc] را میتوان از برگه [Package Explorer] حذف کرد:
![]() |
- مورد [3] را تیک نزنید، زیرا این کار پروژه را به طور فیزیکی از روی دیسک حذف میکند و بازیابی آن را غیرممکن میسازد؛
این عملیات باعث محاسبه مجدد وابستگیهای Maven برای پروژههایی میشود که به پروژهای که از [Package Explorer] حذف شده وابسته هستند. این امر شاخه [Maven Dependencies] این پروژهها را تغییر میدهد. برای مثال، برای پروژه [spring-jdbc-01]، شاخه [Maven Dependencies] به شرح زیر درمیآید:
![]() |
این بار، وابستگی دیگر به یک پروژه نیست، بلکه به آرتیفکت Maven آن است، که در این مورد آرتیفکت [generic-config-jdbc] [1] است. میتوانیم ببینیم که واقعاً به تمام کلاسها و رابطهای این آرتیفکت دسترسی داریم. همانطور که گفته شد، این آرتیفکت توسط تمام پروژههای [*-config-jdbc] تولید خواهد شد. برای جلوگیری از خطاها، ما:
- همیشه یک پروژه [*-config-jdbc] را در زبانه [Package Explorer] نگه میداریم؛
- پیکربندی Maven تمام پروژهها را در زبانه [Package Explorer] (Alt-F5) بهروزرسانی کنید تا پروژه [*-config-jdbc] را در وابستگیهای Maven خود بگنجانند؛
3.4.3. اسکلت کلاس اصلی
![]() |
اسکلت کلاس اصلی [IntroJdbc01] به شرح زیر است:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IntroJdbc01 {
// ثوابت
final static ObjectMapper jsonMapper = new ObjectMapper();
public static void main(String[] args) {
//بارگذاری درایور JDBC از SGBD
try {
Class.forName(ConfigJdbc.DRIVER_CLASSNAME);
} catch (ClassNotFoundException e1) {
doCatchException("Pilote JDBC introuvable", null, e1);
return;
}
//جدول پاک میشود [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
// پر کردن آن
System.out.println(String.format("------------------------------ %s", "Remplissage de la table [PRODUITS]"));
insert();
// جدول را بخوانید
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// بهروزرسانی
System.out.println(String.format("------------------------------ %s", "Mise à jour de la table [PRODUITS]"));
update();
// نمایش
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// جدول را پاک کنید [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
//نمایش
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
// INSERTion شامل دو عنصر یکسان
// INSERTion باید شکست بخورد و هیچیک از دو عنصر به دلیل تراکنش درج نمیشوند
System.out.println(String.format("------------------------------ %s",
"Insertion de deux produits de même clé primaire dans la table [PRODUITS]"));
insert2();
// ما بررسی میکنیم
System.out.println(String.format("------------------------------ %s", "Affichage de la table [PRODUITS]"));
select();
//انجام شد
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// فهرست محصولات
private static void select() {
...
}
//نمایش یک شیء jSON
private static void affiche(Object object) {
...
}
// حذف محصولات
public static void delete() {
...
}
// افزودن محصولات
public static void insert() {
...
}
// افزودن ۲ محصول با کلیدهای اصلی یکسان
public static void insert2() {
...
}
// بهروزرسانی برخی محصولات
public static void update() {
...
}
private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
// بستن ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
//بستن [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
if (connexion != null) {
try {
// بستن اتصال
connexion.close();
} catch (SQLException e3) {
// پیامهای خطا نمایش داده میشوند
show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
getErreursFromThrowable(e3));
}
}
}
private static void doCatchException(String title, Connection connexion, Throwable th) {
// پیامهای خطا نمایش داده میشوند
show(title, getErreursFromThrowable(th));
// معامله لغو شد
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
// نمایش پیامهای خطا
show("Erreur lors de l'annulation de la transaction", getErreursFromThrowable(e2));
}
}
private static List<String> getErreursFromThrowable(Throwable th) {
// فهرست پیامهای خطا را از استثنا بازیابی میکند
List<String> erreurs = new ArrayList<String>();
while (th != null) {
// پیام خطا برای throwable
erreurs.add(th.getMessage());
// به علت خطای قابل پرتاب برو
th = th.getCause();
}
// نتیجه
return erreurs;
}
private static void show(String title, List<String> messages) {
// عنوان
System.out.println(String.format("%s : ", title));
// پیامها
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
- خطوط ۲۳–۲۹: بارگذاری درایور JDBC از SGBD. در خط ۲۵، از ثابت [ConfigJdbc.DRIVER_CLASSNAME] که در پروژه [mysql-config-jdbc] تعریف شده است، استفاده میشود؛
- خطوط 136–147: متد [getErreursFromThrowable] لیست پیامهای خطا را که در یک شیء از نوع [Throwable] جای گرفتهاند، بازمیگرداند، که کلاس والد کلاس [Exception] است. یک استثنا ممکن است شامل یک استثنای دیگر باشد که میتوان آن را با استفاده از متد [Throwable].getCause() بازیابی کرد. این امکان را میدهد تا تمام استثناهای نهفته در شیء [Throwable] را به صورت پیمایشی بررسی کرد؛
- خطوط 149–156: متد [show(String title, List<String> messages)] پیامهایی را که با متن [title] شروع میشوند، نمایش میدهد؛
- خطوط ۱۲۲–۱۳۴: متد [doCatchException(String title, Connection connexion, Throwable th))] استثناءهای برخوردکردهشده توسط متدهای کلاس را مدیریت میکند. استثناء در حال مدیریت توسط پارامتر [Throwable th] نمایانده میشود. هدف این متد:
- برای بازگشت تراکنش جاری شیء [Connection connexion] (خطوط ۱۲۷–۱۲۹);
- نوشتن پیامهای خطایی که در استثنای [Throwable th] گنجانده شدهاند (خطوط 124، 132)؛
- خطوط ۹۳–۱۲۰: متد [doFinally(ResultSet rs, PreparedStatement ps, Connection connexion)] شاخه [finally] از روشهای دسترسی برای SGBD را مدیریت میکند. هدف آن آزادسازی منابع مورد استفاده توسط اتصال است؛
3.4.4. حذف محتویات جدول محصول
روش [delete] محتویات جدول را حذف میکند:
// حذف محصولات
public static void delete() {
Connection connexion = null;
PreparedStatement ps = null;
try {
//اتصال برقرار شد
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت خواندن/نوشتن
connexion.setReadOnly(false);
//پاکسازی جدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_DELETE_PRODUITS);
ps.executeUpdate();
// تثبیت تراکنش
connexion.commit();
} catch (SQLException e1) {
// رسیدگی به استثنا
doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
} finally {
// پردازش بلاک finally
doFinally(null, ps, connexion);
}
}
خط ۷ از ثابتهای زیر از کلاس [ConfigJdbc] استفاده میکند:
public final static String URL_DBPRODUITS = "jdbc:mysql://localhost:3306/dbproduits";
public final static String USER_DBPRODUITS = "root";
public final static String PASSWD_DBPRODUITS = "";
در خط ۱۳، دستور آمادهشده SQL به شرح زیر است:
public final static String V1_DELETE_PRODUITS = "DELETE FROM PRODUITS";
متد [delete] از تراکنشها استفاده میکند. یک تراکنش به شما امکان میدهد سفارشهای SQL را با هم گروهبندی کنید، که باید همگی موفق شوند یا همگی لغو شوند. چهار عملیات وجود دارد که باید از آنها آگاه باشید:
- شروع یک تراکنش: [connexion.setAutoCommit(false)];
- پایان موفقیتآمیز یک تراکنش: [connexion.commit()]. در این حالت، تمام عملیات انجامشده روی BD در طول تراکنش اعتبارسنجی میشوند؛
- پایان یک تراکنش با خطا: [connexion.rollback()]. در این حالت، تمام عملیات انجامشده روی BD در طول تراکنش برگشت داده میشوند؛
در مثالهای ما، هرگاه استثنائی رخ دهد، تراکنش را در متد [doCatchException] بازگردانده (roll back) میکنیم:
private static void doCatchException(String title, Connection connexion, Throwable th) {
// نمایش پیامهای خطا
Static.show(title, Static.getErreursFromThrowable(th));
// برگردان تراکنش
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
//نمایش پیامهای خطا
Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
}
}
3.4.5. ایجاد محتویات جدول محصولات
متد [insert] محتویات جدول را ایجاد میکند:
public static void insert() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت خواندن/نوشتن
connexion.setReadOnly(false);
//جدول در حال پر شدن است
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_WITH_ID);
for (int i = 0; i < 10; i++) {
//آمادهسازی
int n = i + 1;
ps.setInt(1, n);
ps.setString(2, String.format("NOM%s", n));
ps.setInt(3, n / 5 + 1);
ps.setDouble(4, 100 * (1 + (double) i / 100));
ps.setString(5, String.format("DESC%s", n));
// اجرا
ps.executeUpdate();
}
// تثبیت تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
} finally {
// پردازش بلاک finally
doFinally(null, ps, connexion);
}
}
خط ۱۲: دستور آمادهشده SQL به شرح زیر است:
public final static String V1_INSERT_PRODUITS_WITH_ID = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
3.4.6. نمایش محتویات جدول محصولات
روش [select] محتویات جدول را نمایش میدهد:
// فهرست محصولات
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت فقط خواندنی
connexion.setReadOnly(true);
// خواندن جدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبیت تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
//بلوک `finally` پردازش میشود
doFinally(rs, ps, connexion);
}
}
خط ۱۴: دستور آمادهشده SQL به شرح زیر است:
public final static String V1_SELECT_PRODUITS = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
روش [affiche] (خط ۱۸) به شرح زیر است:
//نمایش یک شیء jSON
private static void affiche(Object object) {
try {
System.out.println(jsonMapper.writeValueAsString(object));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
نمایندگی jSON شیء ارسالشده بهعنوان پارامتر را نمایش میدهد (رجوع کنید به jSON، بند 23.12).
3.4.7. بهروزرسانی محتوای جدول
روش [update] برخی محصولات را بهروزرسانی میکند:
// بهروزرسانی برخی محصولات
public static void update() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// باز کردن یک اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// بهروزرسانی جدول
ps = connexion.prepareStatement(ConfigJdbc.V1_UPDATE_PRODUITS);
// دستهبندی ۱
ps.setInt(1, 1);
// اجرا
ps.executeUpdate();
// تأیید تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
} finally {
//بلوک finally اجرا میشود
doFinally(null, ps, connexion);
}
}
خط ۱۳: دستور آمادهشده SQL به شرح زیر است:
public final static String V1_UPDATE_PRODUITS = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
3.4.8. نقش تراکنش
روش [insert2] دو محصول با کلید اصلی یکسان را در جدول درج میکند که مجاز نیست. از آنجا که این بخشی از یک تراکنش است، اولین درج برگشت داده خواهد شد.
// افزودن دو محصول با کلیدهای اصلی یکسان
public static void insert2() {
Connection connexion = null;
PreparedStatement ps = null;
try {
// باز کردن اتصال
connexion = DriverManager.getConnection(ConfigJdbc.URL_DBPRODUITS , ConfigJdbc.USER_DBPRODUITS, ConfigJdbc.PASSWD_DBPRODUITS);
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// افزودن ۱ ردیف
ps = connexion.prepareStatement(ConfigJdbc.V1_INSERT_PRODUITS_2);
// اجرا
ps.executeUpdate();
// همان ردیف برای بار دوم اضافه میشود، بنابراین با همان کلید اصلی
//INSERTion باید شکست بخورد و هیچیک از دو رکورد نباید درج شود به دلیل تراکنش
ps.executeUpdate();
// تأیید تراکنش
connexion.commit();
} catch (SQLException e1) {
// استثنا را مدیریت کنید
doCatchException("Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire",
connexion, e1);
} finally {
// بلوک `finally` اجرا میشود
doFinally(null, ps, connexion);
}
}
خط ۱۳: دستور آمادهشده SQL به شرح زیر است:
public final static String V1_INSERT_PRODUITS_2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
3.4.9. نتایج
ما پیکربندی اجرای به نام [spring-jdbc-generic-01.IntroJdbc01] را اجرا میکنیم:
![]() |
خروجی کنسول زیر نمایش داده میشود:
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Remplissage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Mise à jour de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
------------------------------ Vidage de la table [PRODUITS]
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Insertion de deux produits de même clé primaire dans la table [PRODUITS]
Les erreurs suivantes se sont produites lors de l'ajout de deux produits de même clé primaire :
- Duplicate entry '100' for key 'PRIMARY'
------------------------------ Affichage de la table [PRODUITS]
Liste des produits :
------------------------------ Travail terminé
- خط ۳۰: قبل از درج دو محصول با کلید اصلی یکسان، جدول خالی است؛
- خط ۳۵: پس از درج دو محصول با کلید اصلی یکسان، جدول خالی است. این نقش تراکنش را نشان میدهد:
- اولین درج با موفقیت انجام میشود. دلیلی برای شکست آن وجود ندارد؛
- دومین درج با شکست مواجه میشود (خط ۳۲). در نتیجه، از آنجا که این دو درج در یک تراکنش واحد قرار دارند، تمام دستورات SQL در آن تراکنش، از جمله درج اول، لغو میشوند.
3.4.10. نتیجهگیری
نکته قابل توجه در مورد کد بالا، حجم قابل توجهی از فضا است که به رسیدگی به استثنای [SQLException] اختصاص داده شده است. از آنجایی که هر عملیاتی در JDBC ممکن است این استثنا را فعال کند، بلاکهای try/catch متعددی در کد وجود دارد.
3.5. Exemple-02
ما اپلیکیشن قبلی را با استفاده از یک منبع داده [javax.sql.DataSource] مجدداً بررسی خواهیم کرد:

ما از یک منبع داده پیادهسازیشده توسط کلاس [org.apache.tomcat.jdbc.pool.DataSource] استفاده خواهیم کرد. این کلاس از یک استخر اتصال، یعنی مجموعهای از اتصالات باز، استفاده میکند:
- هنگامی که استخر ایجاد میشود، تعدادی اتصال به پایگاه داده باز میشوند. این تعداد قابل تنظیم است؛
- وقتی کد جاوا یک اتصال را باز میکند، این اتصال توسط پول فراهم میشود؛
- وقتی کد جاوا یک اتصال را میبندد، آن به استخر بازگردانده میشود؛
در نهایت، اتصالات تنها یک بار باز میشوند که این امر عملکرد دسترسی به پایگاه داده را بهبود میبخشد. منبع داده در یک کلاس پیکربندی Spring تعریف خواهد شد
3.5.1. معماری پروژه
![]() |
در این مثال، یک برنامه کنسول از رابط لایه [JDBC] استفاده میکند.
3.5.2. پروژه اکلیپس
میتوان پروژه جدید اکلیپس را با کپی کردن پروژه قبلی، [1-6]، ایجاد کرد:
![]() |
![]() |
سپس پروژه از [6] به [7] بهروزرسانی میشود:
3.5.3. پیکربندی Maven
پروژه [7] یک پروژه 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-02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-02</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- جستجوی والد در مخزن -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--پیکربندی JDBC از SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- خطوط ۲۸–۳۳: وابستگی Maven به پروژه [mysql-config-jdbc]؛
این پروژه [mysql-config-jdbc] است که در میان وابستگیهای Maven خود، کتابخانهای را شامل میشود که پیادهسازی یک منبع داده از نوع [javax.sql.DataSource] را ارائه میدهد (به بخش 3.3.2 مراجعه کنید):
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
3.5.4. پیکربندی اسپرینگ
![]() |
کلاس پیکربندی Spring با شناسه [AppConfig] به شرح زیر است:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@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_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// اتصالات اولیه را باز میکند
dataSource.setInitialSize(5);
// نتیجه
return dataSource;
}
}
- خط ۱۰: [AppConfig] یک کلاس پیکربندی Spring است؛
- خط ۱۱: وارد کردن کلاس پیکربندی [generic.jdbc.config.ConfigJdbc.class] که در پروژه [mysql-config-jdbc] تعریف شده است. این بدان معناست که تمام بی́nها (beans) تعریفشده توسط این فایل پیکربندی در دسترس هستند؛
- خطوط ۱۴–۲۷: تعریف بیون اسپرینگ که منبع داده را مشخص میکند؛
- خط 17: ایجاد منبع داده که هنوز پیکربندی نشده است؛
- خطوط ۱۹–۲۲: اطلاعاتی که به منبع داده امکان اتصال به پایگاه داده را میدهد؛
- خط ۲۴: یک استخر ۵ ارتباطی ایجاد میکند. در اینجا تنها به یک ارتباط نیاز است. هرگز چندین ارتباط همزمان وجود ندارد؛
3.5.5. کلاس اصلی
کلاس اصلی [IntroJdbc02] به شرح زیر است:
package spring.jdbc;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class IntroJdbc02 {
// نقشه jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
//منبع داده
private static DataSource dataSource;
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = null;
try {
// بازیابی کانکست Spring
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// بازیابی منبع داده
dataSource = ctx.getBean(DataSource.class);
//پاکسازی جدول [PRODUITS]
System.out.println(String.format("------------------------------ %s", "Vidage de la table [PRODUITS]"));
delete();
...
// تکمیل شد
System.out.println(String.format("------------------------------ %s", "Travail terminé"));
}
// فهرست محصولات
private static void select() {
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// اتصال برقرار شد
connexion = dataSource.getConnection();
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت فقط خواندنی
connexion.setReadOnly(true);
// خواندن جدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V1_SELECT_PRODUITS);
rs = ps.executeQuery();
System.out.println("Liste des produits : ");
while (rs.next()) {
affiche(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
// تثبیت تراکنش
connexion.commit();
} catch (SQLException e1) {
// پردازش استثنا
doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
} finally {
// پردازش بلاک finally
doFinally(rs, ps, connexion);
}
}
...
- خط ۲۵: منبع داده. توجه داشته باشید که این از نوع [javax.sql.DataSource] (خط ۱۳) است که یک رابط (interface) است؛
- خط ۳۱: نمونهسازی اشیاء Spring؛
- خط ۳۲: دریافت مرجع به منبع داده. توجه داشته باشید که کلاسی که در واقع استفاده میشود هرگز ذکر نشده است. بنابراین، در اینجا هیچ چیزی وجود ندارد که نشان دهد پیادهسازی [TomcatJdbc] در حال استفاده است؛
- خط ۴۹: دریافت یک اتصال باز. این روشی است که متدهای مختلف [IntroJdbc02] برای برقراری اتصال به پایگاه داده استفاده میکنند. بقیه کد با کلاس [IntroJdbc01] یکسان است؛
3.5.6. آزمایشها
ما پیکربندی اجرای به نام [spring-jdbc-generic-02.IntroJdbc02] را اجرا میکنیم:
![]() |
همان نتایج مشابه بخش قبلی (بخش 3.4.9) به دست آمد.
3.6. Exemple-03
3.6.1. معماری پروژه
![]() |
در این مثال، روشهای دسترسی به دادهها در لایهای به نام [dao] محصور شدهاند. این روشها توسط یک تست JUnit آزمایش خواهند شد.
3.6.2. پروژه اکلیپس
پروژه Eclipse با شناسه [spring-jdbc-03] یک پروژه Spring/Maven است که به همان شیوه پروژه قبلی ساخته شده و سپس به شرح زیر توسعه یافته است:
![]() | ![]() |
پکیجهای مختلف نقشهای زیر را دارند:
- [spring.jdbc.config]: پیکربندی پروژه Spring؛
- [spring.jdbc.dao]: پیادهسازی لایه [DAO]؛
- [spring.jdbc.infrastructure]: استثناء رسیدگینشده [DaoException] را پیادهسازی میکند؛
3.6.3. پیکربندی Maven
پروژه 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-03</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jdbc-generic-03</name>
<description>Demo project for API JDBC</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
<relativePath /> <!-- جستجوی والد در مخزن -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--پیکربندی JDBC از SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
این دقیقاً مشابه پروژه [spring-jdbc-02] است. بهویژه، از وابستگی Maven پروژه [mysql-config-jdbc] (خطوط 28–32) استفاده میکند.
3.6.4. رابط لایه [DAO]
![]() |
لایه [DAO] رابط زیر را از [IDao] فراهم میکند:
package spring.jdbc.dao;
import java.util.List;
import spring.jdbc.entities.Produit;
public interface IDao {
// افزودن محصولات
public List<Produit> addProduits(List<Produit> produits);
// فهرست تمام محصولات
public List<Produit> getAllProduits();
// یک محصول خاص
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// بهروزرسانی چندین محصول
public int updateProduits(List<Produit> produits);
// حذف تمام محصولات
public int deleteAllProduits();
// حذف چند محصول
public int deleteProduits(int[] ids);
}
3.6.5. کلاس [DaoException]
کلاس [DaoException] به سادگی کلاس [UncheckedException] ارائهشده در بخش 3.3.5 را گسترش میدهد:
![]() |
package spring.jdbc.infrastructure;
public class DaoException extends UncheckedException {
private static final long serialVersionUID = 1L;
// تولیدکنندگان
public DaoException() {
super();
}
public DaoException(int code, Throwable e, String className) {
super(code, e, className);
}
}
3.6.6. پیکربندی پروژه Spring
![]() |
کلاس [AppConfig] که پروژه اسپرینگ را پیکربندی میکند، به جز خط ۱۱، با فایل پیکربندی اسپرینگ در مثال [spring-jdbc-02] یکسان است:
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;
@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
public class AppConfig {
//منبع داده
@Bean
public DataSource dataSource() {
//منبع داده TomcatJdbc
DataSource dataSource = new DataSource();
//پیکربندی دسترسی JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITS);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITS);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITS);
// اتصالات اولیه را باز کنید
dataSource.setInitialSize(5);
// نتیجه
return dataSource;
}
}
- خط ۱۱: بسته [spring.jdbc.dao] برای یافتن کامپوننتهای Spring دیگر غیر از آنهایی که در این فایل پیکربندی تعریف شدهاند، اسکن خواهد شد؛
3.6.7. پیادهسازی لایه [DAO]
![]() |
![]() |
توجه داشته باشید (بخش 3.6.4) که لایه [DAO] رابط زیر [IDao] را پیادهسازی میکند:
package spring.jdbc.dao;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.List;
public interface IDao {
// افزودن محصولات
public List<Produit> addProduits(List<Produit> produits);
// فهرست تمام محصولات
public List<Produit> getAllProduits();
// یک محصول خاص
public Produit getProduitById(int id);
public Produit getProduitByName(String name);
// بهروزرسانی چندین محصول
public int updateProduits(List<Produit> produits);
// حذف همه محصولات
public int deleteAllProduits();
//حذف چند محصول
public int deleteProduits(int[] ids);
}
هر دو کلاس [Dao1, Dao2] این رابط را پیادهسازی میکنند. کلاس [Dao2] گونهای از کلاس [Dao1] است که یک ویژگی نحوی جدید را معرفی میکند. ما بر روی کلاس [Dao1] تمرکز خواهیم کرد. ساختار پایهای آن به شرح زیر است:
package spring.jdbc.dao;
import generic.jdbc.config.ConfigJdbc;
import generic.jdbc.entities.dbproduits.Produit;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.jdbc.infrastructure.DaoException;
@Component("dao1")
public class Dao1 implements IDao {
// نام کلاس
private String simpleClassName = getClass().getSimpleName();
//منبع داده
@Autowired
protected DataSource dataSource;
// تولیدکننده
public Dao1() {
System.out.println("building Dao1...");
}
// ------------------------------- رابط
@Override
public List<Produit> getAllProduits() {
...
}
@Override
public Produit getProduitById(int id) {
...
}
@Override
public Produit getProduitByName(String name) {
...
}
@Override
public List<Produit> addProduits(List<Produit> produits) {
....
}
@Override
public int updateProduits(List<Produit> produits) {
...
}
@Override
public int deleteAllProduits() {
...
}
@Override
public int deleteProduits(int[] ids) {
...
}
// ---------------------------------------- متدهای محلی
// بالاخره بلوک
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
...
}
// مدیریت خطا
protected DaoException doCatchException(Connection connexion, Throwable th, int code, DaoException daoException) {
...
}
- خط ۲۰: کلاس [Dao] یک کامپوننت Spring با نام [dao1] است. این نام اختیاری است. هنگامی که این نام وجود نداشته باشد، نام استفاده شده، نام کلاس با حروف کوچکشده برای حرف اول خواهد بود؛
- خط 24: نام کلاس. ما از هاردکد کردن '[Dao]' خودداری میکنیم تا امکان تغییر نام کلاس بدون نیاز به تعریف مجدد این فیلد فراهم شود، که در این صورت همواره معتبر باقی میماند؛
- خطوط ۲۶–۲۷: تزریق منبع داده [tomcat-jdbc] که در کلاس پیکربندی [AppConfig] تعریف شده است؛
- خطوط ۳۶–۶۸: پیادهسازی رابط [IDao];
- خطوط ۷۸–۸۰: مدیریت متمرکز catch برای متدهای مختلف؛
- خطوط ۷۲–۷۵: مدیریت متمرکز finally برای روشهای مختلف؛
catch برای روشهای مختلف به شرح زیر مدیریت میشود:
// مدیریت catch
protected DaoException doCatchException(Connection connexion, Throwable th, int code) {
// برگشت تراکنش
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e2) {
e2.printStackTrace();
}
//daoException
return new DaoException(code, th, simpleClassName);
}
- خط ۲: متد با نام [protected] اعلام شده است که به کلاسهای فرزند اجازه میدهد بدون اینکه عمومی باشد از آن استفاده کنند. این متد پارامترهای زیر را میپذیرد:
- [Connection connexion]: اتصال با SGBD – احتمالاً null;
- [Throwable th]: استثنایی که رخ داده است، که در نوع [DaoException] محصور خواهد شد؛
- [int code]: یک کد خطا که در صورتی استفاده میشود که متد یک [DaoException] جدید ایجاد کند؛
- خطوط ۴–۷: هدف اصلی این متد، برگشت تراکنش مرتبط با اتصال ارسالشده بهعنوان پارامتر ۱ است؛
- خطوط ۸–۱۰: اگر بازگشت تراکنش ناموفق باشد، ردیابی استثنا به کنسول نوشته میشود. کار دیگری قابل انجام نیست، زیرا در خط ۱۲ یک استثنا پرتاب خواهد شد؛
کد finally برای متدهای مختلف به شرح زیر مدیریت میشود:
// در نهایت رسیدگی
protected DaoException doFinally(ResultSet rs, PreparedStatement ps, Connection connexion, int code,
DaoException daoException) {
// پایان ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e1) {
}
}
// بستن [PreparedStatement]
if (ps != null) {
try {
ps.close();
} catch (SQLException e2) {
}
}
// خروج
if (connexion != null) {
try {
connexion.close();
} catch (SQLException e3) {
// در صورت امکان خطا را ثبت کنید
if (daoException == null) {
daoException = new DaoException(code, e3, simpleClassName);
}
}
}
// نتیجه
return daoException;
}
- خط ۲: این متد همچنین با نام [protected] تعریف شده است. این متد پارامترهای زیر را دریافت میکند:
- [ResultSet rs]: احتمالاً [ResultSet] اگر عملیات [SELECT] اجرا شده باشد – شاید null؛
- [PreparedStatement ps]: [PreparedStatement] که اجرا شده است – احتمالاً null;
- [Connection connexion]: اتصال با SGBD – احتمالاً null;
- [int code]: یک کد خطا که در صورتی استفاده میشود که متد یک [DaoException] جدید ایجاد کند؛
- [DaoException daoException]: [DaoException] بالقوه در صورتی که یک استثنا قبل از finally رخ داده باشد – احتمالاً null;
- خطوط 21–30: هدف اصلی این متد بستن اتصال است (خط 23);
- خطوط 24–29: اگر در حین این بستن، یک استثنا رخ دهد، آنگاه وضعیت پارامتر [DaoException daoException] را که به ما پاس شده است بررسی میکنیم: اگر [daoException == null] باشد، آنگاه یک [DaoException] جدید با کدی که به عنوان پارامتر پاس شده است ایجاد میکنیم؛
- خط ۳۲: یا [DaoException] قدیمی یا جدید به عنوان نتیجه بازگردانده میشود؛
ما همه متدهای کلاس [Dao] را ارائه نمیکنیم، بلکه فقط چندتایی از آنها را. همه آنها شبیه به هم هستند.
3.6.7.1. متد [getProduitById]
متد [getProduitById] محصولی را برمیگرداند که کلید اصلی آن برابر با پارامتر [id] باشد، و در غیر این صورت null را برمیگرداند؛
@Override
public Produit getProduitById(int id) {
//منابع اتصال
Connection connexion = null;
PreparedStatement ps = null;
ResultSet rs = null;
// در ابتدا هیچ استثنایی وجود ندارد
DaoException daoException = null;
// محصول جستوجو شده
Produit produit = null;
try {
//اتصال باز شد
connexion = dataSource.getConnection();
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت فقط خواندنی
connexion.setReadOnly(true);
// خواندن جدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_PRODUIT_BYID);
ps.setInt(1, id);
rs = ps.executeQuery();
if (rs.next()) {
produit = new Produit(id, rs.getString(1), rs.getInt(2), rs.getDouble(3), rs.getString(4));
}
// تأیید تراکنش
connexion.commit();
//بازگشت به حالت پیشفرض
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// استثنا را مدیریت کنید
daoException = doCatchException(connexion, e1, 112);
} finally {
// پردازش بلوک finally
daoException = doFinally(rs, ps, connexion, 113, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// نتیجه
return produit;
}
- خط ۱۰: محصولی که باید بازگردانده شود روی null تنظیم شده است؛
- خط ۱۹: ترتیب SQL [ConfigJdbc.V2_SELECT_PRODUIT_BYID] به شرح زیر است:
public final static String V2_SELECT_PRODUIT_BYID = "SELECT NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS WHERE ID=?";
- ردههای 22–24: اگر [ResultSet] یک خط داشته باشد، از آن برای ایجاد محصول برگشتی استفاده میشود؛ در غیر این صورت، محصول برگشتی همچنان null باقی میماند؛
- خط ۴۱: محصول بازگردانده میشود؛
- خط ۸: استثنای متد [DaoException] به null مقداردهی اولیه میشود؛
- خط ۳۱: متد [doCatchException] یک استثنا به نام [DaoException] را پرتاب میکند؛
- خط ۳۴: پارامتر [daoException] متد [doFinally] یا null, است یا استثنایی که توسط متد [doCatchException] پرتاب میشود. متد [doFinally]:
- اگر در بستن اتصال موفق باشد، این پارامتر را بدون تغییر باقی میگذارد؛
- این پارامتر را در صورتی که در بستن اتصال ناموفق باشد و قبلاً یک [DaoException] فراخوانی شده باشد، بدون تغییر باقی میگذارد؛
- یک [DaoException] جدید ایجاد میکند اگر در بستن اتصال شکست بخورد و قبلاً هیچ [DaoException] صادر نشده باشد؛
- خطوط ۳۷–۳۹: اگر استثنای محلی [daoException] با null برابر نباشد، آنگاه استثنا ایجاد میشود؛ در غیر این صورت، نتیجهٔ درخواستی بازگردانده میشود (خط ۴۱)؛
3.6.7.2. متد [deleteProduits]
متد [deleteProduits] محصولاتی را که کلیدهای اصلیشان بهعنوان پارامتر به آن ارسال شده است، حذف میکند. این متد تعداد محصولات حذفشده را برمیگرداند.
@Override
public int deleteProduits(int[] ids) {
//منابع اتصال
PreparedStatement ps = null;
Connection connexion = null;
// در ابتدا هیچ استثنایی وجود ندارد
DaoException daoException = null;
// تعداد محصولات بهروزرسانیشده
int nbProduits = 0;
try {
//اتصال باز شد
connexion = dataSource.getConnection();
// شروع تراکنش
connexion.setAutoCommit(false);
// در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// محصولات در حال حذف هستند
ps = connexion.prepareStatement(ConfigJdbc.V2_DELETE_PRODUITS);
for (int id : ids) {
// تنظیمات
ps.setInt(1, id);
//اجرای
nbProduits += ps.executeUpdate();
}
// تأیید تراکنش
connexion.commit();
//بازگشت به حالت پیشفرض
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// استثنا را مدیریت کنید
daoException = doCatchException(connexion, e1, 171);
} finally {
// پردازش بلوک finally
daoException = doFinally(null, ps, connexion, 172, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// نتیجه
return nbProduits;
}
- خط ۱۸، توالی SQL [ConfigJdbc.V2_DELETE_PRODUITS] به شرح زیر است:
public final static String V2_DELETE_PRODUITS = "DELETE FROM PRODUITS WHERE ID=?";
- خطوط ۱۸–۲۴: کد حذف محصول. میبینیم که دستور SQL یک بار (خط ۱۸) آماده شده و n بار (خطوط ۱۹–۲۴) اجرا میشود. این هدف شیء [PreparedStatement] است؛
- خط ۲۳: متد [PreparedStatement].executeUpdate() تعداد ردیفهای تحت تأثیر عملیات بهروزرسانی را برمیگرداند؛
- خط ۴۱: تعداد محصولات بهروزرسانیشده بازگردانده میشود؛
3.6.7.3. متد [updateProduits]
متد [updateProduits] محصولات ارسالشده بهعنوان پارامتر به آن را در پایگاه داده بهروزرسانی میکند. این متد تعداد محصولات بهروزرسانیشده را برمیگرداند.
@Override
public int updateProduits(List<Produit> produits) {
//منابع اتصال
PreparedStatement ps = null;
Connection connexion = null;
// در ابتدا هیچ استثنایی وجود ندارد
DaoException daoException = null;
//تعداد محصولات بهروزرسانیشده
int nbProduits = 0;
try {
// اتصال باز شد
connexion = dataSource.getConnection();
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// بهروزرسانی جدول [PRODUITS]
ps = connexion.prepareStatement(ConfigJdbc.V2_UPDATE_PRODUITS);
for (Produit produit : produits) {
//پارامترها
ps.setString(1, produit.getNom());
ps.setDouble(2, produit.getPrix());
ps.setInt(3, produit.getCategorie());
ps.setString(4, produit.getDescription());
ps.setInt(5, produit.getId());
//اجرا
nbProduits += ps.executeUpdate();
}
// تأیید تراکنش
connexion.commit();
//بازگشت به حالت پیشفرض
connexion.setAutoCommit(true);
} catch (SQLException e1) {
// رسیدگی به استثنا
daoException = doCatchException(connexion, e1, 131);
} finally {
// پردازش بلوک `finally`
daoException = doFinally(null, ps, connexion, 132, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// نتیجه
return nbProduits;
}
- خط ۱۸: توالی SQL [ConfigJdbc.V2_UPDATE_PRODUITS] به شرح زیر است:
public final static String V2_UPDATE_PRODUITS = "UPDATE PRODUITS SET NOM=?, PRIX=?, CATEGORIE=?, DESCRIPTION=? WHERE ID=?";
- خطوط ۱۹–۲۸: کد بهروزرسانی محصول؛
3.6.7.4. متد [addProduits]
متد [addProduits] محصولاتی را که بهعنوان پارامتر به آن ارسال شدهاند، در پایگاه داده درج میکند. این متد همان محصولات را همراه با کلیدهای اصلیشان بازمیگرداند (پیش از درج، محصولات کلید اصلی ندارند).
@Override
public List<Produit> addProduits(List<Produit> produits) {
//منابع اتصال
PreparedStatement ps = null;
Connection connexion = null;
// در ابتدا هیچ استثنایی وجود ندارد
DaoException daoException = null;
try {
//اتصال باز شد
connexion = dataSource.getConnection();
// در حالت خواندن/نوشتن
connexion.setReadOnly(false);
// شروع تراکنش
connexion.setAutoCommit(false);
// افزودن رکوردها به جدول [PRODUITS]
String generatedColumns[] = { ConfigJdbc.TAB_PRODUITS_ID };
ps = connexion.prepareStatement(ConfigJdbc.V2_INSERT_PRODUITS, generatedColumns);
for (Produit produit : produits) {
//پارامترها
ps.setString(1, produit.getNom());
ps.setLong(2, produit.getCategorie());
ps.setDouble(3, produit.getPrix());
ps.setString(4, produit.getDescription());
// اجرای دستور
ps.executeUpdate();
// کلید اصلی تولید شد
ResultSet generatedKeys = ps.getGeneratedKeys();
if (generatedKeys.next()) {
produit.setId(generatedKeys.getInt(1));
} else {
throw new RuntimeException(String.format("Le produit de nom [%s] n'a pas récupéré de clé primaire",
produit.getNom()));
}
}
// تأیید تراکنش
connexion.commit();
//بازگشت به حالت پیشفرض
connexion.setAutoCommit(true);
} catch (SQLException | RuntimeException e1) {
// پردازش استثنا
daoException = doCatchException(connexion, e1, 151);
} finally {
// پردازش بلوک finally
daoException = doFinally(null, ps, connexion, 152, daoException);
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// نتیجه
return produits;
}
- خط ۱۶: توالی SQL [ConfigJdbc.V2_INSERT_PRODUITS] به شرح زیر است:
public final static String V2_INSERT_PRODUITS = "INSERT INTO PRODUITS(NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?)";
در مثال بالا، دستور درج محصول شامل کلید اصلی [ID] نیست. از آنجایی که کلید اصلی در پایگاه داده، MySQL، دارای ویژگی [AUTOINCREMENT] است، SGBD سپس برای هر درج، یک کلید اصلی تولید خواهد کرد. این امر مشکل بازیابی این کلیدها را ایجاد میکند. این یک نکته مهم است زیرا عملیات بر روی محصولات از طریق کلیدهای اصلی آنها انجام میشود. بنابراین، دانستن این کلیدها ضروری است؛
- خطوط ۱۷–۳۳: حلقه درج محصول؛
- خط ۱۶: یک واریانت خاص از متد [prepareStatement]. پارامتر دوم، [generatedColumns]، آرایهای از نام ستونها است که میخواهیم پس از درج، مقادیر آن را بازیابی کنیم. در خط ۱۶ مشخص کردهایم که میخواهیم مقدار را از ستون [id] بازیابی کنیم. در اینجا باید توجه داشت که اگرچه نام ستونها در یک جدول نسبت به حروف بزرگ/کوچک بیتفاوت است، SGBD PostgreSQL این نام را باید به صورت حروف کوچک در نظر بگیرد. این معمولاً نوعی مشکلی است که هنگام انتقال کد از یک SGBD به SGBD دیگر با آن مواجه میشویم؛
- خط ۲۴: درج یک سطر در پایگاه داده؛
- خط ۲۶: بازیابی لیست مقادیر برای ستونهای مشخصشده در خط ۱۶ در یک [ResultSet]. در اینجا، برای ۱ درج، [ResultSet] دارای ۱ ردیف خواهد بود و این ردیف دارای یک ستون واحد حاوی کلید اصلی خواهد بود؛
- خط ۲۸: بازیابی کلید اصلی تولید شده توسط SGBD;
- خطوط ۲۹–۳۲: اگر کلید اصلی تولید شده به دست نیاید، یک [RuntimeException] راهاندازی میشود که در یک [DaoException] (خطوط ۳۸–۴۰) محصور خواهد شد؛
3.6.8. کلاس [Dao2]
![]() |
![]() |
کلاس [Dao2] گونهای از کلاس [Dao1] است که از نحوی به نام «try-with-resource(resource)» استفاده میکند:
- [resource] منبعی است که رابط [java.lang.AutoCloseable] را پیادهسازی میکند. تمام منابعی که با استفاده از متد [close] آزاد شدهاند، در این مورد لحاظ میشوند. این نحو تضمین میکند که در خط ۴، منبع [resource] بسته خواهد شد. این کار از نوشتن یک بند [finally] برای انجام این بستن جلوگیری میکند؛
بهعنوان مثال، متد [getAllProduits] از کلاس [Dao2] را در نظر بگیرید:
@Override
public List<Produit> getAllProduits() {
// استثناء احتمالی
DaoException daoException = null;
// فهرست محصولات
List<Produit> produits = new ArrayList<Produit>();
try (Connection connexion = dataSource.getConnection()) {
// شروع تراکنش
connexion.setAutoCommit(false);
//در حالت فقط خواندنی
connexion.setReadOnly(true);
// خواندن جدول [PRODUITS]
try (PreparedStatement ps = connexion.prepareStatement(ConfigJdbc.V2_SELECT_ALLPRODUITS)) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
produits.add(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
}
}
//پایان تراکنش
connexion.commit();
//بازگشت به حالت پیشفرض
connexion.setAutoCommit(true);
} catch (SQLException e1) {
//لغو تراکنش
daoException = doRollback(connexion, e1, 111);
}
} catch (SQLException e2) {
// پردازش استثنا
if (daoException == null) {
daoException = new DaoException(112, e2, simpleClassName);
}
}
// استثناء؟
if (daoException != null) {
throw daoException;
}
// نتیجه
return produits;
}
- خط ۷: بلوک `try` با منبع `[Connection]`. در خط ۲۷، میتوانیم مطمئن باشیم که این منبع بسته شده است؛
- خط ۱۳: بلوک `try` با منبع [PreparedStatement]. در خط ۲۳، میتوانیم مطمئن باشیم که این منبع بسته شده است؛
- خط ۱۴: بلوک `try` با منبع [ResultSet]. در خط ۱۹، میتوانیم مطمئن باشیم که این منبع بسته شده است؛
- خط ۲۵: تراکنش به شرح زیر برگشت داده میشود:
private DaoException doRollback(Connection connexion, Throwable e1, int code) {
try {
if (connexion != null) {
connexion.rollback();
}
} catch (SQLException e) {
e.printStackTrace();
}
// استثناء تولید شد
return new DaoException(code, e1, simpleClassName);
}
در نهایت، کد خواناتر است.
3.6.9. پیادهسازی لایه تست
3.6.9.1. کلاسهای تست
![]() |
![]() |
- آزمون [JUnitTestDao1] آزمونی برای JUnit از کلاس [Dao1] است؛
- آزمون [JUnitTestDao2] یک آزمون JUnit از کلاس [Dao2] است؛
- [AbstractJUnitTestDao] کلاس والد دو کلاس تست قبلی است؛
- [MainTestDao1] یک کلاس کنسول تست برای کلاس [Dao1] است؛
- [MainTestDao2] یک کلاس کنسول تست برای کلاس [Dao2] است؛
- [AbstractMainTestDao] کلاس والد دو کلاس قبلی است. این کلاس کدهای کلاسهای کنسول [IntroJdbc01, IntroJdbc02] را که قبلاً ارائه شدهاند، در خود گنجانده است، بنابراین ما این کلاسهای کنسول را بررسی نخواهیم کرد؛
کلاس [JUnitTestDao1] به شرح زیر است:
package spring.jdbc.tests;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestDao1 extends AbstractJUnitTestDao {
// لایه [DAO]
@Autowired
@Qualifier("dao1")
private IDao dao;
@Override
IDao getDao() {
return dao;
}
}
- توضیحات متنی در خطوط ۱۲–۱۳ در بخش ۲.۵.۵ مورد بحث قرار گرفتند. آنها به یک تست JUnit امکان میدهند تا به راحتی به کانکست Spring و بینهای آن دسترسی پیدا کند. این کانکست توسط کلاس [AppConfig] (خط ۱۲) پیکربندی میشود، کلاسی که در بخش ۲.۴.۳ مورد بحث قرار گرفت؛
- خط ۱۴: این کلاس از کلاس [AbstractJUnitTestDao] ارث میبرد، که اکنون به آن میپردازیم. متدهای تست JUnit در داخل همین کلاس قرار دارند؛
- خطوط 17–19: بیان (bean) با نام [dao1] (خط 18) تزریق میشود (خط 17). بنابراین، این یک نمونه از کلاس [Dao1] است که در اینجا تزریق میشود؛
- خطوط ۲۱–۲۴: متد [getDao] متد همنام در کلاس والد را بازنویسی میکند؛
در نهایت، هدف این کلاس این است که یک مرجع به لایه [DAO] که قرار است تست شود، در این مورد یک نمونه از [Dao1]، به کلاس والد بازگرداند. به همین ترتیب، کلاس [JUnitTestDao2] یک نمونه از کلاس [Dao2] را به کلاس والد [AbstractJUnitTestDao] بازمیگرداند.
کلاس [AbstractJUnitTestDao] یک کلاس تست برای JUnit است:
package spring.jdbc.tests;
import generic.jdbc.entities.dbproduits.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.BeansException;
import spring.jdbc.dao.IDao;
import spring.jdbc.infrastructure.DaoException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractJUnitTestDao {
// لایه [DAO]
abstract IDao getDao();
// نقشهبردار jSON
final static ObjectMapper jsonMapper = new ObjectMapper();
@Before
public void clean() {
// پایگاه داده قبل از هر آزمون پاک میشود
log("Vidage de la base de données", 1);
getDao().deleteAllProduits();
}
@Test
public void getProduits() throws JsonProcessingException {
...
}
@Test
public void getProduitBy() {
...
}
@Test
public void doInsertsInTransaction() {
...
}
@Test
public void updateProduits() {
...
}
@Test
public void deleteProduits() {
....
}
@Test
public void perf1() {
...
}
@Test
public void perf2() {
...
}
@Test
public void perf3() {
....
}
// -------------- متدهای خصوصی
...
}
- خط ۱۹: کلاس [AbstractJUnitTestDao] انتزاعی است؛
- خط ۲۲: متد انتزاعی [getDao] که مرجعی به لایه [DAO] برای تست شدن ارائه میدهد. این متد توسط کلاسهای فرزند پیادهسازی میشود؛
- خط ۲۵: یک مپر jSON که به ما امکان میدهد مقدار jSON را برای محصولات روی کنسول نمایش دهیم؛
- خطوط ۲۷–۳۲: قبل از هر آزمون (خط ۲۷)، جدول [PRODUITS] پاک میشود؛
3.6.9.2. متد خصوصی [fill]
متد خصوصی [fill] برای درج محصولات در جدول [PRODUITS] استفاده میشود.
private List<Produit> fill(int nbProduits) {
log("Remplissage de la base de données", 1);
// ایجاد یک لیست از محصولات
List<Produit> produits = new ArrayList<Produit>();
for (int i = 0; i < nbProduits; i++) {
int n = i + 1;
// int id, String name, int category, double price, String description
produits.add(new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format(
"DESC%s", n)));
}
// ما آن را در پایگاه داده ذخیره میکنیم – محصولات را با استفاده از کلید اصلیشان بازیابی میکنیم
produits = getDao().addProduits(produits);
// ایجاد یک فرهنگ لغت از محصولات برای بازیابی آسانتر آنها
//کلید دیکشنری کلید اصلی محصول در پایگاه داده است
for (Produit produit : produits) {
mapProduits.put(produit.getId(), produit);
}
// ما محصولات را بازمیگردانیم
return produits;
}
- خط ۱: متد [fill]، [nbProduits] را در جدول [PRODUITS] که فرض میشود خالی است، درج میکند؛
- خطوط ۳–۱۰: ایجاد فهرستی از محصولات به شکل:
new Produit(0, String.format("NOM%s", n), n / 5 + 1, 100 * (1 + (double) i / 100), String.format("DESC%s", n)));
که از سازنده Product (int id, String name, int category, double price, String description) استفاده میکند. مقدار اولین پارامتر، [id] (کلید اصلی جدول [PRODUITS]) بیربط است، زیرا متد [addProduits] در خط ۱۰ آن را در پایگاه داده درج نمیکند و به SGBD اجازه میدهد تا مقدار آن را تولید کند؛
- خط ۱۲: فهرست محصولات در پایگاه داده ذخیره میشود. به هر محصول در این فهرست یک کلید اصلی جدید، [id]، اختصاص داده میشود. متد [addProduits] پارامتر خود، [produits]، را بازمیگرداند. بنابراین میتوانستیم انتخاب کنیم که نتیجه را بازیابی نکنیم؛
- خطوط ۱۵–۱۷: محصولات در یک دیکشنری قرار میگیرند:
//فرهنگ محصول
private Map<Integer, Produit> mapProduits = new HashMap<Integer, Produit>();
کلید دیکشنری کلید اصلی محصول است و مقدار مرتبط، خود محصول است؛
- خط ۱۹: فهرست محصولات بازگردانده میشود؛
3.6.9.3. آزمون [getProduits]
این به شرح زیر است:
@Test
public void getProduits() throws JsonProcessingException {
// پر کردن
fill(10);
// فهرست محصولات
log("Liste des produits", 2);
List<Produit> produits = getDao().getAllProduits();
affiche(produits);
// ما بررسی میکنیم که لیست بازیابیشده و لیست ذخیرهشده یکسان هستند
for (Produit produit : produits) {
Produit found = mapProduits.get(produit.getId());
Assert.assertEquals(found, produit);
mapProduits.remove(found.getId());
}
// تمام محصولات اصلی باید از فرهنگ لغت حذف شده باشند
Assert.assertEquals(0, mapProduits.size());
}
}
- خط ۴: ۱۰ محصول به پایگاه داده اضافه میشود؛
- خط ۷: پس از انجام این کار، درخواست مشاهده همه محصولات در پایگاه داده را میدهیم؛
- خط ۸: آنها نمایش داده میشوند. هدف این است که تأیید کنیم محصولات واقعاً ذخیره شدهاند و دارای کلید اصلی هستند؛
- خطوط ۱۰–۱۳: ما بررسی میکنیم که محصولات بازیابیشده با محصولاتی که ذخیره کردیم یکسان هستند و میتوان آنها را در فرهنگ لغت [mapProduits] یافت؛
- خط ۱۱: ما از فرهنگ لغت، محصولی را با کلید اصلی یکسان با مورد بازگردانده شده از پایگاه داده بازیابی میکنیم. این نشان میدهد که به محصولات ذخیرهشده واقعاً یک کلید اصلی اختصاص داده شده است؛
- خط ۱۲: ما اطمینان حاصل میکنیم که این دو محصول یکسان هستند. به یاد داشته باشید که کلاس [Produit] متدی به نام [equals] را تعریف کرده است (به بخش ۳.۳.۴ مراجعه کنید)؛
- خط ۱۳: عنصر یافتشده از فرهنگ لغت حذف میشود؛
- خط 16: ما بررسی میکنیم که دیکشنری محصولات اولیه واقعاً خالی است، به این معنی که این محصولات اولیه همگی در لیست محصولات بازیابیشده از پایگاه داده وجود داشتهاند؛
متد [affiche] در خط ۸، متد خصوصی زیر است:
// نمایش لیست محصولات
private <T> void affiche(List<T> elements) throws JsonProcessingException {
for (T element : elements) {
System.out.println(jsonMapper.writeValueAsString(element));
}
}
- خط ۲: متد [affiche] یک متد عمومی است. این متد با یک نوع T پارامترگذاری شده است که از نظر نحوی با <T> نشان داده میشود. اگر با دو نوع، T1 و T2، پارامترگذاری میشد، به صورت <T1,T2> نوشته میشد. سینتکس یک متد m که با نوع T پارامتریزه شده است به صورت زیر است:
درون کد متد m، دادههایی از نوع T یافت میشود. متد m از یک نمونه c از کلاس C را میتوان به صورت زیر فراخوانی کرد:
که در آن T1 نوع واقعی است که جایگزین نوع فرمال T متد m میشود. در اکثر موارد، کامپایلر قادر است نوع T1 را از آرگومانهای متد m استنتاج کند. بنابراین، عبارت قبلی معمولاً به شکل زیر سادهتر میشود:
بیایید به متد [affiche] بازگردیم. این متد فهرستی از عناصر از نوع T را نمایش میدهد. این امر ممکن است زیرا مپر jSON که در خط ۴ استفاده شده است، قادر به رندر کردن نمایش jSON برای هر نوع ابجکت است. در این مثال خاص، تنها نوع T که استفاده میشود، نوع [Produit] خواهد بود.
روش [affiche] میتوانست به شکل زیر نیز نوشته شود:
// نمایش لیست محصولات
private void affiche(Object o) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(o));
}
از آنجایی که پارامتر واقعی یک لیست از محصولات است، در خط ۳ میتوانست نمایهسازی jSON این لیست نوشته شود. این با نوشتن نمایهسازی تکتک عناصر آن به صورت جداگانه یکسان نیست.
خروجی تولیدشده توسط تست [getProduits] به شرح زیر است:
3.6.9.4. آزمون [getProduitBy]
این به شرح زیر است:
@Test
public void getProduitBy() {
// پر کردن
fill(10);
log("getProduitBy", 1);
Produit produit = getDao().getProduitByName("NOM3");
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
- خط ۶: متد [getProduitByName] از رابط [IDao] برای بازیابی محصولی به نام [NOM3] استفاده میشود؛
- خط ۷: سپس از متد [getProduitById] در رابط [IDao] برای بازیابی همان محصول استفاده میشود، این بار با شناسه اصلی آن شناسایی شده است؛
- خطوط ۸–۱۰: یک بررسی انجام میشود تا اطمینان حاصل شود که [produit2] و [produit] دارای ویژگیهای یکسان هستند؛
3.6.9.5. آزمون [doInsertsInTransaction]
این به شرح زیر است:
@Test
public void doInsertsInTransaction() {
log("Ajout de deux produits de même nom", 1);
// واردسازی انجام میشود
List<Produit> inserts = new ArrayList<Produit>();
inserts.add(new Produit(0, "x", 1, 1.0, ""));
inserts.add(new Produit(0, "x", 1, 1.0, ""));
boolean erreur = false;
try {
getDao().addProduits(inserts);
} catch (DaoException daoException) {
erreur = true;
}
// بررسیها
Assert.assertTrue(erreur);
List<Produit> produits = getDao().getAllProduits();
Assert.assertEquals(0, produits.size());
}
- خطوط ۵–۷: فهرستی از دو محصول با نام یکسان، [x]، ایجاد میشود؛
- خط ۱۰: این دو محصول در جدول [PRODUITS] که خالی است درج میشوند (متد [clean] با [@Before] توضیح داده شده است). وارد کردن اول با موفقیت انجام میشود، اما مورد دوم نه، زیرا جدول [PRODUITS] یک محدودیت یکتا (unique constraint) بر روی نام محصولات دارد. بنابراین، یک خطا (exception) باید رخ دهد. این مورد در خط ۱۵ آزمایش میشود؛
- از آنجایی که تمام متدهای رابط [IDao] در داخل یک تراکنش اجرا میشوند، شکست دومین درج باعث میشود که کل تراکنش—و در نتیجه اولین درج—لغو شود. در نهایت، هیچ درجدادهای در جدول [PRODUITS] نباید انجام شود؛
- خطوط ۱۶–۱۷: ما این را با بازیابی فهرست محصولات موجود در جدول [PRODUITS] و بررسی اینکه این فهرست خالی است، تأیید میکنیم؛
3.6.9.6. آزمون [updateProduits]
این به شرح زیر است:
@Test
public void updateProduits() {
// پر کردن
fill(10);
log("Mise à jour du prix des produits de catégorie 1", 1);
// بازیابی محصولات
List<Produit> produits = getDao().getAllProduits();
// بهروزرسانی آنهایی که در دستهٔ ۱ هستند
List<Produit> updated = new ArrayList<Produit>();
int nbUpdated = 0;
for (Produit produit : produits) {
if (produit.getCategorie() == 1) {
// شناسه int، نام String، دسته int، قیمت double، توضیحات String
updated
.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
nbUpdated++;
}
}
int nbProduits = getDao().updateProduits(updated);
// چکها
// Assert.assertEquals(nbUpdated, nbProduits); -- با DB2 کار نمیکند
for (Produit produit : updated) {
Produit produit2 = getDao().getProduitById(produit.getId());
Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
}
}
- خط ۴: ۱۰ محصول به پایگاه داده اضافه میشوند؛
- خط ۷: بازیابی آنها؛
- ردههای ۹ تا ۱۸: قیمت محصولات در دستهٔ ۱ به میزان ۱۰ درصد افزایش مییابد؛
- خط ۱۹: این تغییرات در پایگاه داده ذخیره میشوند؛
- خطوط 22–25: لیست محصولات مورد استفاده برای بهروزرسانی در حافظه مرور میشود. برای هر یک، محصول با کلید اصلی مشابه از پایگاه داده بازیابی شده و بررسی میشود تا اطمینان حاصل شود که بهروزرسانی قیمت انجام شده است؛
- خط ۱۹: تعداد محصولاتی که توسط عملیات [updateProduits] بهروزرسانی شدهاند، بازیابی میشود؛
- خط ۲۱: ما بررسی میکنیم که این عدد واقعاً عدد مورد انتظار است. این تست برای تمام عملیات SGBD به جز SGBD و DB2 برقرار است. بنابراین ما آنها را غیرفعال کردهایم؛
3.6.9.7. آزمون [deleteProduits]
این به شرح زیر است:
@Test
public void deleteProduits() {
//پر کردن
fill(10);
log("deleteProduits", 1);
// فهرست محصولات
List<Produit> produits = getDao().getAllProduits();
// حذف دو محصول
Produit produit0 = produits.get(0);
Produit produit5 = produits.get(5);
int nbDeleted = getDao().deleteProduits(new int[] { produit0.getId(), produit5.getId() });
// بررسیها
// Assert.assertEquals(2, nbDeleted); -- با DB2 کار نمیکند
Assert.assertNull(getDao().getProduitById(produit0.getId()));
Assert.assertNull(getDao().getProduitById(produit5.getId()));
Assert.assertEquals(produits.size() - 2, getDao().getAllProduits().size());
}
- خط ۴: ما ۱۰ محصول را به پایگاه داده اضافه میکنیم؛
- خطوط ۷–۱۱: تمام آیتمها را از پایگاه داده بازیابی کرده و آیتمهای بازیابیشده در موقعیتهای ۰ و ۵ را حذف میکنیم؛
- خطوط 14–16: بررسی میکنیم که این دو محصول دیگر در پایگاه داده وجود ندارند و پایگاه داده اکنون دو محصول کمتر دارد؛
- آزمون در خط ۱۳ با SGBD و DB2 شکست میخورد. این آزمون با SGBD دیگر با موفقیت اجرا میشود؛
3.6.9.8. آزمونهای عملکرد
ما سه روش را در آزمایشها گنجاندهایم که تنها هدف آنها ارزیابی عملکرد SGBD است:
@Test
public void perf1() {
// پر کردن
fill(10000);
}
@Test
public void perf2() {
// پر کردن
fill(10000);
// اصلاحیه
List<Produit> produits = getDao().getAllProduits();
// آنها را در دستهٔ ۱ بهروزرسانی کنید
List<Produit> updated = new ArrayList<Produit>();
for (Produit produit : produits) {
// int id, String name, int category, double price, String description
updated.add(new Produit(produit.getId(), produit.getNom(), 1, produit.getPrix() * 1.1, produit.getDescription()));
}
getDao().updateProduits(updated);
}
@Test
public void perf3() {
// پر کردن
fill(10000);
// حذف
List<Produit> produits = getDao().getAllProduits();
// کلیدهای اصلی
int[] keys = new int[produits.size()];
for (int i = 0; i < keys.length; i++) {
keys[i] = produits.get(i).getId();
}
getDao().deleteProduits(keys);
}
- ردههای ۱–۵: درج ۱۰٬۰۰۰ محصول؛
- خطوط ۸–۲۰: درج ۱۰٬۰۰۰ محصول، و سپس اصلاح آنها از طریق کلیدهای اصلیشان؛
- ردههای ۲۳–۳۴: درج ۱۰٬۰۰۰ محصول، و سپس حذف آنها از طریق کلیدهای اصلیشان؛
برای اجرای تستهای [JUnitTestDao1] و [JUnitTestDao2]، میتوان از پیکربندیهای اجرای زیر استفاده کرد:
![]() | ![]() |
نتایج تست [JUnitTestDao1] به شرح زیر است:
![]() |
در [1]، نتایج همان نتایج [JUnitTestDao1] است، و در [2]، همان نتایج [JUnitTestDao2] است. تفاوت قابل توجهی بین آنها وجود ندارد. در [1]:
- آزمایش موفقیتآمیز بود؛
- وارد کردن ۱۰٬۰۰۰ محصول ۳٫۱۵ ثانیه طول میکشد؛
- وارد کردن 10,000 محصول و سپس اصلاح آنها 4.80 ثانیه طول میکشد؛
- وارد کردن 10,000 محصول و سپس حذف آنها 4.40 ثانیه طول میکشد؛
- بنابراین، پرزحمتترین عملیات، درج است؛



















































