春のトランザクション管理例 JDBC

Springフレームワークの中でも、Spring Transaction Managementは最も広く利用されている重要な機能の一つです。トランザクション管理は企業アプリケーションにおいて重要なタスクです。私たちは既にJDBC APIを使用してトランザクション管理の方法を学んでいますが、Springはトランザクション管理に幅広いサポートを提供し、システムの障害が起きた場合でもデータの整合性を心配せずに、開発者がビジネスロジックにより時間を割くことを支援します。

春のトランザクション管理

Spring Transaction Managementの利点のいくつかは次のとおりです:

    1. デクララティブトランザクション管理のサポート。このモデルでは、Springはトランザクションメソッド上にAOPを使用してデータの整合性を提供します。これは推奨されるアプローチであり、ほとんどの場合で動作します。

 

    1. JDBC、Hibernate、JPA、JDO、JTAなどのほとんどのトランザクションAPIのサポート。適切なトランザクションマネージャの実装クラスを使用するだけです。たとえば、JDBCトランザクション管理にはorg.springframework.jdbc.datasource.DriverManagerDataSourceを使用し、HibernateをORMツールとして使用している場合はorg.springframework.orm.hibernate3.HibernateTransactionManagerを使用します。

 

    TransactionTemplateやPlatformTransactionManagerの実装を使用したプログラムによるトランザクション管理のサポート。

ほとんどの機能は、宣言的なトランザクション管理でサポートされているため、私たちはこのアプローチを私たちの例のプロジェクトで使用します。

春のトランザクション管理の JDBC の例

単一のトランザクションで複数のテーブルを更新する簡単なSpring JDBCプロジェクトを作成します。トランザクションは、すべてのJDBCステートメントが正常に実行された場合にのみコミットされ、データの整合性を保つためにロールバックする必要があります。もしJDBCトランザクション管理に詳しい場合、接続に対して自動コミットをfalseに設定して、すべてのステートメントの結果に基づいてトランザクションをコミットまたはロールバックすることが簡単にできるかもしれません。もちろんそれはできますが、トランザクション管理のための大量のひな形のコードが必要になります。また、トランザクション管理が必要なすべての場所で同じコードが現れるため、緊密に結合されたメンテナンス性の低いコードになります。Springの宣言的トランザクション管理は、アスペクト指向プログラミングを使用して緩い結合を実現し、アプリケーション内のひな形のコードを回避することで、これらの懸念に対処します。では、Springがどのようにして簡単な例で実現しているか見てみましょう。Springプロジェクトに取り掛かる前に、使用するためにデータベースのセットアップを行いましょう。

Japanese: スプリングのトランザクション管理 – データベースのセットアップ

私たちは私たちの使用のために2つのテーブルを作成し、それら両方を単一のトランザクションで更新します。

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;

ここで、Addressのid列からCustomerのid列への外部キーリレーションシップを定義することができますが、シンプルさのために制約は定義していません。私たちのデータベースの設定はSpringのトランザクション管理プロジェクトに向けて準備ができていますので、Spring Tool SuiteでシンプルなSpring Mavenプロジェクトを作成しましょう。最終的なプロジェクトの構造は以下の画像のようになります。それぞれの要素を一つずつ見ていきましょう。一緒に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>

今日の最新版にSpringのバージョンを更新しました。あなたのMySQLのインストールに対応したMySQLデータベースドライバーを使用してください。

スプリングトランザクション管理 – モデルクラス

私たちは、テーブルにマッピングされる「Customer」と「Address」という2つのJava Beansを作成します。

package com.scdev.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.scdev.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」Beanには、「Address」が変数の1つとして含まれていることに気づいてください。CustomerのためのDAOを実装する際に、CustomerとAddressの両方のテーブルのデータを取得し、これらのテーブルのために2つの別々の挿入クエリを実行することになります。そのため、データの不整合を避けるためにトランザクション管理が必要です。

春のトランザクション管理 – DAOの実装

単純化のため、顧客テーブルと住所テーブルの両方にレコードを挿入するためのDAO(データアクセスオブジェクト)を顧客のビーンに実装しましょう。

package com.scdev.spring.jdbc.dao;

import com.scdev.spring.jdbc.model.Customer;

public interface CustomerDAO {

	public void create(Customer customer);
}
package com.scdev.spring.jdbc.dao;

import javax.sql.DataSource;

import org.springframework.jdbc.core.JdbcTemplate;

import com.scdev.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の実装を受け取り、これらのクラスを制御することができないからです。」

「Springの宣言的なトランザクション管理 – サービス」

