Search code examples
hibernatejpaspring-data-jpa

java.lang.StackOverflowError When updating entity with many to many relationship


So I have 3 entities, Employee, Shop and EmployeeShop which is the link entity for Employee and Shop.

@Entity
@Table(name = "employee_shops")
class EmployeeShop(

    @Id
    @ManyToOne
    var employee: Employee? = null,

    @Id
    @ManyToOne
    var shop: Shop? = null

) : Serializable {
    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }
        if (other == null || javaClass != other.javaClass) {
            return false
        }
        other as EmployeeShop
        return Objects.equals(employee, other.employee) &&
            Objects.equals(shop, other.shop)
    }

    override fun hashCode(): Int {
        return Objects.hash(employee, shop)
    }
}

The Employee

@Entity
@Table(name = "employees")
class Employee(

    @Id
    var id: Int? = null,

    var userName: String? = null,
    var email: String? = null

    @OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL], orphanRemoval = true)
    var shops: MutableList<EmployeeShop> = mutableListOf()

) {
  override fun equals(other: Any?): Boolean {
    if (this === other) {
        return true
    }
    if (other == null || javaClass != other.javaClass) {
        return false
    }
    other as Employee
    return Objects.equals(userName, other.userName) &&
        Objects.equals(email, other.email)
  }

  override fun hashCode(): Int {
    return Objects.hash(userName, email)
  }
}

And the Shop

@Entity
@Table(name = "shops")
class Shop(

    @Id
    var id: Int? = null,

    @OneToMany(mappedBy = "shop", cascade = [CascadeType.ALL], orphanRemoval = true)
    var users: MutableList<EmployeeShop> = mutableListOf(),

) {
  override fun equals(other: Any?): Boolean {
    if (this === other) {
        return true
    }
    if (other == null || javaClass != other.javaClass) {
        return false
    }
    other as Shop
    return Objects.equals(id, other.id)
  }

  override fun hashCode(): Int {
    return Objects.hash(id)
  }
}

The EmployeeRepository is something like this

interface EmployeeRepository: CrudRepository<Employee, String> {
    @Query(value = "SELECT e FROM Employee e LEFT JOIN FETCH e.shops WHERE e.id = :id")
    override fun findById(id: String): Optional<Employee>
}

The reason I override this is to add LEFT JOIN FETCH in order to avoid LazyInitializationException

To return from the API, I transform the Employee to EmployeeDto

fun getDto(entity: Employee): EmployeeDto {
    return EmployeeDto(
        userName = entity.userName,
        email = entity.email
        shops = entity.shops.map { IdAndNameDto(it.shop?.id?.toString().orEmpty(), it.shop?.name.orEmpty()) }
    )
}

class IdAndNameDto(val id: String = "", val name: String = "")

class EmployeeDto(
    val userName: String,
    val email: String,
    val shops: List<IdAndNameDto>?
)

When I use the repository to update the Employee

employeeRepo.save(employee)

The save is from CrudRepository (doc). In here the save should return an instance of Employee.

I get the following stacktrace

