質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
JUnit

JUnitは、Javaで開発されたプログラムのユニットテストを行うためのアプリケーションフレームワークです。簡単にプログラムのユニットテストを自動化することができ、結果もわかりやすく表示されるため効率的に開発時間を短縮できます。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

データベース

データベースとは、データの集合体を指します。また、そのデータの集合体の共用を可能にするシステムの意味を含めます

Spring Boot

Spring Bootは、Javaのフレームワークの一つ。Springプロジェクトが提供する様々なフレームワークを統合した、アプリケーションを高速で開発するために設計されたフレームワークです。

Q&A

解決済

3回答

6385閲覧

【JPA】結合されたテーブルに対して動的クエリで検索する方法

waito

総合スコア23

JUnit

JUnitは、Javaで開発されたプログラムのユニットテストを行うためのアプリケーションフレームワークです。簡単にプログラムのユニットテストを自動化することができ、結果もわかりやすく表示されるため効率的に開発時間を短縮できます。

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

データベース

データベースとは、データの集合体を指します。また、そのデータの集合体の共用を可能にするシステムの意味を含めます

Spring Boot

Spring Bootは、Javaのフレームワークの一つ。Springプロジェクトが提供する様々なフレームワークを統合した、アプリケーションを高速で開発するために設計されたフレームワークです。

0グッド

0クリップ

投稿2020/08/04 20:29

編集2020/08/05 04:12

はじめに

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が発生します。
本題とはずれますが、これについてもご存じの方がいれば教えて下さい。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

A-pZ

2020/08/05 00:12

テストクラスの @Sql(scripts = "classpath:/sql/data.sql", config = @SqlConfig(encoding = "utf-8")) で指定している SQL も追記したほうが良いでしょう。
waito

2020/08/05 01:43

ありがとうございます。テストクラスの下に追記しました。
guest

回答3

0

試していないのですが、@Sqlで定義しているINSERT文が@Testごとに実行されているように思えます。

@Transactionを併用するか、@BeforeAllで投入SQLを1度だけにするかなどで対応できるかもしれません。

投稿2020/08/05 04:18

A-pZ

総合スコア12011

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

自己解決

大変恥ずかしいのですが、自己解決しました。

テストクラスに@Sql(scripts = "classpath:/sql/data.sql", config = @SqlConfig(encoding = "utf-8"))をつけていたのですが、これが原因でした。

クラスに@Sqlつけると一番最初に一度だけ、
指定したsqlが実行されると思っていたのですが、
各メソッド実行前に呼び出されるようです。

つまり、テストメソッドが2個あるので、
テストデータ作成用sqlが2回実行されてしまっていたということです。

見当違いな質問をしてしまい、申し訳ございませんでした。
恥ずかしいので、この質問は数日後に消すかもしれません。

また、ご存じの方がいれば教えて頂きたいのですが、
テストの最初に一度だけ、テストデータ作成用のsqlを実行する方法はありますでしょうか?
(@BeforeAllをつけたメソッドに@Sqlをつけるという方法ではsqlが実行されませんでした)

また、JPAが実行したsqlではなく、自分が作成したsqliptから実行したsqlもログに吐き出すようにする方法はありますでしょうか?

以上、宜しくお願い致します。

投稿2020/08/05 04:11

waito

総合スコア23

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sazi

2020/08/05 04:27 編集

恥ずかしいから消すと言いつつ、追加の質問をここでするのですね。 解決したのなら、別質問にされた方が良いと思います。 一応回答に追記はしています。
waito

2020/08/05 04:36

> 解決したのなら、別質問にされた方が良いと思います。 失礼しました。 >DB側でSQLトレースすれば良いかと思います。 調べてやってみようと思います。 ありがとうございました。
guest

0

selectではデータ更新が行われることはありません。
追加用の処理が呼び出されているのではないでしょうか。

JPAが実行したsqlではなく、自分が作成したsqliptから実行したsqlもログに吐き出すようにする方法はありますでしょうか?

DB側でSQLトレースすれば良いかと思います。

投稿2020/08/04 23:54

編集2020/08/05 04:24
sazi

総合スコア25327

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

waito

2020/08/05 01:50 編集

回答ありがとうございます。 どこかで追加用の処理が呼び出されてしまっているのだとは思います。 ただ、追加用の処理は実装しておらず、 JPAにあらかじめ備わっている追加用の処理も使用しようとはしていないので、 何が原因か分かっていない状態です。 テストデータのSQLを追記したので、何か解決のアドバイスがあれば、頂けるとありがたいです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問