循環参照するEntity

ここでいう循環参照とは

  • 互いを外部キーで参照しあう、1組のリソースの関係。

  • 外部キーとなる項目はNotNull制約が付けられているものとする。

  • JPA上で、1対多・1対1の関連の設定が2つ以上Entity内含む。

../../_images/circular_ref.png
  • チャネルのアカウントIDは通知先のアカウントID、通知先の代表チャネルNoは、チャネルのチャネルNoを外部キーとして参照している

記述例

【通知先エンティティ】
@id
@Column(name = "account_id")
String accounId

-略-

// account_idで関連する、通知先-チャネルの 1:nの 関係
@OneToMany(mappedBy = "notifyDestination", cascade = CascadeType.ALL)
private List<ChannelEntity> channels;

// representativ_channel_noで関連する、通知先-代表チャネルの 1:1 の関係
@OneToOne
@JoinColumn(name = "representative_channel_no", referencedColumnName = "channel_no")
private ChannelEntity channel;

// 関連付け用メソッドは省略
【チャネルエンティティ】
@id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "channel_no")
Integer channelNo;

-略-

@ManyToOne
@JoinColumn(name = "account_id")
private NotifyDestinationEntity notiryDestination;

JPAの動作

  • 大前提として、JPAは循環参照をサポートしていない。

  • JPAのInsert時に値が連動できるのは、連動先のレコードが正常にInsert/Update完了しているときのみ。

    • 互いに項目を参照しあっているため、片方のみInsertが完了することはない

    • こういった場合、JPAは一度参照項目をNullで挿入し、後からUpdateを行うという戦略をとる

      • 今回の定義では、ここでNull制約に抵触する。

対応方針

  • 大原則:循環参照するリソースは設計しない※

  • 別途管理用のフラグを設ける、相互参照を管理するリソースを追加する等

  • java上で外部キーのどちらかを、手動で設定するような記述に変える → 後述

  • NativeQueryを使用する。

  • nullable制約を外す(下策。)

Caution

※あくまで、JPAを利用するのであればという前提での話である。

java上で外部キーのどちらかを、手動で設定するような記述に変える

【通知先エンティティ】
@id
@Column(name = "account_id")
String accounId

-略-

@OneToMany(mappedBy = "notifyDestination", cascade = CascadeType.ALL)
private List<ChannelEntity> channels;

@OneToOne
@JoinColumn(name = "representative_channel_no", referencedColumnName = "channel_no")
【チャネルエンティティ】
@id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "channel_no")
Integer channelNo;

// 外部キーの設定とは別に、手動設定用のカラムを用意する
@Column(name = "account_id")
String accountId;

-略-

// insertable/updatable = false とし、外部キー設定のaccount_idをSQLの登録/更新対象から外す
@ManyToOne
@JoinColumn(name = "account_id", insertable = false, updatable = false) //
private NotifyDestinationEntity notiryDestination;
上記例では、account_idを、双方のエンティティで手動入力の値とすることで、
ChannelEntity側だけ単体でInsert可能な状態にしている。
上記処理を利用するAPIの入力として、account_idが与えられるため行えた対応。

外部キーが双方ともに自動採番のサロゲートキーのようなものである場合、
片方のみ先に採番結果を取得→手動設定という流れをとれば、同様に実装可能だが、
その場合、シーケンスジェネレーターは使用不可となるため注意。

Note

互いのサロゲートキーを外部キーとして参照しあうような設計は避けるべきである。