vladmihalcea / hypersistence-optimizer

Hypersistence Optimizer allows you to get the most out of JPA and Hibernate. By scanning your application configuration and mappings, Hypersistence Optimizer can tell you what changes you need to do to speed up your data access layer.
https://vladmihalcea.com/hypersistence-optimizer/
Apache License 2.0
306 stars 43 forks source link

NullCollectionEvent is triggered for entities that use the Bytecode Enhancement lazy loading #214

Closed DawidStuzynski closed 8 months ago

DawidStuzynski commented 9 months ago

Hi, I am using Kotlin + Quarkus 3.5.0 with Hypersistence Optimizer I have something similar in my app @OneToMany(mappedBy = "someId", cascade = [PERSIST, REMOVE], fetch = LAZY) val enities: MutableSet<ChildEntity> = mutableSetOf() but after test it using Hypersistence Optimizer I got a NullCollectionEvent on this element, is it something related to Kotlin or Quarkus?

vladmihalcea commented 9 months ago

It could be a false positive in Hypersistence Optimizer due to the differences between Kotlin and Java. Hypersistence Optimizer is built and tested using Java, and it can work with Kotlin, but there might be some issues that need to be addressed.

I will try to build an example using your mapping and see if I can replicate it.

DawidStuzynski commented 9 months ago

After deeper investigation I found out that it only takes place if primary constructor of entity is not empty

@Entity
class ParentEntity(
    val stringField: String
) {

    @Id
    @SequenceGenerator(name = "parent_entity_sq", sequenceName = "parent_entity_sq", allocationSize = 50)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "parent_entity_sq")
    var id: Long? = null

    @OneToMany(mappedBy = "parentEntity", cascade = [CascadeType.PERSIST], fetch = FetchType.LAZY)
    val entities: MutableSet<ChildEntity> = mutableSetOf()

    fun addEntity(entity: ChildEntity) {
        entities.add(entity)
        entity.parentEntity = this
    }

    fun removeEntity(entity: ChildEntity) {
        entities.remove(entity)
        entity.parentEntity = null
    }
}
@Entity
class ChildEntity(

    val field: String,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_entity_id")
    var parentEntity: ParentEntity? = null
) {

    @Id
    @SequenceGenerator(name = "child_entity_sq", sequenceName = "child_entity_sq", allocationSize = 50)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "child_entity_sq")
    var id: Long? = null
    override fun toString(): String {
        return "ChildEntity(field='$field', id=$id)"
    }
}

here is example of mapping I made In one of my test projects I also made branch with little reproducer, you can find it here: https://github.com/DawidStuzynski/hibernate-quarkus-batching-issue/tree/hypersistence-optimizer-null-event

vladmihalcea commented 9 months ago

Thanks for the replicating test case. I'll debug it through.

vladmihalcea commented 9 months ago

@DawidStuzynski I debugged the example, and the issue is generated by the Hibernate bytecode enhancement mechanism which allows the attribute to be null as the initialization will happen upon calling the getter for the very first time.

I will fix this issue and release it in the 2.8 version which will be released soon.

vladmihalcea commented 8 months ago

Fixed.