今回も引き続き、Atomikosを利用した分散トランザクションの実装について述べる。ここでは、具体的なサンプルプログラムのソースコードと、Atomikosを利用するために必要なDB更新内容を共有する。
前提条件
下記記事を参照のこと。
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。

なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。
build.gradleの内容は以下の通りで、atomikosを利用するための設定を追加し、カスタムログ出力関係のライブラリを削除している。
plugins {
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'java'
}
apply plugin: 'io.spring.dependency-management'
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok:1.18.10'
annotationProcessor 'org.projectlombok:lombok:1.18.10'
compile files('lib/ojdbc6.jar')
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
compile group: 'org.springframework.data', name: 'spring-data-commons-core', version: '1.1.0.RELEASE'
//AOPを利用するための設定
implementation 'org.springframework.boot:spring-boot-starter-aop'
//Atomikosを利用するための設定
implementation 'org.springframework.boot:spring-boot-starter-jta-atomikos'
}
また、application.ymlの内容は以下の通りで、プライマリ/セカンダリ両方のデータベース情報と、SQLログ出力情報を追加している。
server:
port: 8084
# DB接続情報
spring:
datasource:
primary:
url: jdbc:oracle:thin:@localhost:1521:xe
username: USER01
password: USER01
driverClassName: oracle.jdbc.driver.OracleDriver
secondary:
url: jdbc:oracle:thin:@localhost:1521:xe
username: USER02
password: USER02
driverClassName: oracle.jdbc.driver.OracleDriver
# 一覧画面で1ページに表示する行数
demo:
list:
pageSize: 5
# SQLログ出力
logging:
level:
org:
springframework: warn
com:
example:
demo:
mapper:
primary:
UserDataMapperPrimary: debug
secondary:
UserDataMapperSecondary: debug
また、application.ymlのデータベース情報は、以下のクラスで取得している。
package com.example.demo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
//application.ymlに指定したPrimaryデータベースの設定を取得する
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.primary")
@Data
public class PrimaryDataBaseConfig {
/** URL */
private String url;
/** ユーザー名 */
private String username;
/** パスワード */
private String password;
/** ドライバクラス名 */
private String driverClassName;
}
package com.example.demo.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
//application.ymlに指定したSecondaryデータベースの設定を取得する
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.secondary")
@Data
public class SecondaryDataBaseConfig {
/** URL */
private String url;
/** ユーザー名 */
private String username;
/** パスワード */
private String password;
/** ドライバクラス名 */
private String driverClassName;
}
さらに、データベースの設定と使用するMapperクラスの紐づけは、以下のクラスで実施している。
package com.example.demo.config;
import javax.sql.DataSource;
import oracle.jdbc.xa.client.OracleXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import java.sql.SQLException;
//PrimaryデータベースとMapperクラスの紐づけを行う
//@MapperScanアノテーションにて、アノテーションのbasePackages下に指定したMapperオブジェクトと、
//sqlSessionTemplateRefで指定した(接続先データベース情報を含む)SqlセッションTemplateオブジェクト
//を関連付ける
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.primary"
, sqlSessionTemplateRef = "primarySqlSessionTemplate")
public class PrimaryMyBatisConfig {
/**
* Primaryデータベースのデータソースオブジェクトを生成する
* @param dbConfig Primaryデータベースの設定
* @return データソースオブジェクト
* @throws SQLException SQL例外
*/
@Primary
@Bean(name = "primaryDataSource")
public DataSource createPrimaryDataSource(
PrimaryDataBaseConfig dbConfig) throws SQLException {
//OracleXAデータソースオブジェクトを作成
OracleXADataSource xaDataSource = new OracleXADataSource();
//URL・ユーザー名・パスワードを引数の定義ファイルから取得し設定
xaDataSource.setURL(dbConfig.getUrl());
xaDataSource.setUser(dbConfig.getUsername());
xaDataSource.setPassword(dbConfig.getPassword());
//AtomikosデータソースBeanオブジェクトを生成
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
//一意なリソース名・OracleXAデータソースオブジェクトを設定し返却
atomikosDataSourceBean.setUniqueResourceName("primary");
atomikosDataSourceBean.setXaDataSource(xaDataSource);
return atomikosDataSourceBean;
}
/**
* PrimaryデータベースのSqlセッションファクトリBeanオブジェクトを生成する
* @param dataSource データソースオブジェクト
* @return SqlセッションファクトリBeanオブジェクト
* @throws Exception 例外
*/
@Primary
@Bean(name = "primarySqlSessionFactory")
public SqlSessionFactory createSqlSessionFactory(
@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//Mapperクラス内で参照しているXMLファイルのパスを指定
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/com/example/demo/mapper/primary/UserDataMapperPrimary.xml"));
return bean.getObject();
}
/**
* PrimaryデータベースのSqlセッションTemplateオブジェクトを生成する
* @param sqlSessionFactory SqlセッションTemplateオブジェクト
* @return SqlセッションファクトリBeanオブジェクト
*/
@Primary
@Bean(name = "primarySqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("primarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
package com.example.demo.config;
import oracle.jdbc.xa.client.OracleXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
import java.sql.SQLException;
//SecondaryデータベースとMapperクラスの紐づけを行う
//@MapperScanアノテーションにて、アノテーションのbasePackages下に指定したMapperオブジェクトと、
//sqlSessionTemplateRefで指定した(接続先データベース情報を含む)SqlセッションTemplateオブジェクト
//を関連付ける
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.secondary"
, sqlSessionTemplateRef = "secondarySqlSessionTemplate")
public class SecondaryMyBatisConfig {
/**
* Secondaryデータベースのデータソースオブジェクトを生成する
* @param dbConfig Secondaryデータベースの設定
* @return データソースオブジェクト
* @throws SQLException SQL例外
*/
@Bean(name = "secondaryDataSource")
public DataSource createPrimaryDataSource(
SecondaryDataBaseConfig dbConfig) throws SQLException {
//OracleXAデータソースオブジェクトを作成
OracleXADataSource xaDataSource = new OracleXADataSource();
//URL・ユーザー名・パスワードを引数の定義ファイルから取得し設定
xaDataSource.setURL(dbConfig.getUrl());
xaDataSource.setUser(dbConfig.getUsername());
xaDataSource.setPassword(dbConfig.getPassword());
//AtomikosデータソースBeanオブジェクトを生成
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
//一意なリソース名・OracleXAデータソースオブジェクトを設定し返却
atomikosDataSourceBean.setUniqueResourceName("secondary");
atomikosDataSourceBean.setXaDataSource(xaDataSource);
return atomikosDataSourceBean;
}
/**
* SecondaryデータベースのSqlセッションファクトリBeanオブジェクトを生成する
* @param dataSource データソースオブジェクト
* @return SqlセッションファクトリBeanオブジェクト
* @throws Exception 例外
*/
@Bean(name = "secondarySqlSessionFactory")
public SqlSessionFactory createSqlSessionFactory(
@Qualifier("secondaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//Mapperクラス内で参照しているXMLファイルのパスを指定
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:/com/example/demo/mapper/secondary/UserDataMapperSecondary.xml"));
return bean.getObject();
}
/**
* SecondaryデータベースのSqlセッションTemplateオブジェクトを生成する
* @param sqlSessionFactory SqlセッションTemplateオブジェクト
* @return SqlセッションファクトリBeanオブジェクト
*/
@Bean(name = "secondarySqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("secondarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
また、PrimaryデータベースのMapperクラスと参照するXMLファイルの内容は以下の通りで、配置場所を変更したものの、内容は前提条件のプログラムと変えていない。
package com.example.demo.mapper.primary;
import com.example.demo.SearchForm;
import com.example.demo.UserData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.data.domain.Pageable;
import java.util.Collection;
@Mapper
public interface UserDataMapperPrimary {
/**
* ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する
* @param searchForm 検索用Formオブジェクト
* @param pageable ページネーションオブジェクト
* @return ユーザーデータテーブル(user_data)の検索条件に合うデータ
*/
Collection<UserData> findBySearchForm(
@Param("searchForm") SearchForm searchForm
, @Param("pageable") Pageable pageable);
/**
* 指定したIDをもつユーザーデータテーブル(user_data)のデータを取得する
* @param id ID
* @return ユーザーデータテーブル(user_data)の指定したIDのデータ
*/
UserData findById(Long id);
/**
* 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する
* @param id ID
*/
void deleteById(Long id);
/**
* 指定したユーザーデータテーブル(user_data)のデータを追加する
* @param userData ユーザーデータテーブル(user_data)の追加データ
*/
void create(UserData userData);
/**
* 指定したユーザーデータテーブル(user_data)のデータを更新する
* @param userData ユーザーデータテーブル(user_data)の更新データ
*/
void update(UserData userData);
/**
* ユーザーデータテーブル(user_data)の最大値IDを取得する
* @return ユーザーデータテーブル(user_data)の最大値ID
*/
long findMaxId();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.primary.UserDataMapperPrimary">
<resultMap id="userDataResultMap" type="com.example.demo.UserData" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="birthY" property="birthY" jdbcType="VARCHAR" />
<result column="birthM" property="birthM" jdbcType="VARCHAR" />
<result column="birthD" property="birthD" jdbcType="VARCHAR" />
<result column="sex" property="sex" jdbcType="VARCHAR" />
<result column="memo" property="memo" jdbcType="VARCHAR" />
<result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
</resultMap>
<select id="findBySearchForm" resultMap="userDataResultMap">
SELECT u.id, u.name, u.birth_year as birthY, u.birth_month as birthM
, u.birth_day as birthD, u.sex, u.memo, u.sex_value
FROM
( SELECT
u1.id, u1.name, u1.birth_year, u1.birth_month, u1.birth_day
, u1.sex, u1.memo, m.sex_value
, ROW_NUMBER() OVER (ORDER BY u1.id) AS rn
FROM USER_DATA u1, M_SEX m
WHERE u1.sex = m.sex_cd
<if test="searchForm.searchName != null and searchForm.searchName != ''">
AND u1.name like '%' || #{searchForm.searchName} || '%'
</if>
<if test="searchForm.fromBirthYear != null and searchForm.fromBirthYear != ''">
AND #{searchForm.fromBirthYear} || lpad(#{searchForm.fromBirthMonth}, 2, '0')
|| lpad(#{searchForm.fromBirthDay}, 2, '0')
<= u1.birth_year || lpad(u1.birth_month, 2, '0') || lpad(u1.birth_day, 2, '0')
</if>
<if test="searchForm.toBirthYear != null and searchForm.toBirthYear != ''">
AND u1.birth_year || lpad(u1.birth_month, 2, '0') || lpad(u1.birth_day, 2, '0')
<= #{searchForm.toBirthYear} || lpad(#{searchForm.toBirthMonth}, 2, '0')
|| lpad(#{searchForm.toBirthDay}, 2, '0')
</if>
<if test="searchForm.searchSex != null and searchForm.searchSex != ''">
AND u1.sex = #{searchForm.searchSex}
</if>
ORDER BY u1.id
) u
<if test="pageable != null and pageable.pageSize > 0">
<where>
u.rn between #{pageable.offset} and (#{pageable.offset} + #{pageable.pageSize} - 1)
</where>
</if>
</select>
<select id="findById" resultMap="userDataResultMap">
SELECT id, name, birth_year as birthY, birth_month as birthM
, birth_day as birthD, sex, memo
FROM USER_DATA
WHERE id = #{id}
</select>
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM USER_DATA WHERE id = #{id}
</delete>
<insert id="create" parameterType="com.example.demo.UserData">
INSERT INTO USER_DATA ( id, name, birth_year, birth_month
, birth_day, sex, memo )
VALUES (#{id}, #{name}, #{birthY}, #{birthM}
, #{birthD}, #{sex}, #{memo,jdbcType=VARCHAR})
</insert>
<update id="update" parameterType="com.example.demo.UserData">
UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
, birth_month = #{birthM}, birth_day = #{birthD}
, sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
WHERE id = #{id}
</update>
<select id="findMaxId" resultType="long">
SELECT NVL(max(id), 0) FROM USER_DATA
</select>
</mapper>さらに、SecondaryデータベースのMapperクラスと参照するXMLファイルの内容は以下の通りで、内容はデータ更新系のみとなっている。
package com.example.demo.mapper.secondary;
import com.example.demo.UserData;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDataMapperSecondary {
/**
* 指定したIDをもつユーザーデータテーブル(user_data)のデータを削除する
* @param id ID
*/
void deleteById(Long id);
/**
* 指定したユーザーデータテーブル(user_data)のデータを追加する
* @param userData ユーザーデータテーブル(user_data)の追加データ
*/
void create(UserData userData);
/**
* 指定したユーザーデータテーブル(user_data)のデータを更新する
* @param userData ユーザーデータテーブル(user_data)の更新データ
*/
void update(UserData userData);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.secondary.UserDataMapperSecondary">
<resultMap id="userDataResultMap" type="com.example.demo.UserData" >
<id column="id" property="id" jdbcType="BIGINT" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="birthY" property="birthY" jdbcType="VARCHAR" />
<result column="birthM" property="birthM" jdbcType="VARCHAR" />
<result column="birthD" property="birthD" jdbcType="VARCHAR" />
<result column="sex" property="sex" jdbcType="VARCHAR" />
<result column="memo" property="memo" jdbcType="VARCHAR" />
<result column="sex_value" property="sex_value" jdbcType="VARCHAR" />
</resultMap>
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM USER_DATA WHERE id = #{id}
</delete>
<insert id="create" parameterType="com.example.demo.UserData">
INSERT INTO USER_DATA ( id, name, birth_year, birth_month
, birth_day, sex, memo )
VALUES (#{id}, #{name}, #{birthY}, #{birthM}
, #{birthD}, #{sex}, #{memo,jdbcType=VARCHAR})
</insert>
<update id="update" parameterType="com.example.demo.UserData">
UPDATE USER_DATA SET name = #{name}, birth_year = #{birthY}
, birth_month = #{birthM}, birth_day = #{birthD}
, sex = #{sex}, memo = #{memo,jdbcType=VARCHAR}
WHERE id = #{id}
</update>
</mapper>
また、トランザクション管理を行うJtaTransactionManagerオブジェクトの定義内容は、以下の通り。
package com.example.demo.config;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.transaction.UserTransaction;
//分散トランザクション管理を行うためのオブジェクトを生成する
@Configuration
public class TransactionConfig {
/**
* トランザクション管理を行うJtaTransactionManagerを生成
* @return JtaTransactionManagerオブジェクト
*/
@Bean
@Primary
public JtaTransactionManager regTransactionManager () {
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
}
さらに、サービスクラスの内容は以下の通りで、deleteByIdメソッド・createOrUpdateメソッドの戻り値をvoid型からint型に変更している。
package com.example.demo;
import org.springframework.data.domain.Pageable;
import java.util.List;
public interface DemoService {
/**
* ユーザーデータリストを取得
* @param searchForm 検索用Formオブジェクト
* @param pageable ページネーションオブジェクト
* @return ユーザーデータリスト
*/
List<DemoForm> demoFormList(SearchForm searchForm, Pageable pageable);
/**
* 引数のIDに対応するユーザーデータを取得
* @param id ID
* @return ユーザーデータ
*/
DemoForm findById(String id);
/**
* 引数のIDに対応するユーザーデータを削除
* @param id ID
* @return 更新成功(0)/失敗(1)
*/
int deleteById(String id);
/**
* 引数のユーザーデータがあれば更新し、無ければ削除
* @param demoForm 追加・更新用Formオブジェクト
* @return 更新成功(0)/失敗(1)
*/
int createOrUpdate(DemoForm demoForm);
/**
* ユーザー検索時に利用するページング用オブジェクトを生成する
* @param pageNumber ページ番号
* @return ページング用オブジェクト
*/
Pageable getPageable(int pageNumber);
/**
* 一覧画面の全ページ数を取得する
* @param searchForm 検索用Formオブジェクト
* @return 全ページ数
*/
int getAllPageNum(SearchForm searchForm);
}
また、サービスクラスの実装クラスは以下の通りで、deleteByIdメソッド・createOrUpdateメソッドでは、JtaTransactionManagerクラスを利用した分散トランザクション管理をしていて、Primaryデータベース・Secondaryデータベースの両方を更新する仕様になっている。また、データ参照はPrimaryデータベースから取得するようにしている。
package com.example.demo;
import com.example.demo.mapper.primary.UserDataMapperPrimary;
import com.example.demo.mapper.secondary.UserDataMapperSecondary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.util.StringUtils;
import javax.transaction.UserTransaction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
public class DemoServiceImpl implements DemoService{
/**
* 分散データベースのトランザクション管理
*/
@Autowired
private JtaTransactionManager jtaTransactionManager;
/**
* Primaryデータベースのユーザーデータ(user_data)へアクセスするマッパー
*/
@Autowired
private UserDataMapperPrimary mapperPrimary;
/**
* Secondaryデータベースのユーザーデータ(user_data)へアクセスするマッパー
*/
@Autowired
private UserDataMapperSecondary mapperSecondary;
//ログ出力のためのクラス
private Logger logger = LogManager.getLogger(DemoServiceImpl.class);
/**
* 1ページに表示する行数(application.propertiesから取得)
*/
@Value("${demo.list.pageSize}")
private String listPageSize;
/**
* {@inheritDoc}
*/
@Override
public List<DemoForm> demoFormList(SearchForm searchForm, Pageable pageable) {
List<DemoForm> demoFormList = new ArrayList<>();
//ユーザーデータテーブル(user_data)から検索条件に合うデータを取得する
Collection<UserData> userDataList
= mapperPrimary.findBySearchForm(searchForm, pageable);
for (UserData userData : userDataList) {
demoFormList.add(getDemoForm(userData));
}
return demoFormList;
}
/**
* {@inheritDoc}
*/
@Override
public DemoForm findById(String id) {
Long longId = stringToLong(id);
UserData userData = mapperPrimary.findById(longId);
return getDemoForm(userData);
}
/**
* {@inheritDoc}
*/
@Override
public int deleteById(String id){
UserTransaction userTransaction;
try{
//トランザクションを開始する
userTransaction = jtaTransactionManager.getUserTransaction();
userTransaction.begin();
try{
//引数のIDに該当するデータを削除する
Long longId = stringToLong(id);
mapperPrimary.deleteById(longId);
mapperSecondary.deleteById(longId);
//トランザクションをコミットする
userTransaction.commit();
//「0:更新成功」を返す
return 0;
}catch (Exception ex2){
//DB更新またはコミット処理でエラーが発生した場合
logger.error(ex2);
try{
//トランザクションをロールバックする
userTransaction.rollback();
}catch (Exception ex3){
//ロールバック処理でエラーが発生した場合
logger.error(ex3);
}finally {
//「1:更新失敗」を返す
return 1;
}
}
}catch (Exception ex){
//トランザクション開始処理でエラーが発生した場合
logger.error(ex);
//「1:更新失敗」を返す
return 1;
}
}
/**
* {@inheritDoc}
*/
@Override
public int createOrUpdate(DemoForm demoForm){
UserTransaction userTransaction;
try{
//トランザクションを開始する
userTransaction = jtaTransactionManager.getUserTransaction();
userTransaction.begin();
try{
//更新・追加処理を行うエンティティを生成
UserData userData = getUserData(demoForm);
//追加・更新処理を行う
if(demoForm.getId() == null){
userData.setId(mapperPrimary.findMaxId() + 1);
mapperPrimary.create(userData);
mapperSecondary.create(userData);
}else{
mapperPrimary.update(userData);
mapperSecondary.update(userData);
}
//トランザクションをコミットする
userTransaction.commit();
//「0:更新成功」を返す
return 0;
}catch (Exception ex2){
//DB更新またはコミット処理でエラーが発生した場合
logger.error(ex2);
try{
//トランザクションをロールバックする
userTransaction.rollback();
}catch (Exception ex3){
//ロールバック処理でエラーが発生した場合
logger.error(ex3);
}finally {
//「1:更新失敗」を返す
return 1;
}
}
}catch (Exception ex){
//トランザクション開始処理でエラーが発生した場合
logger.error(ex);
//「1:更新失敗」を返す
return 1;
}
}
/**
* {@inheritDoc}
*/
@Override
public Pageable getPageable(int pageNumber){
Pageable pageable = new Pageable() {
@Override
public int getPageNumber() {
//現在ページ数を返却する
return pageNumber;
}
@Override
public int getPageSize() {
//1ページに表示する行数を返却する
//listPageSizeは、本プログラムの先頭に定義している
return Integer.parseInt(listPageSize);
}
@Override
public int getOffset() {
//表示開始位置を返却する
//例えば、1ページに2行表示する場合の、2ページ目の表示開始位置は
//(2-1)*2+1=3 で計算される
return ((pageNumber - 1) * Integer.parseInt(listPageSize) + 1);
}
@Override
public Sort getSort() {
//ソートは使わないのでnullを返却する
return null;
}
};
return pageable;
}
/**
* {@inheritDoc}
*/
@Override
public int getAllPageNum(SearchForm searchForm) {
//1ページに表示する行数を取得する
int listPageSizeNum = Integer.parseInt(listPageSize);
if(listPageSizeNum == 0){
return 1;
}
//一覧画面に表示する全データを取得する
//第二引数のpageableにnullを設定することで、一覧画面に表示する
//全データが取得できる
Collection<UserData> userDataList
= mapperPrimary.findBySearchForm(searchForm, null);
//全ページ数を計算
//例えば、1ページに2行表示する場合で、全データ件数が5の場合、
//(5+2-1)/2=3 と計算される
int allPageNum = (userDataList.size() + listPageSizeNum - 1) / listPageSizeNum;
return allPageNum == 0 ? 1 : allPageNum;
}
/**
* DemoFormオブジェクトに引数のユーザーデータの各値を設定する
* @param userData ユーザーデータ
* @return DemoFormオブジェクト
*/
private DemoForm getDemoForm(UserData userData){
if(userData == null){
return null;
}
DemoForm demoForm = new DemoForm();
demoForm.setId(String.valueOf(userData.getId()));
demoForm.setName(userData.getName());
demoForm.setBirthYear(String.valueOf(userData.getBirthY()));
demoForm.setBirthMonth(String.valueOf(userData.getBirthM()));
demoForm.setBirthDay(String.valueOf(userData.getBirthD()));
demoForm.setSex(userData.getSex());
demoForm.setMemo(userData.getMemo());
demoForm.setSex_value(userData.getSex_value());
return demoForm;
}
/**
* UserDataオブジェクトに引数のフォームの各値を設定する
* @param demoForm DemoFormオブジェクト
* @return ユーザーデータ
*/
private UserData getUserData(DemoForm demoForm){
UserData userData = new UserData();
if(!StringUtils.isEmpty(demoForm.getId())){
userData.setId(Long.valueOf(demoForm.getId()));
}
userData.setName(demoForm.getName());
userData.setBirthY(Integer.valueOf(demoForm.getBirthYear()));
userData.setBirthM(Integer.valueOf(demoForm.getBirthMonth()));
userData.setBirthD(Integer.valueOf(demoForm.getBirthDay()));
userData.setSex(demoForm.getSex());
userData.setMemo(demoForm.getMemo());
userData.setSex_value(demoForm.getSex_value());
return userData;
}
/**
* 引数の文字列をLong型に変換する
* @param id ID
* @return Long型のID
*/
private Long stringToLong(String id){
try{
return Long.parseLong(id);
}catch(NumberFormatException ex){
return null;
}
}
}
さらに、コントローラクラスの内容は以下の通りで、データベース更新エラー時にエラー画面に遷移するように修正している。
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.data.domain.Pageable;
import java.util.ArrayList;
import java.util.List;
@Controller
@SessionAttributes(types = {DemoForm.class, SearchForm.class})
public class DemoController {
/**
* Demoサービスクラスへのアクセス
*/
@Autowired
private DemoService demoService;
/**
* ユーザーデータテーブル(user_data)のデータを取得して返却する
* @return ユーザーデータリスト
*/
@ModelAttribute("demoFormList")
public List<DemoForm> userDataList(){
List<DemoForm> demoFormList = new ArrayList<>();
return demoFormList;
}
/**
* 追加・更新用Formオブジェクトを初期化して返却する
* @return 追加・更新用Formオブジェクト
*/
@ModelAttribute("demoForm")
public DemoForm createDemoForm(){
DemoForm demoForm = new DemoForm();
return demoForm;
}
/**
* 検索用Formオブジェクトを初期化して返却する
* @return 検索用Formオブジェクト
*/
@ModelAttribute("searchForm")
public SearchForm createSearchForm(){
SearchForm searchForm = new SearchForm();
return searchForm;
}
/**
* 初期表示(検索)画面に遷移する
* @return 検索画面へのパス
*/
@RequestMapping("/")
public String index(){
return "search";
}
/**
* 検索処理を行い、一覧画面に遷移する
* @param searchForm 検索用Formオブジェクト
* @param result バインド結果
* @param model Modelオブジェクト
* @return 一覧画面へのパス
*/
@PostMapping("/search")
public String search(@Validated SearchForm searchForm
, BindingResult result, Model model){
//検索用Formオブジェクトのチェック処理でエラーがある場合は、
//検索画面のままとする
if(result.hasErrors()){
return "search";
}
//現在ページ数を1ページ目に設定し、一覧画面に遷移する
searchForm.setCurrentPageNum(1);
return movePageInList(model, searchForm);
}
/**
* 一覧画面で「先頭へ」リンク押下時に次ページを表示する
* @param searchForm 検索用Formオブジェクト
* @param model Modelオブジェクト
* @return 一覧画面へのパス
*/
@GetMapping("/firstPage")
public String firstPage(SearchForm searchForm, Model model){
//現在ページ数を先頭ページに設定する
searchForm.setCurrentPageNum(1);
return movePageInList(model, searchForm);
}
/**
* 一覧画面で「前へ」リンク押下時に次ページを表示する
* @param searchForm 検索用Formオブジェクト
* @param model Modelオブジェクト
* @return 一覧画面へのパス
*/
@GetMapping("/backPage")
public String backPage(SearchForm searchForm, Model model){
//現在ページ数を前ページに設定する
searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() - 1);
return movePageInList(model, searchForm);
}
/**
* 一覧画面で「次へ」リンク押下時に次ページを表示する
* @param searchForm 検索用Formオブジェクト
* @param model Modelオブジェクト
* @return 一覧画面へのパス
*/
@GetMapping("/nextPage")
public String nextPage(SearchForm searchForm, Model model){
//現在ページ数を次ページに設定する
searchForm.setCurrentPageNum(searchForm.getCurrentPageNum() + 1);
return movePageInList(model, searchForm);
}
/**
* 一覧画面で「最後へ」リンク押下時に次ページを表示する
* @param searchForm 検索用Formオブジェクト
* @param model Modelオブジェクト
* @return 一覧画面へのパス
*/
@GetMapping("/lastPage")
public String lastPage(SearchForm searchForm, Model model){
//現在ページ数を最終ページに設定する
searchForm.setCurrentPageNum(demoService.getAllPageNum(searchForm));
return movePageInList(model, searchForm);
}
/**
* 更新処理を行う画面に遷移する
* @param id 更新対象のID
* @param model Modelオブジェクト
* @return 入力・更新画面へのパス
*/
@GetMapping("/update")
public String update(@RequestParam("id") String id, Model model){
//更新対象のユーザーデータを取得する
DemoForm demoForm = demoService.findById(id);
//ユーザーデータを更新する
model.addAttribute("demoForm", demoForm);
return "input";
}
/**
* 削除確認画面に遷移する
* @param id 更新対象のID
* @param model Modelオブジェクト
* @return 削除確認画面へのパス
*/
@GetMapping("/delete_confirm")
public String delete_confirm(@RequestParam("id") String id, Model model){
//削除対象のユーザーデータを取得する
DemoForm demoForm = demoService.findById(id);
//ユーザーデータを更新する
model.addAttribute("demoForm", demoForm);
return "confirm_delete";
}
/**
* 削除処理を行う
* @param demoForm 追加・更新用Formオブジェクト
* @return 一覧画面の表示処理
*/
@PostMapping(value = "/delete", params = "next")
public String delete(DemoForm demoForm){
//指定したユーザーデータを削除する
int retStatus = demoService.deleteById(demoForm.getId());
//削除処理が失敗した場合は、エラー画面に遷移する
if(retStatus == 1){
return "redirect:/to_error";
}
//一覧画面に遷移
return "redirect:/to_index";
}
/**
* エラー画面に遷移する
* @return エラー画面
*/
@GetMapping("/to_error")
public String toError(){
return "error";
}
/**
* 削除完了後に一覧画面に戻る
* @param searchForm 検索用Formオブジェクト
* @param model Modelオブジェクト
* @return 一覧画面
*/
@GetMapping("/to_index")
public String toIndex(SearchForm searchForm, Model model){
//一覧画面に戻り、1ページ目のリストを表示する
searchForm.setCurrentPageNum(1);
return movePageInList(model, searchForm);
}
/**
* 削除確認画面から一覧画面に戻る
* @param model Modelオブジェクト
* @param searchForm 検索用Formオブジェクト
* @return 一覧画面
*/
@PostMapping(value = "/delete", params = "back")
public String confirmDeleteBack(Model model, SearchForm searchForm){
//一覧画面に戻る
return movePageInList(model, searchForm);
}
/**
* 追加処理を行う画面に遷移する
* @param model Modelオブジェクト
* @return 入力・更新画面へのパス
*/
@PostMapping(value = "/add", params = "next")
public String add(Model model){
model.addAttribute("demoForm", new DemoForm());
return "input";
}
/**
* 追加処理を行う画面から検索画面に戻る
* @return 検索画面へのパス
*/
@PostMapping(value = "/add", params = "back")
public String addBack(){
return "search";
}
/**
* エラーチェックを行い、エラーが無ければ確認画面に遷移し、
* エラーがあれば入力画面のままとする
* @param demoForm 追加・更新用Formオブジェクト
* @param result バインド結果
* @return 確認画面または入力画面へのパス
*/
@PostMapping(value = "/confirm", params = "next")
public String confirm(@Validated DemoForm demoForm, BindingResult result){
//追加・更新用Formオブジェクトのチェック処理でエラーがある場合は、
//入力画面のままとする
if(result.hasErrors()){
return "input";
}
//エラーが無ければ確認画面に遷移する
return "confirm";
}
/**
* 一覧画面に戻る
* @param model Modelオブジェクト
* @param searchForm 検索用Formオブジェクト
* @return 一覧画面の表示処理
*/
@PostMapping(value = "/confirm", params = "back")
public String confirmBack(Model model, SearchForm searchForm){
//一覧画面に戻る
return movePageInList(model, searchForm);
}
/**
* 完了画面に遷移する
* @param demoForm 追加・更新用Formオブジェクト
* @param sessionStatus セッションステータス
* @return 完了画面
*/
@PostMapping(value = "/send", params = "next")
public String send(DemoForm demoForm, SessionStatus sessionStatus){
//ユーザーデータがあれば更新し、無ければ削除する
int retStatus = demoService.createOrUpdate(demoForm);
//セッションオブジェクトを破棄
sessionStatus.setComplete();
//DB更新時エラーが発生した場合はエラー画面に、
//そうでなければ完了画面に遷移する
if(retStatus == 1){
return "redirect:/to_error";
}
return "redirect:/complete";
}
/**
* 完了画面に遷移する
* @return 完了画面
*/
@GetMapping("/complete")
public String complete(){
return "complete";
}
/**
* 入力画面に戻る
* @return 入力画面
*/
@PostMapping(value = "/send", params = "back")
public String sendBack(){
return "input";
}
/**
* 一覧画面に戻り、指定した現在ページのリストを表示する
* @param model Modelオブジェクト
* @param searchForm 検索用Formオブジェクト
* @return 一覧画面の表示処理
*/
private String movePageInList(Model model, SearchForm searchForm){
//現在ページ数, 総ページ数を設定する
model.addAttribute("currentPageNum", searchForm.getCurrentPageNum());
model.addAttribute("allPageNum", demoService.getAllPageNum(searchForm));
//ページング用オブジェクトを生成し、現在ページのユーザーデータリストを取得する
Pageable pageable = demoService.getPageable(searchForm.getCurrentPageNum());
List<DemoForm> demoFormList = demoService.demoFormList(searchForm, pageable);
//ユーザーデータリストを更新する
model.addAttribute("demoFormList", demoFormList);
return "list";
}
}
その他、新しく作成したエラー画面のHTMLは、以下の通り。
<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<title>error page</title>
</head>
<body>
DB更新時エラーが発生しました。もう一度やり直してください。<br/><br/>
<form method="post" th:action="@{/}">
<input type="submit" value="検索画面に戻る" />
</form>
</body>
</html>その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/java/tree/master/spring-boot-atomikos-2/demo
データベースの権限変更
先ほどのサンプルプログラムで、前提条件の記事の「完成した画面イメージとログの共有」に記載した画面動作は実現できるが、一定時間毎に「javax.transaction.xa.XAException: null」というエラーログが出力されてしまう。
これを防ぐためには、下記サイトに記載されている権限設定を行う。
https://blog.csdn.net/qq_37279783/article/details/89137766
実際に、sysユーザーでログイン後に実行したSQLは以下の通り。
grant select on sys.dba_pending_transactions to USER01; grant select on sys.pending_trans$ to USER01; grant select on sys.dba_2pc_pending to USER01; grant execute on sys.dbms_system to USER01; grant select on sys.dba_pending_transactions to USER02; grant select on sys.pending_trans$ to USER02; grant select on sys.dba_2pc_pending to USER02; grant execute on sys.dbms_system to USER02;

要点まとめ
- Atomikosを利用できるようにするには、build.gradleに「spring-boot-starter-jta-atomikos」を追加する。
- データベースとMapperクラスの紐づけするには、@MapperScanアノテーションのbasePackages属性でMapperオブジェクトを指定し、sqlSessionTemplateRef属性で指定した(接続先データベース情報を含む)SqlセッションTemplateオブジェクトを関連付ければよい。
- Atomikosによる分散トランザクション管理には、JtaTransactionManagerクラスを利用する。





