Springboot × JPAで循環参照に気をつけろ！

## はじめに
SpringBootで×JPA×Kotlinでバックエンド開発を行ってる時にハマったのでメモがてら共有します。


## 問題のコード
発生した問題を再現したコードです。
従業員と部署のテーブルがあります。

```kotlin:DepartmentEntity
package com.example.demo.entity
import jakarta.persistence.*

@Entity
@Table(name = "departments")
data class Department(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    var name: String,

    @OneToMany(mappedBy = "department",cascade=[CascadeType.ALL], fetch = FetchType.EAGER)
    val employees: MutableList<Employee> = mutableListOf()
)
```

```kotlin:EmployeeEntity
package com.example.demo.entity

import jakarta.persistence.*

@Entity
@Table(name = "employees")
data class Employee(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    var name: String ="",

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "department_id")
    var department: Department
)
```

これら2つのentityは
departmentからは@ OneToManyで
employeesからは＠ ManyToOneでリレーションされているので
departmentを取得するとemployeeも同時に取得できますし
employeeを取得するとemployeeも同時に取得できます。

そして以下の様にserviceとrepositoryを実装しました

```kotlin:service
package com.example.demo.service

import com.example.demo.entity.Department
import com.example.demo.repository.DepartmentRepository
import org.springframework.stereotype.Service

interface DefaultDepartmentService{
    fun getAllDepartment():List<Department>
}

@Service
class DepartmentService(
   val  departmentRepository: DepartmentRepository
):DefaultDepartmentService {
    override fun getAllDepartment(): List<Department> {
        return departmentRepository.findAll()
    }
}
```
```kotlin:DepartmentRepository
package com.example.demo.repository
import com.example.demo.entity.Department
import com.example.demo.entity.Employee
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface DepartmentRepository : JpaRepository<Department, Long> {
}

```

```kotlin:EmployeeRepository
ppackage com.example.demo.repository
import com.example.demo.entity.Employee
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface EmployeeRepository : JpaRepository<Employee, Long> {
}

```


上記の様な実装に対して以下の様にH2Databaseを使って
serviceとrepositoryの統合テストを書いてる途中に
serviceの返り値をprintして確認してみようと思いresultをprintしたところ問題が発生しました。
(assertがないテストになってないですがそこは無視してください)

```kotlin:test
package com.example.demo.service

import com.example.demo.entity.Department
import com.example.demo.entity.Employee
import com.example.demo.repository.DepartmentRepository
import com.example.demo.repository.EmployeeRepository
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext

@SpringBootTest
class DepartmentServiceTest {
    @Autowired
    private lateinit var departmentService: DepartmentService

    @Autowired
    private lateinit var departmentRepository: DepartmentRepository

    @Autowired
    private lateinit var employeeRepository: EmployeeRepository

    @Test
    fun `全件取得する`(){

        //データの仕込み
        val departmentRecord1 = Department(
            name = "開発部"
        )
        val employee1 = Employee(
            name = "nami",
            department = departmentRecord1
        )
        val employee2 = Employee(
            name = "suga",
            department = departmentRecord1
        )
        val employee3 = Employee(
            name = "suganami",
            department = departmentRecord1
        )

        val departmentRecord2 = Department(
            name = "人事部"
        )
        val employee4 = Employee(
            name = "nami2",
            department = departmentRecord2
        )
        val employee5 = Employee(
            name = "suga2",
            department = departmentRecord2
        )
        val employee6 = Employee(
            name = "suganami2",
            department = departmentRecord2
        )

        departmentRecord1.employees.addAll(listOf(employee1,employee2,employee3))
        departmentRecord2.employees.addAll(listOf(employee4,employee5,employee6))

        // データの保存処理
        departmentRepository.saveAll(
            listOf(
                departmentRecord1,
                departmentRecord2
            )
        )

        // データの取得
        val result = departmentService.getAllDepartment()

        //ここでstackOverFlowのエラーが発生
        println(result)
    }

}
```

以下がエラーの内容です
java.StackOverFlowErrorが表示されてしまいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916357/0c87d42b-75f2-29b4-539e-6d22910675ee.png)


## 原因
employeesとdepartmentsのentityが
@ManyToOneと@OneToManyで循環参照しており

printlnしたときに
departmentを取得するとentityが取得され
そのentityが取得されるとdepartmentが取得され
またそのdepartmentが取得されるとentityが取得され・・・
と無限ループしてしまいjava.StackOverFlowErrorが表示されてしまいました。

各EntityのfetchTypeをlazyにしても結果は一緒です。
printlnの際に実行されるtoString()メソッドが関連entityを全て読み込んでしまうからです。


## 対策
循環参照の問題が発生するからって
循環参照の構成はやめたくないので
toString()をオーバーライドしました


```kotlin:Employees
package com.example.demo.entity

import jakarta.persistence.*

@Entity
@Table(name = "employees")
data class Employee(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    var name: String ="",

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "department_id")
    var department: Department
)
{
    //ここでtoStringメソッドをoverrideすることで循環参照によるstackOverFlowを防止
    //以下をコメントアウトするとエラーになります
    override fun toString(): String {
        return "Employee(" +
                "id = $id, " +
                "name = $name, " +
                "department = Department(" +
                "id = ${department.id} "+
                "name = ${department.name}" +
                "employee = 循環参照の為省略"+
        ")"
    }
}

```
オーバーライドするのはDepartmentかemployeeどちらかで大丈夫です
今回はemployeeでオーバーライドしました。

この様にtoStringをオーバーライドすることで
employeeの出力時にdepartmentの中のemployeeの出力を省略することができ
StackOverFlowErrorを防げます。



## 最後に
今回のコードのリポジトリです
https://github.com/riiiiion/CircularReferencing
参考にしてください。

循環参照は便利だけど
こんなデメリットもあるので気をつけて使いましょう！

もっといい対処法があれば教えてください〜