ניהול העסקאות בספרינג הוא אחד התכונות הנפוצות והחשובות ביותר של מסגרת ה-Spring. ניהול העסקאות הוא משימה טריביאלית בכל יישום עסקי. כבר למדנו כיצד להשתמש ב-ממשק ה- JDBC לניהול העסקאות. Spring מספקת תמיכה נרחבת בניהול העסקאות ועוזרת למפתחים להתמקד יותר בלוגיקת העסקית במקום לדאוג לאינטגרציה של הנתונים במקרה של כשל במערכת.
ניהול העסקאות בספרינג
כמה מהיתרונות של שימוש בניהול העסקאות של Spring הם:
- תמיכה בניהול העסקאות דקלרטיבי. במודל זה, Spring משתמשת ב-AOP מעל השיטות העסקאותיות כדי לספק אינטגרציה של הנתונים. זהו הגישה המועדפת והפועלת ברוב המקרים.
- תמיכה ברוב ממשקי ה- API לעסקאות כמו JDBC, Hibernate, JPA, JDO, JTA וכו '. כל מה שאנחנו צריכים לעשות הוא להשתמש במחלקת המימוש המתאימה למנהל העסקאות הרצוי. לדוגמה
org.springframework.jdbc.datasource.DriverManagerDataSource
עבור ניהול עסקאות JDBC ו־org.springframework.orm.hibernate3.HibernateTransactionManager
אם אנחנו משתמשים ב- Hibernate ככלי ORM. - תמיכה בניהול עסקאות תכנתי באמצעות שימוש ב־
TransactionTemplate
או במימוש שלPlatformTransactionManager
.
רוב התכונות שאנחנו רוצים במנהל עסקאות נתמכות על ידי ניהול עסקאות הצהות, ולכן נשתמש בגישה זו לדוגמת הפרויקט שלנו.
דוגמה לניהול עסקאות JDBC ב-Spring
אנו ניצור פרויקט פשוט ב-Spring JDBC, בו נעדכן טבלאות מרובות בעסקה אחת. העסקה צריכה להתחייב רק כאשר כל ההצהרות של JDBC מבוצעות בהצלחה, אחרת עליה לבטל את השינויים כדי למנוע אי עסקיות בנתונים. אם אתה מכיר את ניהול העסקאות ב-JDBC, יתכן שתטען שניתן לעשות זאת בקלות על ידי הגדרת auto-commit לשקרות לחיבור ובהתאם לתוצאה של כל ההצהרות, לבצע commit או rollback לעסקה. ברור שאנו יכולים לעשות זאת, אך זה יביא להרבה קוד קרסול רק עבור ניהול העסקאות. גם קוד זה יהיה נמצא בכל המקומות שבהם אנו מחפשים ניהול עסקיות, מייצר קוד מצומצם ושלא ניתן לתחזק. ניהול העסקאות הצהרתי של 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, בואו ניצור פרויקט Maven פשוט ב-Spring Tool Suite. מבנה הפרויקט הסופי שלנו ייראה כמו בתמונה למטה. בואו נתרגל כל אחת מהרכיבים לחלקים, יחד הם יספקו לנו דוגמה פשוטה לניהול עסקאות Spring עם JDBC.
ניהול עסקאות Spring – תלות Maven
מכיוון שאנו משתמשים ב- JDBC API, עלינו לכלול את תלות 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.
ניהול עסקאות באמצעות Spring – מחלקות מודל
ניצור שני Java Beans, Customer ו-Address שיתאימו לטבלאות שלנו.
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;
}
}
שימו לב שב- Customer יש את Address כאחד מהמשתנים שלו. כאשר נממש DAO עבור Customer, נקבל נתונים לשני הטבלאות – ללקוח ולכתובת, ונבצע שני שאילתות insert נפרדות עבור הטבלאות אלו, ולכן אנו זקוקים לניהול עסקאות כדי למנוע אי עקביות בנתונים.
ניהול עסקאות באמצעות Spring – יישום DAO
בואו נממש את DAO עבור Customer Bean, לפשטות נהיה לנו רק שיטה אחת להכניס רשומה בשני הטבלאות – ללקוח ולכתובת.
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 כדי ליצור את הלקוח אך סופק ניהול עסקאות באמצעות הערות בקוד @Transactional
על השיטה createCustomer(). זהו כל מה שצריך לעשות בקוד שלנו כדי לקבל את היתרונות של ניהול עסקאות של Spring. @Transactional ניתן ליישם על שיטות כמו גם על כל המחלקה. אם ברצונך שכל השיטות שלך יכילו יכולות ניהול עסקאות, עליך להסמין את המחלקה שלך באנוטציה זו. קרא עוד על ההערות ב-מדריך ההערות ב-Java. החלק היחיד שנשאר הוא לחבר את יצירת הקצרנים של Spring כדי להפעיל את דוגמת ניהול העסקאות של Spring.
ניהול עסקאות של Spring – הגדרת Bean
צור קובץ תצורת Bean שםו "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>
נקודות חשובות לשים לב אליהן בקובץ התצורה של Bean של הקפיצות הם:
- האלמנט tx:annotation-driven משמש להודיע לקשת Spring שאנו משתמשים בהגדרת ניהול העסקאות המבוססת על אנוטציות. המאפיין transaction-manager משמש לספק את שם בון מנהל העסקאות. ערך ברירת המחדל של transaction-manager הוא transactionManager אך אני עדיין מציין אותו כדי למנוע בלבול. המאפיין proxy-target-class משמש להודיע לקשת Spring להשתמש בפרוקסיות מבוססות קלאס, בלעדיו יתקבלו חריגות זמן ריצה עם הודעה כגון חריגה בתהליך "ראשי" org.springframework.beans.factory.BeanNotOfRequiredTypeException: בון בשם 'customerManager' חייב להיות מסוג [com.journaldev.spring.jdbc.service.CustomerManagerImpl], אך היה למעשה מסוג [com.sun.proxy.$Proxy6]
- מאחר ואנו משתמשים ב-JDBC, אנו יוצרים את בון transactionManager מסוג
org.springframework.jdbc.datasource.DataSourceTransactionManager
. זה מאוד חשוב ועלינו להשתמש בממשק בון מנהל העסקאות המתאים בהתבסס על שימוש ב-API של העסקאות שלנו. - בון dataSource משמש ליצירת אובייקט DataSource ואנו חייבים לספק את המאפיינים התצורתיים של מסד הנתונים כגון driverClassName, url, username ו-password. שנה את הערכים האלה בהתאם להגדרות המקומיות שלך.
- אנחנו מכניסים את ה-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. הספרייה משתמשת בטכניקת Around Advice ליצירת מחלקת פרוקסי עבור CustomerManagerImpl ורק במידה והשיטה מחזירה בהצלחה, היא מבצעת את העסקה. אם יש חריגה כלשהי, היא פשוטה מסתירה את כל העסקה. אני ממליץ לך לקרוא את דוגמת Spring AOP כדי ללמוד עוד על מודל תכנות פונקציונלי. זהו כל הסיפור לדוגמת ניהול עסקאות ב-Spring. ניתן להוריד את הפרויקט הדוגמא מהקישור למטה ולנסות ולשחק עם זה כדי ללמוד עוד.
Source:
https://www.digitalocean.com/community/tutorials/spring-transaction-management-jdbc-example