Accessing members of associated @OneToOne entity in POST_LOAD event listener


#1

Hi,

I have two entities that look similar to this:

EntityA:

@Entity
@Getter
@Setter
@Table(name = "TABLE_A")
public class EntityA implements SomeInterface {

    @Id
    @Column(name = "A_ID")
    private Long id;

    @OneToOne(mappedBy = "entityA")
    private EntityB entityB;

    @ManyToOne
    @JoinColumn(name = "A_C_ID")
    private EntityC entityC;

    ...
    some other fields

    @Override
    public EntityC retrieveEntityC() {
        return getEntityC();
    }
}

EntityB:

@Entity
@Getter
@Setter
@Table(name = "TABLE_B")
public class EntityB implements SomeInterface {

    @Id
    @Column(name = "B_ID")
    private Long id;

    @OneToOne(fetch = FetchType.EAGER, optional = false)
    @JoinColumn(name = "B_A_ID", unique = true, nullable = false)
    private EntityA entityA;

    ...
    some other fields

    @Override
    public EntityC retrieveEntityC() {
        return getEntityA().retrieveEntityC();
    }
}

I have implemented Hibernate event listener of type POST_LOAD. When Hibernate tries to load EntityA, it triggers loading of its child - entityB - first. When entityB is loaded, the POST_LOAD event is fired and my event listener is beeing called (in context of EntityB). In my listener I call retrieveEntityC() method from entityB, which calls retrieveEntityC() method from entityA to finally get entityC from entityA. The problem is that I get NULL instead of exact entityC object. The entityA object is just a proxy, with only id field populated and other fields set to NULL.

Does the problem result from the fact that we want to access fields from one entity (entityA) from other entity (entityB) that is a part of the initialization process (beeing in progress) of the first entity?

In this scenario, is there any way to make Hibernate actually get entityA.entityC object while handling entityB POST_LOAD event? I’m using Hibernate version 5.2.12.

I hope I’ve made myself clear, if not I will try to provide more information.


#2

It’s better if you add the entity listeners too since it’s easier to understand what you are doing inside that custom entity listener.


#3

My listener look like this:

@Component
public class CustomEventListener implements PostLoadEventListener {

@Autowired
    private EntityManagerFactory entityManagerFactory;

    @PostConstruct
    private void init() {
        SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
        EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
        registry.getEventListenerGroup(EventType.POST_LOAD).appendListener(this);
    }

    @Override
    public void onPostLoad(PostLoadEvent postLoadEvent) {
        Object loadedEntity = postLoadEvent.getEntity();
		
        if (loadedEntity instanceof SomeInterface) {
				List<EntityC> entitiesCForLoggedUser = Util.getEntitiesCForLoggedUser();
                Set<String> entityCIdsForLoggedUser = getEntityCIds(entitiesCForLoggedUser);
				
				EntityC entityCForLoadedEntity = ((SomeInterface) loadedEntity).retrieveEntityC();
				
                String entityCIdForLoadedEntity = entityCForLoadedEntity.getId();
                if (!entityCIdsForLoggedUser.contains(entityCIdForLoadedEntity)) {
					throw new EntityAccessException("validation.entity.access");
                }
        }
		
		// ... some other code
    }

    private Set<String> getEntityCIds(List<EntityC> entityCList) {
        return entityCList.stream().map(c -> c.getEntityC().getId()).collect(Collectors.toSet());
    }
}

The code breaks at line

String entityCIdForLoadedEntity = entityCForLoadedEntity.getId();

as entityCIdForLoadedEntity is null.

So to sum up the scenario is:

  1. We try to find entityA which triggers loading of a member - entityB.
  2. POST_LOAD event is fired for entityB in which we try to access other member of entityA - but it’s null.

#4

Try to debug if and see why if didn’t get initialized. I see it’s EAGER too, so it should have been initialized.


#5

What I’ve noticed is that when entity is simple enough for Hibernate to get it and its members with one query, then order of POST_LOAD events is different than in case when we need several queries to achive that goal.

For instance, if EntityA is simple like this:

@Getter
@Setter
@Table(name = "TABLE_A")
public class EntityA implements SomeInterface {

    @Id
    @Column(name = "A_ID")
    private Long id;

    @OneToOne(mappedBy = "entityA")
    private EntityB entityB;
}

and Hibernate gets EntityA and its member EntityB with one query, then POST_LOAD event is fired for EntityA first (and it’s fully loaded), then for EntityB (and it’s fully loaded as well).

But, if EntityA is more complex so when we get it we need separete query to retrive EntityB, then POST_LOAD event is called for EntityB first - and at this moment its member - entityA - is not initialized.

Is this normal Hibernate behaviour? Is there a workaround that would allow me, in second scenario, to get some member of entityA from within POST_LOAD event raised for entityB? Or maybe there is some other event that is raised when entity is actually FULLY loaded?


#6

I suppose that in the first case, EntityB is loaded because Hibernate fetched it eagerly since it cannot know whether to set the association to a Proxy or null.

In the second case, only if the entity is LAZY or if the FK is null, I suppose that the event would not be triggered.

Try to replicate it with this test case and prove that the listener does not execute the second time. If that’s the case, we could inspect to see if there’s something going wrong in Hibernate core.


#7

Maybe I didn’t make myself clear. Event IS triggered for BOTH entities and BOTH scenarios.

The difference is in the order of execution and - what’s more problematic - in the fact, that in second scenario event is triggered before the object is actually fully binded.

Steps that occur:

  1. Find entityA.
  2. Listener executes for entityB - but its member, entityA, is a proxy event if it’s EAGER. Calling entityB.getEntityA does not result in fetching object.
  3. Listener executes for other entityA members
  4. Listener executes for entityA itself - at this moment all entities are fully loaded and binded.

What I would really need is some kind of event that is raised when entity is fully loaded and binded, so all of its members are accessbile.


#8

In order to implement this feature, we need a replicating test case first.