今回も引き続き、Atomikosを利用した分散トランザクションの実装について述べる。ここでは、具体的なサンプルプログラムのソースコードと、Atomikosを利用するために必要なDB更新内容を共有する。
前提条件
下記記事を参照のこと。
![](https://www.purin-it.com/wp-content/uploads/2020/04/forest-5060319_640-320x180.jpg)
作成したサンプルプログラムの内容
作成したサンプルプログラムの構成は以下の通り。
なお、上図の赤枠は、前提条件に記載したソースコードと比較し、変更になったソースコードを示す。赤枠のソースコードについては今後記載する。
build.gradleの内容は以下の通りで、atomikosを利用するための設定を追加し、カスタムログ出力関係のライブラリを削除している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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ログ出力情報を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | 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のデータベース情報は、以下のクラスで取得している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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クラスの紐づけは、以下のクラスで実施している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | 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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | 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ファイルの内容は以下の通りで、配置場所を変更したものの、内容は前提条件のプログラムと変えていない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | <?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ファイルの内容は以下の通りで、内容はデータ更新系のみとなっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?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オブジェクトの定義内容は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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型に変更している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | 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データベースから取得するようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | 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; } } } |
さらに、コントローラクラスの内容は以下の通りで、データベース更新エラー時にエラー画面に遷移するように修正している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | 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は、以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!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は以下の通り。
1 2 3 4 5 6 7 8 9 | 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; |
![権限変更の実行](https://www.purin-it.com/wp-content/uploads/2020/04/grant_exec.png)
要点まとめ
- Atomikosを利用できるようにするには、build.gradleに「spring-boot-starter-jta-atomikos」を追加する。
- データベースとMapperクラスの紐づけするには、@MapperScanアノテーションのbasePackages属性でMapperオブジェクトを指定し、sqlSessionTemplateRef属性で指定した(接続先データベース情報を含む)SqlセッションTemplateオブジェクトを関連付ければよい。
- Atomikosによる分散トランザクション管理には、JtaTransactionManagerクラスを利用する。