I’m wondering what’s the suggested approach to map (unidirectional, optional) ManyToOne and OneToOne relations lazily (see Kotlin Spring Boot Data JPA example below).
I’ve read about suggestion from @vlad using MapsId for OneToOne, but that’s clearly not an option if the IDs are different or for ManyToOne.
The only way to this seems to be using bytecode enhancement, but the documentation says that hibernate.enhancer.enableLazyInitialization is deprecated without a replacement.
@Entity
class BusinessPartner(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val businessPartnerId: Int = 0,
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "address_id")
val address: BusinessPartnerAddress? = null,
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "customer_id")
val customer: BusinessPartnerRole? = null,
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
@JoinColumn(name = "supplier_id")
val supplier: BusinessPartnerRole? = null,
)
I wonder why hibernate can’t just create a proxy from the foreign-key value if the foreign-key is non-null and set it to null otherwise.
That property has only been deprecated because it will no longer be possible to disable byte code-enhanced laziness, so this is still a viable option.
That is exactly what Hibernate does, and would do with the mappings you pasted above. Lazy to-ones will need to be eagerly fetched only when using e.g. @NotFound, meaning that there is no foreign-key constraint on the table and there might be non-null foreign keys that point to non-existed associated records.
Regarding bytecode enhancement i found 6.2 Migration Guide confirming what you explained: that the settings are removed, but only because they’re enabled by default. Would be nice if that would’ve been also mentioned in the docs like that
With bytecode enhancement I can see the access being rewritten like this (original source is Kotlin):
@OneToOne(
cascade = {CascadeType.ALL},
fetch = FetchType.LAZY
)
@JoinColumn(
name = "customer_id"
)
@Nullable
BusinessPartnerRole customer;
@Nullable
public final BusinessPartnerRole getCustomer() {
return this.$$_hibernate_read_customer();
}
public BusinessPartnerRole $$_hibernate_read_customer() {
if (this.$$_hibernate_getInterceptor() != null) {
this.customer = (BusinessPartnerRole)this.$$_hibernate_getInterceptor().readObject(this, "customer", this.customer);
}
return this.customer;
}
This leads to org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor#handleRead being called when trying to access e.g. the customer property.
However, inside that method there is a check for isAttributeLoaded and isLazyAttribute and both return false, as the internal Set<String> lazyFields is empty.
This leads to nothing lazy-fetched from the database - every read of customer returns an entity with all the fields null (except for the identifier).
What is wrong here? How to make lazy fetching working?
I dug deeper into this and it seems the behavior of LazyAttributeLoadingInterceptor is expected since a proxy should be used for the customer entity (not the attribute-interceptor).
The customer instance is not a HibernateProxy, but it has been bytecode-enhanced and Hibernate.isInitialized returns false for it. However, the initialization seems to be never executed.
val businessPartner = businessPartnerRepository.findById(1).orElseThrow()
Hibernate.isInitialized(businessPartner) // true
// expecting non-initialized proxy here, this looks fine
Hibernate.isInitialized(businessPartner.customer) // false
// property access to getCustomer() internally (Kotlin compiler)
println(businessPartner.customer?.fullName) // null
// expecting initialized proxy here, but still empty
Hibernate.isInitialized(businessPartner.customer) // false
@stw lazy-initialization should occur when accessing the lazy entity’s properties. Not sure why it’s not happening here, but it might be to do with how Kotlin generates Java classes, we know there are issues there with inheritance, for example, since it duplicates every super-type property for each of the sub-classes.
In general, Kotlin support in Hibernate is best-effort, and bytecode enhancement could easily break there. If you find you can reproduce the issue with plain Java code, we can try looking into it for you.
@mbladel I converted the entity classes to Java and that fixed the issue (everything else is still Kotlin). So it really looks like Hibernate byte enhancement is buggy for Kotlin .
In Java the lazy loading is triggered by EnhancementAsProxyLazinessInterceptor as far as I saw; i still need to check why this doesn’t work for Kotlin.
Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.