今回も引き続き、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クラスを利用する。