如何正确初始化带双向ManyToOne关联的Kotlin JPA实体?
Kotlin数据类作为JPA实体处理双向关联的问题解决
我使用Kotlin数据类作为JPA实体,为了让数据类能被JPA代理,已经引入了jpa和all-open Maven插件,同时重写了equals()和hashCode()方法(选择数据类主要是为了利用它的copy()方法便捷修改字段值)。
在处理Book(外键拥有方)和Employee的双向关联时,遇到了两个问题:
- 保存关联数据时出现
StackOverflowError,根源是双向关联的循环引用触发了递归调用 - 尝试先创建Employee对象再关联Book时,出现Employee记录被重复插入的情况
最终通过修正实体间的关联引用逻辑解决了所有问题,以下是完整实现:
SQL表结构
CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL ); CREATE TABLE book ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, employee_id BIGINT, FOREIGN KEY (employee_id) REFERENCES employee(id) );
实体类代码
Employee实体(非外键拥有方)
@Entity @AllOpen data class Employee( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val name: String, @OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL], orphanRemoval = true) val books: MutableList<Book> = mutableListOf() ) { // 重写equals和hashCode,仅基于主键和业务字段判断,规避关联集合的循环引用 override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Employee if (id != other.id) return false if (name != other.name) return false return true } override fun hashCode(): Int { var result = id?.hashCode() ?: 0 result = 31 * result + name.hashCode() return result } // 统一处理双向关联的辅助方法,确保双方引用一致 fun addBook(book: Book) { books.add(book) book.employee = this } }
Book实体(外键拥有方)
@Entity @AllOpen data class Book( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val title: String, @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "employee_id") var employee: Employee? = null ) { // 重写equals和hashCode,仅基于主键和业务字段判断 override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Book if (id != other.id) return false if (title != other.title) return false return true } override fun hashCode(): Int { var result = id?.hashCode() ?: 0 result = 31 * result + title.hashCode() return result } }
Repository代码
interface EmployeeRepository : JpaRepository<Employee, Long> interface BookRepository : JpaRepository<Book, Long>
修正后的初始化代码
@Service class DataInitializer(private val employeeRepo: EmployeeRepository) { @PostConstruct fun initData() { val employee = Employee(name = "Alice") val book1 = Book(title = "Kotlin in Action") val book2 = Book(title = "Spring Boot Guide") // 通过辅助方法建立双向关联,保证引用一致性 employee.addBook(book1) employee.addBook(book2) // 仅保存主实体,借助CascadeType.ALL自动保存关联的子实体 employeeRepo.save(employee) } }
核心解决思路
- 解决StackOverflowError:重写
equals()和hashCode()时,排除关联的集合或实体字段,仅使用自身的主键和业务唯一标识字段进行判断,避免双向关联导致的递归调用。 - 解决重复插入问题:通过实体内部的辅助方法统一维护双向关联,确保关联双方的引用完全一致;同时利用
CascadeType.ALL配置,只需要保存主实体即可完成所有关联数据的持久化,避免单独操作子实体时触发主实体的重复插入。
内容的提问来源于stack exchange,提问作者firstpostcommenter
相关产品推荐
相关产品推荐

