はじめに
Spring Bootを使用してJavaの勉強をしているのですが、
JPAの挙動で悩んでいるので、少しでも思い当たることがあれば教えて下さい。
やりたいこと
以下のような検索処理を実装しています。
- 9個の検索項目の内、一つ以上の項目を入力して検索する
- 検索対象のテーブルは2つ(CLAIMテーブルとFRAUD_SCORE_HISTORYテーブル)
- CLAIMテーブルとFRAUD_SCORE_HISTORYテーブルは親子の関係
※FRAUD_SCORE_HISTORYにも子孫テーブルが存在する
- CLAIMテーブルとFRAUD_SCORE_HISTORYテーブルから取得した要素をプロパティに持つオブジェクトのリストを検索結果として表示する
環境(pom.xmlから抜粋)
言語:Java 11
フレームワーク:Spring Boot 2.3.2
DB:H2データベース
DB操作:JPA
テストツール:Junit5
問題
Claimテーブルに1レコード挿入し、
Claimテーブルの各子孫テーブルにもそれぞれレコードを挿入した状態で
検索処理のテストをしました。
すると検索後になぜか1レコード増えてしまっていました。
主キーを自動採番する設定にしているのですが、
増えてしまった1レコードは主キー以外が1レコード目と全く同じです。
どのタイミングで増えているかですが、まず検索時は増えていません。
テーブルに1レコード存在する状態で検索すると1レコード取得出来ます。
また、同一メソッド内で2回検索処理をすると、2回とも1レコードだけ取得出来ます。
さらに、1回目のメソッド終了後も増えません。
(1メソッド実行後にDBを直接見ると1レコードしかありませんでした)
ただ、2つのメソッドで検索処理をすると、
後に実行したメソッドは2件のレコードを取得してしまいます。
テスト終了後にDBを直接見ても、レコードが増えています。
また、Claimテーブルだけでなく、その子孫テーブル全てのレコードが2倍になっています。
全て、Claimテーブルと同じく主キー以外同じレコードが2件ずつあります。
単一テーブルに対して動的クエリで検索した場合は、
正しく検索出来たので、結合されたテーブルに対して処理したことが原因だと思っています。
以下、実装したコードです。
コード
量が多いので一部省略します。
なお、動的クエリは以下を参考に実装しました。
[JPA] DB検索時の条件を動的に設定する
エンティティクラス
Java
1@Entity 2@Table(name = "CLAIM") 3@Data 4@NoArgsConstructor 5@AllArgsConstructor 6@ToString(exclude = "fraudScoreHistory") 7public class Claim implements Serializable { 8 private static final long serialVersionUID = 1L; 9 10 @Id 11 @GeneratedValue(strategy = GenerationType.IDENTITY) 12 private Integer Id; 13 14(中略) 15 16 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "claim") 17 private List<FraudScoreHistory> fraudScoreHistory; 18}
Java
1@Entity 2@Table(name = "FRAUD_SCORE_HISTORY") 3@Data 4@NoArgsConstructor 5@AllArgsConstructor 6@ToString(exclude = "fraudScoreDetails") 7public class FraudScoreHistory implements Serializable{ 8 private static final long serialVersionUID = 1L; 9 10 @Id 11 @GeneratedValue(strategy = GenerationType.IDENTITY) 12 private Integer Id; 13 14(中略) 15 16 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "fraudScoreHistory") 17 private List<FraudScoreDetail> fraudScoreDetails; 18 19 @ManyToOne(fetch = FetchType.EAGER) 20 @JoinColumn(nullable = false, name = "CLAIM_ID") 21 private Claim claim; 22}
Java
1@Entity 2@Table(name = "FRAUD_SCORE_DETAILS") 3@Data 4@NoArgsConstructor 5@AllArgsConstructor 6@ToString(exclude = "reasons") 7public class FraudScoreDetail implements Serializable{ 8 private static final long serialVersionUID = 1L; 9 10 @Id 11 @GeneratedValue(strategy = GenerationType.IDENTITY) 12 private Integer Id; 13 14(中略) 15 16 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "fraudScoreDetail") 17 private List<Reason> reasons; 18 19 @ManyToOne(fetch = FetchType.EAGER) 20 @JoinColumn(nullable = false, name = "FRAUD_SCORE_HISTORY_ID") 21 private FraudScoreHistory fraudScoreHistory; 22}
Java
1@Entity 2@Table(name = "REASONS") 3@Data 4@NoArgsConstructor 5@AllArgsConstructor 6public class Reason implements Serializable { 7 private static final long serialVersionUID = 1L; 8 9 @Id 10 @GeneratedValue(strategy = GenerationType.IDENTITY) 11 private Integer Id; 12 13(中略) 14 15 @ManyToOne(fetch = FetchType.EAGER) 16 @JoinColumn(nullable = false, name = "FRAUD_SCORE_DETAIL_ID") 17 private FraudScoreDetail fraudScoreDetail; 18}
レポジトリクラス・検索条件の実装クラス
Java
1public interface ClaimRepository extends JpaRepository<Claim, Integer>, JpaSpecificationExecutor<Claim> { 2}
Java
1@Component 2public class ClaimSpecifications { 3 4 /** 5 * @param claimNumber 6 * @return 指定文字を事案番号に含む事案 7 */ 8 public Specification<Claim> claimNumberContains(String claimNumber) { 9 return StringUtils.isEmpty(claimNumber) ? null : new Specification<Claim>() { 10 @Override 11 public Predicate toPredicate(Root<Claim> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { 12 return criteriaBuilder.equal(root.get("claimNumber"), claimNumber); 13 } 14 }; 15 } 16 17(中略:同様に各検索条件のためのメソッドを作成) 18 19 /** 20 * @param claimCategory 21 * @return 指定文字を事案カテゴリに含む事案 22 */ 23 public Specification<Claim> claimCategoryContains(String claimCategory) { 24 return StringUtils.isEmpty(claimCategory) ? null : new Specification<Claim>() { 25 @Override 26 public Predicate toPredicate(Root<Claim> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) { 27claimCategory); 28 return criteriaBuilder.equal( 29 root.join("fraudScoreHistory", JoinType.LEFT).get("claimCategory"), claimCategory); 30 } 31 }; 32 } 33 34}
サービスクラス
Java
1@Service 2@Transactional 3public class ClaimService { 4 5 @Autowired 6 ClaimRepository claimRepository; 7 8 @Autowired 9 ClaimSpecifications claimSpecifications; 10 11 /** 12 * @param claimNumber 13 * @param insuredName 14 * @param contractorName 15 * @param departmentInCharge 16 * @param baseInCharge 17 * @param insuranceType 18 * @param updateDate 19 * @param accidentDate 20 * @param claimCategory 21 * @return 指定した値で検索した事案一覧の結果 22 */ 23 public List<Claim> getClaimListByCriteria( 24 String claimNumber, String insuredName, String contractorName, 25 String departmentInCharge, String baseInCharge, String insuranceType, 26 String updateDate, String accidentDate, String claimCategory) { 27 return claimRepository.findAll(Specification 28 .where(claimSpecifications.claimNumberContains(claimNumber)) 29 .and(claimSpecifications.insuredNameContains(insuredName)) 30 .and(claimSpecifications.contractorNameContains(contractorName)) 31 .and(claimSpecifications.departmentInChargeContains(departmentInCharge)) 32 .and(claimSpecifications.baseInChargeContains(baseInCharge)) 33 .and(claimSpecifications.insuranceTypeContains(insuranceType)) 34 .and(claimSpecifications.accidentDateContains(accidentDate)) 35 .and(claimSpecifications.updateDateContains(updateDate)) 36 .and(claimSpecifications.claimCategoryContains(claimCategory))); 37 } 38}
テストクラス
Java
1@Slf4j 2@ExtendWith(SpringExtension.class) 3//@SpringBootTest(properties = 4//{ "spring.datasource.url:jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE;" }) 5@SpringBootTest 6@Sql(scripts = "classpath:/sql/data.sql", config = @SqlConfig(encoding = "utf-8")) 7class ClaimServiceTest { 8 9 @Autowired 10 ClaimService claimSerive; 11 12 @Test 13 public void testGetClaimListByCriteria() { 14 log.info("テストNo.1"); 15 List<Claim> claimList = claimSerive.getClaimListByCriteria( 16 "12345678910", "", "", "", "", "", "", "", ""); 17 log.info("claimList:{}", claimList); 18 log.info("claimList.size():{}", claimList.size()); 19 } 20 21 @Test 22 public void testGetClaimListByCriteria2() { 23 log.info("テストNo.2"); 24 List<Claim> claimList = claimSerive.getClaimListByCriteria( 25 "12345678910", "", "", "", "", "", "", "", ""); 26 log.info("claimList:{}", claimList); 27 log.info("claimList.size():{}", claimList.size()); 28 } 29}
テストデータ(scripts = "classpath:/sql/data.sql")
sql
1INSERT INTO CLAIM 2(CLAIM_NUMBER, INSURED_NAME, CONTRACTOR_NAME, DEPARTMENT_IN_CHARGE, BASE_IN_CHARGE, INSURANCE_TYPE, UPDATE_DATE, ACCIDENT_DATE) 3VALUES 4('12345678910', '鈴木一郎', '山田次郎', '第一部署', '東京本社', 'スポ協', '2020-07-25T12:00:00.000Z', '2020-07-25T12:00:00.000Z'); 5INSERT INTO FRAUD_SCORE_HISTORY 6(SCORING_DATE, CLAIM_CATEGORY, CLAIM_ID) 7VALUES 8('2020-06-25T12:00:00.000Z', '低', 1), 9('2020-07-25T12:00:00.000Z', '中', 1); 10 11INSERT INTO FRAUD_SCORE_DETAILS 12(MODEL_CATEGORY_NAME, RANK, SCORE, FRAUD_SCORE_HISTORY_ID) 13VALUES 14('特殊事案モデル', 'Low', 30, 1), 15('NC/PDモデル', 'Low', 30, 1), 16('特殊事案モデル', 'Middle', 50, 2), 17('NC/PDモデル', 'Middle', 50, 2); 18 19INSERT INTO REASONS 20(RANK, SCORE, DESCRIPTION, FEATURE_DESCRIPTION, FRAUD_SCORE_DETAIL_ID) 21VALUES 22('High', 50, '特徴量1', '特徴量1の説明', 1), 23('Middle', 10, '特徴量2', '特徴量2の説明', 1), 24('Low', -30, '特徴量3', '特徴量3の説明', 1), 25('High', 50, '特徴量1', '特徴量1の説明', 2), 26('Middle', 10, '特徴量2', '特徴量2の説明', 2), 27('Low', -30, '特徴量3', '特徴量3の説明', 2), 28('High', 70, '特徴量1', '特徴量1の説明', 3), 29('Middle', 10, '特徴量2', '特徴量2の説明', 3), 30('Low', -30, '特徴量3', '特徴量3の説明', 3), 31('High', 70, '特徴量1', '特徴量1の説明', 4), 32('Middle', 10, '特徴量2', '特徴量2の説明', 4), 33('Low', -30, '特徴量3', '特徴量3の説明', 4);
JPAで実行されたSQL
SQLはSELECT文しか実行されていません。
SQLの実行文字数は1回目よりも2回目の方が2倍以上多くなっています。
1回目のメソッド実行時は600文字程度。
2回目のメソッド実行時は1800文字程度。
全量はGitHubに上げました。
実行されたSQL
その他試したこと
動的クエリの作成方法が悪いのかと思い、以下を参考に他の方法でも試しました。
Spring Data JPAで動的にクエリを生成するQuery by Example機能
ただ、結果は同じでした。
Java
1@Service 2@Transactional 3public class ClaimService { 4 5 @Autowired 6 ClaimRepository claimRepository; 7 8 @Autowired 9 ClaimSpecifications claimSpecifications; 10 11 /** 12 * @param claimNumber 13 * @param insuredName 14 * @param contractorName 15 * @param departmentInCharge 16 * @param baseInCharge 17 * @param insuranceType 18 * @param updateDate 19 * @param accidentDate 20 * @param claimCategory 21 * @return 指定した値で検索した事案一覧の結果 22 */ 23 public List<Claim> getClaimListByExample( 24 String claimNumber, String insuredName, String contractorName, 25 String departmentInCharge, String baseInCharge, String insuranceType, 26 String updateDate, String accidentDate, String claimCategory) { 27 28 Claim claim = new Claim(); 29 30 if (!StringUtils.isEmpty(claimNumber)) { 31 claim.setClaimNumber(claimNumber); 32 } 33 34(中略:同様にif文で判定し、入力されていればset) 35 36 if (!StringUtils.isEmpty(claimCategory)) { 37 FraudScoreHistory fraudScoreHistory = new FraudScoreHistory(); 38 fraudScoreHistory.setClaimCategory(claimCategory); 39 List<FraudScoreHistory> fraudScoreList = new ArrayList<>(); 40 fraudScoreList.add(fraudScoreHistory); 41 claim.setFraudScoreHistory(fraudScoreList); 42 } 43 44 return claimRepository.findAll(Example.of(claim)); 45 }
また、関係ないと思いますが、
エンティティクラスでfetch = FetchType.LAZYとすると、
検索時にLazyInitializationExceptionが発生します。
本題とはずれますが、これについてもご存じの方がいれば教えて下さい。
回答3件
あなたの回答
tips
プレビュー