0

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.

firstpostcommenter
  • 2,328
  • 4
  • 30
  • 59
  • Have you tried to save `employe` and `books` without calling `employeeTable = employee()`, I assume that hibernate should take care of making relations between child to parent, as during employee save you have provided list of books? – artiomi Oct 14 '22 at 09:47
  • if I make employeeTable field as var and nullable and do not pass any value for this field then I get the error `ids for this class must be manually assigned before calling save(): persistence.model.Book; ` – firstpostcommenter Oct 14 '22 at 09:56
  • Try setting for `Book` an id before saving it, cause field the field in the model is not annotated with ` @GeneratedValue(generator = "UUID")` – artiomi Oct 14 '22 at 10:02
  • oh, that was my mistake. now I get the error `org.springframework.dao.DataIntegrityViolationException: not-null property references a null or transient value : persistence.model.Book.employeeTable` – firstpostcommenter Oct 14 '22 at 10:21
  • If I remove the `nullable=false` for the ManyToOne mapping in the `Book` child class then I would get error from the database(I am using Postgres database). `Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "employee_table_id" of relation "book" violates not-null constraint` – firstpostcommenter Oct 14 '22 at 10:40
  • Try to pass employee entity to book creation method `bookList = listOf(book(this))` and in `private fun book(parent:Employee)` assign received parent `employeeTable = parent` – artiomi Oct 14 '22 at 13:17
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248799/discussion-between-artiomi-and-firstpostcommenter). – artiomi Oct 14 '22 at 13:48
  • https://stackoverflow.com/questions/60798514/one-to-many-many-to-many-attribute-value-type-should-not-be-extends – firstpostcommenter Oct 17 '22 at 08:57
  • https://www.reddit.com/r/Kotlin/comments/6s7uia/using_kotlin_with_jpa_hibernate/ – firstpostcommenter Oct 17 '22 at 11:11
  • https://stackoverflow.com/questions/37192657/jpa-several-manytoone-with-cascade – firstpostcommenter Oct 17 '22 at 11:17

1 Answers1

0

I had to change the child collection to mutableList instead of List and initialise it in 2 steps instead of one step.

Employee:

  @OneToMany(cascade = [CascadeType.ALL], mappedBy = "employeeTable")
  @OnDelete(action = OnDeleteAction.CASCADE)
  @LazyCollection(LazyCollectionOption.FALSE) //eager fetch type
  val bookList: MutableList<Book> = mutableListOf()

Book:

  @ManyToOne //no cascade to other entities by default in jpa
  @JoinColumn(name = "employee_table_id")
  @LazyCollection(LazyCollectionOption.TRUE) //lazy fetch type
  val employeeTable: Employee

Entity initialization:

private fun employee(): Employee {
    val employee = Employee(
      //id = UUID.randomUUID(), id autogenerated
      modifiedTime = LocalDateTime.now(),
      employeeName = "name"
      //bookList = listOf(book(this.employee()))
    )
    //employee.bookList = mutableListOf(book(employee))
    employee.bookList.addAll(mutableListOf(book(employee)))

    return employee
  }

  private fun book(parent: Employee): Book {
    return Book(
      //id = UUID.randomUUID(), id autogenerated
      modifiedTime = LocalDateTime.now(),
      bookName = "book name",
      bookPrice = BigDecimal.TEN,
      employeeTable = parent
    )
  }

I hope there is a better way to do this without changing the List to mutableList.


Edit:

I think it has to be mutableList. For example, if I retrieve a parent object and add an item to the child collection then I have to do it as a mutable list. Otherwise I would have to create a copy of the retrieved Parent object with the updated Child collection. Then this updatedParent object will not be a JPA managed object anymore. It will be a detached object so during the update call the JPA will make multiple select statements(to check that the Parent and already existing elements in Child collection are not changed) and then it will insert the new Child element. This would affect the performance.

firstpostcommenter
  • 2,328
  • 4
  • 30
  • 59
  • Also, due to the circular reference this cannot be serialised/deserialised with Jackson so I have to use `JsonIdentityInfo`. I am struggling to make JsonIdentityInfo work with Kotlin. – firstpostcommenter Oct 17 '22 at 16:48