Spring Boot DB連携

Atomikosによる分散トランザクション処理を実装してみた(ソースコード編)

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

前提条件

下記記事を参照のこと。

Atomikosによる分散トランザクション処理を実装してみた(完成イメージ編)複数のデータベース間で一連のデータ操作を行うことを「分散トランザクション」といい、分散トランザクションにおいてデータの整合性を管理できる...

作成したサンプルプログラムの内容

作成したサンプルプログラムの構成は以下の通り。
サンプルプログラムの構成
なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。

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')
               &lt;= 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')
                   &lt;= #{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クラスを利用する。