The effective fetch type was changed from EAGER to LAZY when using BatchSize in the Hibernate 6 version

Hi team,

In our application we use these properties for the persistent unit:

            <property name="hibernate.default_batch_fetch_size" value="100"/>
            <property name="hibernate.jdbc.batch_size" value="100"/>
            <property name="hibernate.jdbc.fetch_size" value="400"/>

When we were on the Hibernate 5.4.x version, the above properties only changed the “global” BatchSize but since Hibernate 6.2 the effective fetch type was changed to LAZY for @ManyToOne relations.

Digging into Hibernate modules shows that setting “global” BatchSize forces to use BatchInitializeEntitySelectFetchInitializer instead of the default EntitySelectFetchInitialize for @ManyToOne fields

Then in the BatchInitializeEntitySelectFetchInitializer#resolveInstance() method, we have got a new logic that forces to use a Proxy. As a result for our EAGER @ManyToOne field, we got the HibernateProxy.
(in Hibernate 6.6 that code was changed a bit but core logic remains the same)

// Force creating a proxy
initializedEntityInstance = rowProcessingState.getSession().internalLoad(
		entityKey.getEntityName(),
		entityKey.getIdentifier(),
		false,
		false
);

Besides the changes in the above LAZY/EAGER behavior, we have another unexpected side effect related to our Entity customization.

In our Framework we have such entity:

@Entity
public class OrdeImpl {

    @ManyToOne(targetEntity = CustomerImpl.class)
    @JoinColumn(name = "CUSTOMER_ID")
    protected Customer customer;

    public Customer getCustomer() {
        return this.customer;
    }

}

And in our application, the Entity Customer was extended to CustomCustumer

Let’s compare 5.x and 6.x Hibernate behavior.
When we fetch Order in orderDao and then call getCustomer()
5.x
We get the extended type of that field - CustomCustumer.
This is exactly what we expected.

6.x
We get the parent Framework type of that field - Customer.
It is an instance of HibernateProxy.
Both are unexpected for us.

And then after getting that Customer from Order we could not cast it to our extended CustomCustumer -
We faced ClassCastException.

My questions are:

  1. I have not found anything about changes in the default LAZY/EAGER behavior in the migration guides. Are these changes intentional?

  2. Is there some way to come back to the previous behavior - always to have effective fetch type EAGER for @ManyToOne even when we use “global” BatchSize?

Thank you!

I don’t know how you came to this conclusion, but Hibernate ORM respects the fetch type just the same way as before. If you think there is a bug, 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.

This is just the way Hibernate ORM works. Sometimes, it must create proxies. Why do you separate interfaces from implementations when you rely on the implementations after all though?

You might be able to use the @ConcreteProxy annotation on these entity types for which you need the actual type, but you should re-evaluate why you need to do this in the first place. There are many other possible reasons for why you might get a proxy, so if you rely on an object being castable to a subclass, this might fail in various other ways as well.

I don’t know how you came to this conclusion, but Hibernate ORM respects the fetch type just the same way as before.

My conclusion is based on this simple logic:

  1. If I don’t set hibernate.default_batch_fetch_size property for PU then Hibernate chooses EntitySelectFetchInitializer to fetch the Customer field for Order and as a result I don’t have HibernateProxys for my field.
  2. But If I set hibernate.default_batch_fetch_size property -
    Then I get BatchInitializeEntitySelectFetchInitializer to fetch the Customer field and in this BatchInitializeEntitySelectFetchInitializer there is no other way how to get the entity except HibernateProxy.

In Hibernate 5.x independently on the hibernate.default_batch_fetch_size property I always have got Customer entity without HibernateProxy

Thanks for the link to the test case template.
But before I dig into it could you please take a quick look at the class BatchInitializeEntitySelectFetchInitializer.

In the header of that class, we see the comment:

Loads entities from the persistence context or creates proxies if not found there,
and initializes all proxies in a batch.

In the row below we force to use HibernateProxy and there is no other way to get the entity without proxy in that class.

IDK if this is a bug or not - this is actually what I wanted to clarify.
Because for us it causes unexpected behavior.

Hello, @beikov

When I tried to recreate the issue on hibernate-test-case-templates -
I have noticed one important difference between the test case and my application.

The issue does not appear in the hibernate-test-case-templates with the simplified version of my entities (just the Order entity with one Customer @ManyToOne field)
But
For hibernate-test-case-templates I have got for the Customer field
EntityJoinedFetchInitializer

For the real application I have got
EntitySelectFetchInitializer

For me this looks like a key difference but I was not able to find any documentation on how these Initializers work.
At least according to the naming EntityJoinedFetchInitializer looks more reasonable for that case ) And that variant don’t provide batches that causes an issue in my case.

Any thoughts/links on why I have got for the same type of field different types of Initializers with different fetch logic?

Thank you

As the name implies, one is used when an association is joined and the other when an association is fetched via a separate select. You can control this with the @Fetch(FetchMode.SELECT) annotation.

The thing you have to understand though, is that Hibernate ORM does not give you any guarantees on the actual object type. If you query for Customer, you’re never guaranteed to get a CustomerImpl, unless you annotate @ConcreteProxy on the entity type. The changes to how batch fetching is applied are just an example of how this was never guaranteed.
If you need the actual object type, but don’t want to use the @ConcreteProxy annotation since it has other implications, use Hibernate.unproxy().

As the name implies, one is used when an association is joined and the other when an association is fetched via a separate select. You can control this with the @Fetch(FetchMode.SELECT) annotation.

  1. When I was checking in debug mode in the real application
    org.hibernate.boot.model.internal.ToOneBinder#handleFetch
    I see that we set JOIN for toOne binder explicitly.
    But anyway, for some reason, we finally got for this field
    EntitySelectFetchInitializer

  2. When I set @Fetch(FetchMode.SELECT) for Customer in for hibernate-test-case-templates - I get
    EntityResultInitializer
    but not
    EntitySelectFetchInitializer

Possibly there is some additional logic in how Hibernate finally chooses the Initializer. But this is unclear to me.
Maybe you could suggest some point to debug where this magic happens?

Thank you

So far, you didn’t share the code yet, so I can only guess what you’re trying, but if you look into org.hibernate.sql.results.graph.entity.internal.EntitySelectFetchInitializerBuilder#createInitializer which is where the initializer is created for an association, you will see that EntityResultInitializer is not an option, so you must be debugging the wrong thing.

In addition to the above. I have found some explanations here:

If a field is not annotated with @Fetch, then the default FetchMode for this field depends on a FetchType and how the query is done:

  • FetchType.LAZY => FetchMode.SELECT
  • FetchType.EAGER:
    • getting by ID (Session.get) => FetchMode.JOIN
    • JPQL query => FetchMode.SELECT
    • Criteria API query => FetchMode.SELECT

When in hibernate-test-case-templates I switched to Criteria API to retrieve the Order -
I get for Order
BatchInitializeEntitySelectFetchInitializer
But for the customer itself I still getting
EntityResultInitializer

Possibly the is some “global” Hibernate property that drives this logic for whole application?