JPAオブジェクトのLombokでStackOverflowErrorが発生
循環参照が発生しメモリリーク
JPA+Lombokを組み合わせて利用している際に、以下のようなJPAで取得した親子関係を持つEntityオブジェクトを標準出力や文字列化、エラーログ出力時にそのまま渡してtoString()メソッドを呼び出した際にjava.lang.StackOverflowError: nullが発生する場合があります。
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;
}
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から除外したいフィールドに以下をつけるだけです。
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を組み合わせて利用する可能性がある方は是非覚えておくと良いでしょう♪