java.lang.StackOverflowError: null
    at ch.qos.logback.classic.spi.TurboFilterList.getTurboFilterChainDecision(TurboFilterList.java:49) ~[logback-classic-1.2.3.jar:na]
    at ch.qos.logback.classic.LoggerContext.getTurboFilterChainDecision_0_3OrMore(LoggerContext.java:269) ~[logback-classic-1.2.3.jar:na]
    at ch.qos.logback.classic.Logger.callTurboFilters(Logger.java:751) ~[logback-classic-1.2.3.jar:na]
    at ch.qos.logback.classic.Logger.isTraceEnabled(Logger.java:623) ~[logback-classic-1.2.3.jar:na]
    at org.apache.logging.slf4j.SLF4JLogger.isEnabledFor(SLF4JLogger.java:213) ~[log4j-to-slf4j-2.12.1.jar:2.12.1]
    at org.apache.logging.slf4j.SLF4JLogger.isEnabled(SLF4JLogger.java:121) ~[log4j-to-slf4j-2.12.1.jar:2.12.1]
    at org.apache.logging.log4j.spi.AbstractLogger.isEnabled(AbstractLogger.java:1505) ~[log4j-api-2.12.1.jar:2.12.1]
    at org.jboss.logging.Log4j2Logger.isEnabled(Log4j2Logger.java:46) ~[jboss-logging-3.4.1.Final.jar:3.4.1.Final]
    at org.jboss.logging.Logger.isTraceEnabled(Logger.java:98) ~[jboss-logging-3.4.1.Final.jar:3.4.1.Final]
    at org.jboss.logging.DelegatingBasicLogger.isTraceEnabled(DelegatingBasicLogger.java:54) ~[jboss-logging-3.4.1.Final.jar:3.4.1.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:502) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:208) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:332) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:108) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:113) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1176) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1041) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:687) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolve(EntityType.java:464) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.ManyToOneType.resolve(ManyToOneType.java:240) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolve(EntityType.java:457) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.ComponentType.resolve(ComponentType.java:695) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.extractKeysFromResultSet(Loader.java:878) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:732) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.processResultSet(Loader.java:1008) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQuery(Loader.java:964) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:354) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:324) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.loadEntity(Loader.java:2410) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:63) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:4396) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4386) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:569) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:537) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:208) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:332) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:108) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:113) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1176) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1041) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:687) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolve(EntityType.java:464) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.ManyToOneType.resolve(ManyToOneType.java:240) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.EntityType.resolve(EntityType.java:457) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.type.ComponentType.resolve(ComponentType.java:695) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.extractKeysFromResultSet(Loader.java:878) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:732) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.processResultSet(Loader.java:1008) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQuery(Loader.java:964) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:354) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:324) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.Loader.loadEntity(Loader.java:2410) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:63) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:4396) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
    at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4386) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
...

The rest of the stack trace is just repeating of these few lines which you can also see from the above stacktrace

at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:502) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:208) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:332) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:108) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:113) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1176) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1041) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:687) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.type.EntityType.resolve(EntityType.java:464) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.type.ManyToOneType.resolve(ManyToOneType.java:240) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.type.EntityType.resolve(EntityType.java:457) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.type.ComponentType.resolve(ComponentType.java:695) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.extractKeysFromResultSet(Loader.java:878) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:732) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.processResultSet(Loader.java:1008) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.doQuery(Loader.java:964) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:354) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:324) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.Loader.loadEntity(Loader.java:2410) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:74) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:63) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.doLoad(AbstractEntityPersister.java:4396) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]
at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:4386) ~[hibernate-core-5.4.12.Final.jar:5.4.12.Final]

But I don't have any error when I try to create an Employee with the same save method.

Also, this only happens if the employee entity contains an non-empty shops object. If the shop is empty, which means the Employee I got using findById doesn't have any record in the EmployeeShop link table, then the update also don't any error.

I'm still new to JPA, so my suspect is that the way I declared those Entities, like how I use annotations, is not correct. But I'm not sure where is the problem.

Does anyone know why I got the StackOverflow error?


Solution

  • I accidentally solved this by using the "trying-everything-I-can" approach.

    1. I removed the EmployeeShop entity class.
    2. In the Employee class, I made this change.

      // old
      @OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL], orphanRemoval = true)
      var shops: MutableList<EmployeeShop> = mutableListOf()
      
      // new
      @ManyToMany
      @JoinTable(
          name = "employee_shops",
          joinColumns = [JoinColumn(name = "employee_id")],
          inverseJoinColumns = [JoinColumn(name = "shop_id")]
      )
      var shops: MutableSet<Shop> = mutableSetOf()
      
    3. And in the Shop class

      // old
      @OneToMany(mappedBy = "shop", cascade = [CascadeType.ALL], orphanRemoval = true)
      var users: MutableList<EmployeeShop> = mutableListOf()
      
      // new
      @ManyToMany(mappedBy = "shops")
      var users: MutableSet<Employee> = mutableSetOf()
      

    Then the exception was gone and the app was back to work again. My Hibernate/JPA knowledge is not adequate to explain why this works. But if anyone has the same issue, this may be the solution for you.