Featured image of post Lombokの@DataでStackOverflowErrorが出る原因と解決方法【循環参照】

Lombokの@DataでStackOverflowErrorが出る原因と解決方法【循環参照】

JPAとLombokの@Dataを組み合わせた親子関係のEntityでtoStringを呼ぶとStackOverflowErrorになる原因は循環参照。除外したいフィールドに@ToString.Excludeを付ければ解決します。exclude指定は非推奨です♪

906文字

JPAオブジェクトのLombokでStackOverflowErrorが発生

循環参照が発生しメモリリーク

JPA+Lombokを組み合わせて利用している際に、以下のようなJPAで取得した親子関係を持つEntityオブジェクトを標準出力や文字列化、エラーログ出力時にそのまま渡してtoString()メソッドを呼び出した際にjava.lang.StackOverflowError: nullが発生する場合があります。

entities.account.AccountEntity.java
package entities.account;

import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "accounts")
@NoArgsConstructor
@Data
@Builder
@AllArgsConstructor
public class AccountEntity{
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Id
  private Long id;
  @OneToOne
  @JoinColumn(name = "id", referencedColumnName = "accountId")
  public AccountStatusEntity accountStatusEntity;
  @Column
  private String name;
  @Column
  private LocalDateTime createdAt;
  @Column
  private LocalDateTime updatedAt;
  @Column
  private LocalDateTime deletedAt;
}

entities.account.AccountStatusEntity;
package entities.account;

import java.io.Serializable;
import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "account_status")
@NoArgsConstructor
@Data
@Builder
@AllArgsConstructor
public class AccountStatusEntity implements Serializable {
  @Id
  @OneToOne
  @JoinColumn(name = "accountId", referencedColumnName = "id")
  public AccountEntity accountEntity;
  @Column private AccountStatusEnum status;

  @Column private LocalDateTime createdAt;
  @Column private LocalDateTime updatedAt;
}

エラーログ
Caused by: java.lang.StackOverflowError: null
	at entities.account.AccountStatusEntity.toString(AccountStatusEntity.java:21)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at entities.account.AccountEntity.toString(AccountEntity.java:22)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at entities.account.AccountStatusEntity.toString(AccountStatusEntity.java:21)
	at java.base/java.lang.String.valueOf(String.java:2951)
	at entities.account.AccountEntity.toString(AccountEntity.java:22)
	at java.base/java.lang.String.valueOf(String.java:2951)
...(以下無限ループ)

ログを見てもらうとわかる通り、toStringを実行した際に依存している子モデルtoStringが呼ばれ、その中のさらに依存している親モデルtoStringが呼ばれ・・・という形で無限ループしてしまっています。

解決方法

@ToString.Excludeを使う

この現象を避けるには、子Entity側のtoStringで親EntityのtoStringを呼ばないようにする必要があります。

幸い、Lombokでは上記を実装するための便利なアノテーションがあるのでそちらを利用しましょう。

toStringから除外したいフィールドに以下をつけるだけです。

entities.account.AccountStatusEntity;
package entities.account;

import java.io.Serializable;
import java.time.LocalDateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString; // ★追加

@Entity
@Table(name = "account_status")
@NoArgsConstructor
@Data
@Builder
@AllArgsConstructor
public class AccountStatusEntity implements Serializable {
  @ToString.Exclude // ★追加
  @Id
  @OneToOne
  @JoinColumn(name = "accountId", referencedColumnName = "id")
  public AccountEntity accountEntity;
  @Column private AccountStatusEnum status;

  @Column private LocalDateTime createdAt;
  @Column private LocalDateTime updatedAt;
}

これで循環参照がされなくなるので、正常にtoStringメソッドを呼び出す事が可能になりました。

@ToString(exclude = {“XXX”})は非推奨

ちなみにクラスに対して@ToString(exclude = {"除外対象の親Entityプロパティ名"})とする事でも同様の処理をする事が可能だが、こちらの記述方法は非推奨のためいずれ無くなるらしいので使わないことをオススメします。

終わりに

以上のように循環参照の恐怖を知っていないと、サーバーが落ちてしまう原因の一つとなってしまいます。

LombokとJPAを組み合わせて利用する可能性がある方は是非覚えておくと良いでしょう♪