「顧客DAOの実装を使用し、顧客と住所のテーブルにレコードを挿入する際にトランザクション管理を提供する顧客サービスを作成しましょう。」

package com.scdev.spring.jdbc.service;

import com.scdev.spring.jdbc.model.Customer;

public interface CustomerManager {

	public void createCustomer(Customer cust);
}
package com.scdev.spring.jdbc.service;

import org.springframework.transaction.annotation.Transactional;

import com.scdev.spring.jdbc.dao.CustomerDAO;
import com.scdev.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アノテーションを付けることで宣言的なトランザクション管理を提供しています。これだけでSpringのトランザクション管理の利点を得るために私たちがコードで行う必要があることです。@Transactionalアノテーションはメソッドだけでなくクラス全体にも適用することができます。すべてのメソッドにトランザクション管理の機能を持たせたい場合は、このアノテーションをクラスに付ける必要があります。Springのトランザクション管理の例を動作させるためには、Springのビーンをワイヤリングするだけです。

春のトランザクション管理 – ビーンの設定

「spring.xml」という名前のSpring Beanの設定ファイルを作成します。このファイルは、テストプログラムで使用して、Spring Beanを接続し、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="scdev" />
		<property name="password" value="scdev123" />
	</bean>

	<bean id="customerDAO" class="com.scdev.spring.jdbc.dao.CustomerDAOImpl">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<bean id="customerManager" class="com.scdev.spring.jdbc.service.CustomerManagerImpl">
		<property name="customerDAO" ref="customerDAO"></property>
	</bean>

</beans>

春のビーン設定ファイルで注意すべき重要なポイントは以下のとおりです:

  • tx:annotation-driven element is used to tell Spring context that we are using annotation based transaction management configuration. transaction-manager attribute is used to provide the transaction manager bean name. transaction-manager default value is transactionManager but I am still having it to avoid confusion. proxy-target-class attribute is used to tell Spring context to use class based proxies, without it you will get runtime exception with message such as Exception in thread “main” org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘customerManager’ must be of type [com.scdev.spring.jdbc.service.CustomerManagerImpl], but was actually of type [com.sun.proxy.$Proxy6]
  • Since we are using JDBC, we are creating transactionManager bean of type org.springframework.jdbc.datasource.DataSourceTransactionManager. This is very important and we should use proper transaction manager implementation class based on our transaction API use.
  • dataSource bean is used to create the DataSource object and we are required to provide the database configuration properties such as driverClassName, url, username and password. Change these values based on your local settings.
  • We are injecting dataSource into customerDAO bean. Similarly we are injecting customerDAO bean into customerManager bean definition.

私たちの設定は完了していますので、トランザクション管理の実装をテストするために単純なテストクラスを作成しましょう。

package com.scdev.spring.jdbc.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.scdev.spring.jdbc.model.Address;
import com.scdev.spring.jdbc.model.Customer;
import com.scdev.spring.jdbc.service.CustomerManager;
import com.scdev.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");
		// setting value more than 20 chars, so that SQLException occurs
		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.scdev.spring.jdbc.dao.CustomerDAOImpl.create(CustomerDAOImpl.java:27)
	at com.scdev.spring.jdbc.service.CustomerManagerImpl.createCustomer(CustomerManagerImpl.java:19)
	at com.scdev.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.scdev.spring.jdbc.service.CustomerManagerImpl$$EnhancerBySpringCGLIB$$891ec7ac.createCustomer(<generated>)
	at com.scdev.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

以下のフレーズを日本語で同意表現してください。1つのオプションで結構です。

ログメッセージによれば、データが顧客テーブルに正常に挿入されましたが、MySQLデータベースドライバによって明確に住所の列が長すぎるという例外が発生しています。今、Customerテーブルを確認すると、そこには何の行もありません。つまり、トランザクションは完全にロールバックされています。トランザクション管理の仕組みがどこにあるのか疑問であれば、ログを注意深く見て、Springフレームワークによって作成されたAOPとProxyクラスに注目してください。SpringフレームワークはAround adviceを使用してCustomerManagerImplのプロキシクラスを生成し、メソッドが正常に返された場合にのみトランザクションをコミットしています。例外がある場合は、トランザクション全体をロールバックします。より多くの情報を学ぶためには、Spring AOPの例を読むことをお勧めします。これでSpringトランザクション管理の例は以上です。以下のリンクからサンプルプロジェクトをダウンロードして、もっと試してみてください。

スプリング JDBC トランザクション管理プロジェクトをダウンロードしてください。

コメントを残す 0

Your email address will not be published. Required fields are marked *


广告
広告は10秒後に閉じます。
bannerAds