jpa
spring-boot

Spring Data JPA において、複数エンティティから参照された際に予期せぬ SQL が実行されて EntityNotFoundException が発生する

More than 1 year has passed since last update.

概要

複数のエンティティから参照されるエンティティに JPQL にて結合を
実施した際、予期せぬ SQL が実行されて EntityNotFoundException が発生しました。
具体的には、JPQL 実行後に、参照されているエンティティの情報を元に、2 つのエンティティのうち、データが存在しないエンティティに対して SQL が実行されるため、結果としてデータが見つからない動作となります。
下記の例だと、RecordCompany に存在し、Office に存在しない ID で Office のデータを検索しようとする動作になります。
現象としては こちらの内容 に類似していると判断しております。

Artist.java
@Entity
@Getter
public class Artist {
  @Id
  private int Id;

  @ManyToOne
  @JoinColumn(name = "record_company_id")
  private RecordCompany recordCompany;

  @ManyToOne
  @JoinColumn(name = "office_id")
  private Office office;
}
RecordCompany.java
@Entity
@Getter
public RecordCompany {
   @Id
   private int id;

   @OneToMany(mappedBy = "recordCompany")
   private Set<Artist> artists;
}
Office.java
@Entity
@Getter
public Office {
   @Id
   private int id;

   @OneToMany(mappedBy = "office")
   private Set<Artist> artists;
}

JPQL

select x from RecordCompany x left join fetch Artist left join fetch Office where ...

SQLイメージ

-- 想定通りの 1度目の SQL
select ... 
    from 
record_artist a 
    left join record_company b 
on a.record_company_id = b.id 
    left join office c
on a.office_id = b.id 

-- 予期せぬ 2度目の SQL id = 1 は Office に存在しない。
select ... 
    from 
artist a 
    left join office b 
on a.office_id = b.id 
where
    b.id = 1;

回避策

幸いにドメインモデルが継承戦略を適用するにふさわしいモデルであったため、SINGLE_TABLE の継承戦略を適用することで回避できました。
2 つ以上のエンティティから参照される際、参照元のエンティティの ID は参照先のテーブルに格納されている必要があるようです。
上記例では、Affiliation クラスを用意して、affiliation テーブルに、RecordCompany と Office のレコードを格納して回避しました。

Affiliation.java
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Getter
public abstract class Affiliation {
   @Id
   private int id;
}

RecordCompany.java
@Entity
@Getter
@DiscriminatorVaue("record")
public RecordCompany extends Affiliation {
   @OneToMany(mappedBy = "recordCompany")
   private Set<Artist> artists;
}
Office.java
@Entity
@Getter
@DiscriminatorVaue("office")
public Office extends Affiliation {
   @OneToMany(mappedBy = "office")
   private Set<Artist> artists;
}
Artist.java
@Entity
@Getter
public class Artist {
  @Id
  private int Id;

  @ManyToOne
  @JoinColumn(name = "record_company_id")
  private Affiliation recordCompany;

  @ManyToOne
  @JoinColumn(name = "office_id")
  private Affiliation office;
}

補足

targetEntity 属性を使うことで こちらのページ にあるような関連テーブルで解決できる可能性もございましたが、SINGLE_TABLE のシンプルさが有効であると判断したため、上記対処策を実施いたしました。