إدارة المعاملات في الربيع هي واحدة من أكثر الميزات استخدامًا وأهمية في إطار الربيع. إدارة المعاملات هي مهمة تافهة في أي تطبيق مؤسسي. لقد تعلمنا بالفعل كيفية استخدام واجهة برمجة تطبيقات قاعدة البيانات JDBC لإدارة المعاملات. يوفر الربيع دعمًا شاملاً لإدارة المعاملات ويساعد المطورين على التركيز أكثر على منطق الأعمال بدلاً من القلق حول سلامة البيانات في حالة فشل النظام.
إدارة المعاملات في الربيع
بعض فوائد استخدام إدارة المعاملات في الربيع تشمل:
- الدعم لإدارة المعاملات التصريحية. في هذا النموذج، يستخدم الربيع البرمجة الجانبية عبر الطرق التي تتعامل مع المعاملات لضمان سلامة البيانات. هذا هو النهج المفضل ويعمل في معظم الحالات.
- دعم لمعظم واجهات برمجة التطبيقات للمعاملات مثل JDBC، Hibernate، JPA، JDO، JTA الخ. كل ما نحتاج إليه هو استخدام فئة تنفيذ مدير المعاملة المناسبة. على سبيل المثال
org.springframework.jdbc.datasource.DriverManagerDataSource
لإدارة المعاملات JDBC وorg.springframework.orm.hibernate3.HibernateTransactionManager
إذا كنا نستخدم Hibernate كأداة ORM. - دعم لإدارة المعاملات برمجيًا من خلال استخدام
TransactionTemplate
أو تنفيذ مدير المعاملةPlatformTransactionManager
.
معظم الميزات التي نرغب فيها في مدير المعاملات مدعومة بواسطة إدارة المعاملات الواضحة، لذا سنستخدم هذا النهج لمشروعنا التوضيحي.
مثال على إدارة المعاملات في Spring JDBC
سنقوم بإنشاء مشروع بسيط لـ Spring JDBC حيث سنقوم بتحديث عدة جداول في عملية واحدة. يجب أن تؤكد العملية فقط عندما تُنفذ جميع عبارات JDBC بنجاح ، وإلا يجب أن تتراجع لتجنب عدم اتساق البيانات. إذا كنت تعرف إدارة معاملات JDBC ، فقد تجادل بأنه يمكننا القيام بذلك بسهولة من خلال تعيين auto-commit على false للاتصال وبناءً على نتيجة جميع البيانات ، إما تأكيد أو إلغاء عملية النقل. من الواضح أننا يمكننا القيام بذلك ، ولكن ذلك سيؤدي إلى الكثير من الشفرة المستهلكة لإدارة المعاملات. أيضًا ، ستكون نفس الشفرة موجودة في جميع الأماكن التي نبحث فيها عن إدارة المعاملات ، مما يسبب تقارنًا بالشفرة وشفرة غير قابلة للصيانة. تتناول إدارة المعاملات الإعلانية في Spring هذه القضايا عن طريق استخدام برمجة موجهة للجوانب لتحقيق فصل فصل فصل فصل فصل الشفرة في تطبيقنا. دعنا نرى كيف تفعل Spring ذلك بمثال بسيط. قبل أن نقفز إلى مشروعنا في Spring ، دعنا نقوم ببعض إعدادات قاعدة البيانات لاستخدامنا.
إدارة المعاملات في Spring – إعدادات قاعدة البيانات
سنقوم بإنشاء جدولين لاستخدامنا وتحديث كل منهما في عملية واحدة.
CREATE TABLE `Customer` (
`id` int(11) unsigned NOT NULL,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `Address` (
`id` int(11) unsigned NOT NULL,
`address` varchar(20) DEFAULT NULL,
`country` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
يمكننا تحديد علاقة المفتاح الأجنبي هنا من عمود معرف العنوان إلى عمود معرف العميل، ولكن للبساطة ليس لدي أي قيد محدد هنا. إعداد قاعدة البيانات لدينا جاهز لمشروع إدارة المعاملات الربيع، دعونا ننشئ مشروعًا بسيطًا للربيع في Spring Tool Suite. ستبدو هيكلة مشروعنا النهائي كما في الصورة أدناه. لنلقي نظرة على كل قطعة واحدة تلو الأخرى، معًا ستوفر مثالًا بسيطًا عن إدارة المعاملات الربيع مع JDBC. لنلقي نظرة على كل قطعة واحدة تلو الأخرى، معًا ستوفر مثالًا بسيطًا عن إدارة المعاملات الربيع مع JDBC.
إدارة المعاملات في الربيع – تبعيات ميفن
بما أننا نستخدم واجهة برمجة تطبيقات JDBC، سيتعين علينا تضمين تبعية spring-jdbc في تطبيقنا. سنحتاج أيضًا إلى سائق قاعدة بيانات MySQL للاتصال بقاعدة بيانات MySQL، لذلك سنضمن أيضًا تبعية mysql-connector-java. توفر تبعية spring-tx تبعيات إدارة المعاملات، وعادةً ما تتم إضافتها تلقائيًا بواسطة STS ولكن إذا لم يحدث ذلك، فعليك أن تضمنها أيضًا. قد ترى بعض التبعيات الأخرى لتسجيل السجلات واختبار الوحدات، ومع ذلك، لن نستخدم أيًا منها. يبدو ملف pom.xml النهائي لدينا كما في الكود أدناه.
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId>
<artifactId>SpringJDBCTransactionManagement</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.7</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring -->
<spring-framework.version>4.0.2.RELEASE</spring-framework.version>
<!-- Logging -->
<logback.version>1.0.13</logback.version>
<slf4j.version>1.7.5</slf4j.version>
<!-- Test -->
<junit.version>4.11</junit.version>
</properties>
<dependencies>
<!-- Spring and Transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<!-- Spring JDBC and MySQL Driver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring-framework.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.5</version>
</dependency>
<!-- Logging with SLF4J & LogBack -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Test Artifacts -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring-framework.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
I have updated the Spring versions to the latest one as of today. Make sure MySQL database driver is compatible with your mysql installation.
إدارة المعاملات في الربيع – فصول النموذج
سنقوم بإنشاء فصلين في جافا، العميل والعنوان اللذين سيتم تعيينهما إلى جداولنا.
package com.journaldev.spring.jdbc.model;
public class Address {
private int id;
private String address;
private String country;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
}
package com.journaldev.spring.jdbc.model;
public class Customer {
private int id;
private String name;
private Address address;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
لاحظ أن فصل العميل لديه العنوان كواحد من متغيراته. عندما نقوم بتنفيذ DAO للعميل، سنحصل على البيانات لكل من جدول العميل والعنوان وسنقوم بتنفيذ استعلامين إدراج منفصلين لهذه الجداول ولهذا السبب نحتاج إلى إدارة المعاملات لتجنب عدم اتساق البيانات.
إدارة المعاملات في الربيع – تنفيذ DAO
لنقم بتنفيذ DAO لفصل العميل، للبساطة سنكون لدينا طريقة واحدة فقط لإدراج سجل في كل من جداول العميل والعنوان.
package com.journaldev.spring.jdbc.dao;
import com.journaldev.spring.jdbc.model.Customer;
public interface CustomerDAO {
public void create(Customer customer);
}
package com.journaldev.spring.jdbc.dao;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import com.journaldev.spring.jdbc.model.Customer;
public class CustomerDAOImpl implements CustomerDAO {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void create(Customer customer) {
String queryCustomer = "insert into Customer (id, name) values (?,?)";
String queryAddress = "insert into Address (id, address,country) values (?,?,?)";
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.update(queryCustomer, new Object[] { customer.getId(),
customer.getName() });
System.out.println("Inserted into Customer Table Successfully");
jdbcTemplate.update(queryAddress, new Object[] { customer.getId(),
customer.getAddress().getAddress(),
customer.getAddress().getCountry() });
System.out.println("Inserted into Address Table Successfully");
}
}
لاحظ أن تنفيذ CustomerDAO لا يهتم بإدارة المعاملات. بهذه الطريقة نحقق فصل الاهتمام لأنه في بعض الأحيان نحصل على تنفيذات DAO من أطراف ثالثة ولا نملك السيطرة على هذه الفئات.
إدارة المعاملات التصريحية في الربيع – الخدمة
لنقم بإنشاء خدمة العملاء التي ستستخدم تنفيذ CustomerDAO وتوفر إدارة المعاملات عند إدراج السجلات في جداول العملاء والعناوين في طريقة واحدة.
package com.journaldev.spring.jdbc.service;
import com.journaldev.spring.jdbc.model.Customer;
public interface CustomerManager {
public void createCustomer(Customer cust);
}
package com.journaldev.spring.jdbc.service;
import org.springframework.transaction.annotation.Transactional;
import com.journaldev.spring.jdbc.dao.CustomerDAO;
import com.journaldev.spring.jdbc.model.Customer;
public class CustomerManagerImpl implements CustomerManager {
private CustomerDAO customerDAO;
public void setCustomerDAO(CustomerDAO customerDAO) {
this.customerDAO = customerDAO;
}
@Override
@Transactional
public void createCustomer(Customer cust) {
customerDAO.create(cust);
}
}
إذا لاحظت تنفيذ CustomerManager، فهو يستخدم فقط تنفيذ CustomerDAO لإنشاء العميل ولكن يوفر إدارة المعاملات التصريحية من خلال توسيم طريقة createCustomer() بالتوسيم @Transactional
. هذا كل ما نحتاج إلى فعله في كودنا للحصول على فوائد إدارة المعاملات في الربيع. @Transactional يمكن تطبيق التوسيم على الطرق بالإضافة إلى الفئة بأكملها. إذا كنت ترغب في أن تكون لديك كافة الطرق مزودة بميزات إدارة المعاملات، يجب أن توسم فئتك بهذا التوسيم. اقرأ المزيد حول التوسيمات في دليل توسيمات جافا. الجزء الوحيد المتبقي هو ربط فول الربيع للحصول على مثال عملي على إدارة المعاملات في الربيع.
إدارة المعاملات في الربيع – تكوين الفول
أنشئ ملف تكوين الفاصل الربيعي بالاسم “spring.xml”. سنستخدم هذا في برنامج الاختبار الخاص بنا لربط فول الربيع وتنفيذ برنامج JDBC الخاص بنا لاختبار إدارة المعاملات.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:context="https://www.springframework.org/schema/context"
xmlns:tx="https://www.springframework.org/schema/tx"
xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-4.0.xsd
https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- Enable Annotation based Declarative Transaction Management -->
<tx:annotation-driven proxy-target-class="true"
transaction-manager="transactionManager" />
<!-- Creating TransactionManager Bean, since JDBC we are creating of type
DataSourceTransactionManager -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- MySQL DB DataSource -->
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/TestDB" />
<property name="username" value="pankaj" />
<property name="password" value="pankaj123" />
</bean>
<bean id="customerDAO" class="com.journaldev.spring.jdbc.dao.CustomerDAOImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="customerManager" class="com.journaldev.spring.jdbc.service.CustomerManagerImpl">
<property name="customerDAO" ref="customerDAO"></property>
</bean>
</beans>
نقاط مهمة يجب ملاحظتها في ملف تكوين فول الربيع هي:
- تستخدم العنصر tx:annotation-driven لإخبار سياق الربيع بأننا نستخدم تكوين إدارة المعاملات القائم على التعليقات. يستخدم السمة transaction-manager لتوفير اسم فول إدارة المعاملات. القيمة الافتراضية لـ transaction-manager هي transactionManager ولكنني ما زلت أستخدمها لتجنب الارتباك. تستخدم السمة proxy-target-class لإخبار سياق الربيع باستخدام الوكلاء القائمة على الفئة، وبدونها ستحصل على استثناء تشغيل مع رسالة مثل Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.journaldev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
-
نظرًا لأننا نستخدم JDBC ، فإننا ننشئ فول إدارة المعاملات من النوع
org.springframework.jdbc.datasource.DataSourceTransactionManager
. هذا أمر مهم جدًا ويجب علينا استخدام فئة تنفيذ فول إدارة المعاملات المناسبة بناءً على استخدام واجهة برمجة التطبيقات للمعاملات الخاصة بنا. - يتم استخدام فول dataSource لإنشاء كائن DataSource ويتعين علينا توفير خصائص تكوين قاعدة البيانات مثل driverClassName ، url ، اسم المستخدم وكلمة المرور. قم بتغيير هذه القيم استنادًا إلى الإعدادات المحلية الخاصة بك.
- نحن نقوم بحقن dataSource في تعريف حبة customerDAO. بنفس الطريقة، نقوم بحقن حبة customerDAO في تعريف حبة customerManager.
إعدادنا جاهز، دعونا نقوم بإنشاء صنف اختبار بسيط لاختبار تنفيذ إدارة العمليات.
package com.journaldev.spring.jdbc.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.journaldev.spring.jdbc.model.Address;
import com.journaldev.spring.jdbc.model.Customer;
import com.journaldev.spring.jdbc.service.CustomerManager;
import com.journaldev.spring.jdbc.service.CustomerManagerImpl;
public class TransactionManagerMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
"spring.xml");
CustomerManager customerManager = ctx.getBean("customerManager",
CustomerManagerImpl.class);
Customer cust = createDummyCustomer();
customerManager.createCustomer(cust);
ctx.close();
}
private static Customer createDummyCustomer() {
Customer customer = new Customer();
customer.setId(2);
customer.setName("Pankaj");
Address address = new Address();
address.setId(2);
address.setCountry("India");
// تعيين قيمة تزيد عن 20 حرفًا، حتى يحدث استثناء SQLException
address.setAddress("Albany Dr, San Jose, CA 95129");
customer.setAddress(address);
return customer;
}
}
لاحظ أنني أقوم بتعيين قيمة عمود العنوان بشكل صريح بحيث نحصل على استثناء أثناء إدراج البيانات في جدول العناوين. الآن عند تشغيل برنامج الاختبار الخاص بنا، نحصل على الناتج التالي.
Mar 29, 2014 7:59:32 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3fa99295: startup date [Sat Mar 29 19:59:32 PDT 2014]; root of context hierarchy
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName
INFO: Loaded JDBC driver: com.mysql.jdbc.Driver
Inserted into Customer Table Successfully
Mar 29, 2014 7:59:32 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [org/springframework/jdbc/support/sql-error-codes.xml]
Mar 29, 2014 7:59:32 PM org.springframework.jdbc.support.SQLErrorCodesFactory <init>
INFO: SQLErrorCodes loaded: [DB2, Derby, H2, HSQL, Informix, MS-SQL, MySQL, Oracle, PostgreSQL, Sybase]
Exception in thread "main" org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [insert into Address (id, address,country) values (?,?,?)]; Data truncation: Data too long for column 'address' at row 1; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:100)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:907)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:968)
at org.springframework.jdbc.core.JdbcTemplate.update(JdbcTemplate.java:978)
at com.journaldev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$FastClassBySpringCGLIB$$84f71441.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:711)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
at com.journaldev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
at com.journaldev.spring.jdbc.main.TransactionManagerMain.main(TransactionManagerMain.java:20)
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Data too long for column 'address' at row 1
at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:2939)
at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1623)
at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:1715)
at com.mysql.jdbc.Connection.execSQL(Connection.java:3249)
at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1268)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1541)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1455)
at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1440)
at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:914)
at org.springframework.jdbc.core.JdbcTemplate$2.doInPreparedStatement(JdbcTemplate.java:907)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:642)
... 16 more
يرجى ملاحظة أن رسالة السجل تشير إلى أن البيانات تم إدراجها بنجاح في جدول العملاء، لكن الاستثناء الذي أثير بوضوح من قبل سائق قاعدة بيانات MySQL يشير إلى أن القيمة طويلة جدًا لعمود العنوان. الآن إذا قمت بفحص جدول العملاء، فلن تجد أي صف هناك، مما يعني أن العملية تم إلغاؤها بالكامل. إذا كنت تتساءل أين يحدث سحر إدارة العمليات، فانظر إلى السجلات بعناية ولاحظ فصول AOP والبروكسي التي أنشأتها إطار العمل Spring. إطار العمل Spring يستخدم نصائح Around لإنشاء فئة وسيطة لـ CustomerManagerImpl والتزام العملية فقط إذا عادت الطريقة بنجاح. في حالة وجود أي استثناء، فإنه يلغي العملية بأكملها. أوصيك بقراءة مثال على AOP في Spring للمزيد من المعلومات حول نموذج البرمجة الموجه نحو الجوانب. هذا كل شيء بخصوص مثال إدارة المعاملات في Spring، قم بتنزيل المشروع العيني من الرابط أدناه وتجربته للمزيد من المعرفة.
Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example