I am using Kotlin data classes as JPA Entities. I have used the jpa
and all-open
maven plugin. I had override the equals()
and hashcode()
methods.
(I want to use data class as Entity because then I can use copy()
method of data class incase I want to change any field value)
I get stackoverflowerror
when I try to save the data with JPA bidirectional relationship. The book
child class is the foreign key owning side.
SQL:
create table employee
(
employee_id UUID PRIMARY KEY,
modified_time TIMESTAMP NOT NULL,
employee_name VARCHAR(50) NOT NULL
);
create table book
(
book_id UUID PRIMARY KEY,
modified_time TIMESTAMP NOT NULL,
book_name VARCHAR(50) NOT NULL,
book_price DECIMAL NOT NULL,
employee_table_id UUID NOT NULL
constraint employee_table_FK REFERENCES employee (employee_id) on delete cascade
);
Entity classes:
@Entity
@Table(name = "employee")
data class Employee(
@Id
@GeneratedValue(generator = "UUID")
@Column(name = "employee_id", updatable = false, nullable = false)
val id: UUID? = null,
@Column(name = "modified_time", nullable = false)
val modifiedTime: LocalDateTime,
@Column(name = "employee_name", nullable = false)
val employeeName: String,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "employeeTable")
@OnDelete(action = OnDeleteAction.CASCADE)
@LazyCollection(LazyCollectionOption.FALSE) //eager fetch type
val bookList: List<Book>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
other as Employee
return id != null && id == other.id
}
override fun hashCode(): Int = javaClass.hashCode()
}
@Entity
@Table(name = "book")
data class Book(
@Id
@GeneratedValue(generator = "UUID")
@Column(name = "book_id", updatable = false, nullable = false)
val id: UUID? = null,
@Column(name = "modified_time", nullable = false)
val modifiedTime: LocalDateTime,
@Column(name = "book_name", nullable = false)
val bookName: String,
@Column(name = "book_price", nullable = false)
val bookPrice: BigDecimal,
@ManyToOne
@JoinColumn(name = "employee_table_id", nullable = false)
@LazyCollection(LazyCollectionOption.TRUE) //lazy fetch type
val employeeTable: Employee
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || Hibernate.getClass(this) != Hibernate.getClass(other)) return false
other as Book
return id != null && id == other.id
}
override fun hashCode(): Int = javaClass.hashCode()
}
EmployeeRepository:
interface EmployeeRepository : CrudRepository<Employee, UUID>
Code to initialise the Employee object and insert/save to database:
class ProcessService(
private val employeeRepository: EmployeeRepository
) {
fun dbSave() {
val employeeOptional = employeeRepository.findById(UUID.randomUUID())
if (employeeOptional.isPresent) {
//update the database record
//....
} else {
//insert new database record
val employee = employee()
employeeRepository.save(employee)
}
}
private fun employee(): Employee {
return Employee(
//id = UUID.randomUUID(), id autogenerated
modifiedTime = LocalDateTime.now(),
employeeName = "name",
bookList = listOf(book())
)
}
private fun book(): Book {
return Book(
//id = UUID.randomUUID(), id autogenerated
modifiedTime = LocalDateTime.now(),
bookName = "book name",
bookPrice = BigDecimal.TEN,
employeeTable = employee() //recursion so stackoverflow error. How to solve this?
)
}
}
pom.xml plugin for reference:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<jvmTarget>11</jvmTarget>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin> <!-- creates no-arg constructor for every Entity -->
<plugin>all-open</plugin>
</compilerPlugins>
<pluginOptions>
<!-- Lazy Fetch would not be possible on Entities in Kotlin data classes unless we enable all-open plugin -->
<option>all-open:annotation=javax.persistence.Entity</option>
<option>all-open:annotation=javax.persistence.Embeddable</option>
<option>all-open:annotation=javax.persistence.MappedSuperclass</option>
</pluginOptions>
<args>
<arg>-Xjsr305=strict</arg> <!-- Enable strict mode for JSR-305 annotations -->
</args>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
Each employee
parent class will always have atleast 1 Book
child class so I never have to insert employee
record into the database without a book
record.
I think the way I am initializing the employeeTable
field in the Book
child class is causing the stackoverflowerror. How do I solve this error?
EDIT:
I tried to create Employee object first and then add Book list collection to it.
private fun employee(): Employee {
val employee = Employee(
//id = UUID.randomUUID(), id autogenerated
modifiedTime = LocalDateTime.now(),
employeeName = "name"
//bookList = listOf(book(this.employee()))
)
val bookList = listOf(book(employee))
val updatedEmployee = employee.copy(bookList = bookList)
return updatedEmployee
}
private fun book(parent: Employee): Book {
return Book(
//id = UUID.randomUUID(), id autogenerated
modifiedTime = LocalDateTime.now(),
bookName = "book name",
bookPrice = BigDecimal.TEN,
employeeTable = parent
)
}
Added cascade type "Persist" to ManyToOne mapping:
@ManyToOne(cascade = [CascadeType.PERSIST])
@JoinColumn(name = "employee_table_id")
@LazyCollection(LazyCollectionOption.TRUE) //lazy fetch type
val employeeTable: Employee
Now I am able to save the data to database but the Employee table record is inserted twice with 2 different ID values and Book record is inserted once.
Edit2: There were 2 inserts because the child table refers to old parent object.
private fun employee(): Employee {
val employee = Employee(
//id = UUID.randomUUID(), id autogenerated
modifiedTime = LocalDateTime.now(),
employeeName = "name"
//bookList = listOf(book(this.employee()))
)
val bookList = listOf(book(employee))
val updatedEmployee = employee.copy(bookList = bookList)
updatedEmployee.bookList?.get(0)?.employeeTable = updatedEmployee
return updatedEmployee
}
This code is inserting single parent record and child record.