Unexpected proxy self-relation

I encounter a certain behaviour that I wonder if it is as expected. I have a Branch entity with a @ManyToMany to Commit. Commit has a self-relationship. When 1 branch is fetched including commits with fetch join, 1 of the commits is a hibernate proxy. The parent field on commit is Lazy. The one commit that is a proxy is not a proxy on the entity it is the parent of.

When a field with a getter is called, it works fine. With a public field you get null.

When the self-relation is removed, the behaviour doesn’t appear.

@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue
    public Long id;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }
}
@Entity
@Table(name="branch")
public class Branch extends BaseEntity {
    @ManyToMany
    public Set<Commit> commits;
}
@Entity
public class Commit extends BaseEntity {
    @ManyToOne(fetch = FetchType.LAZY)
    public Commit parent;
}
String hql = "SELECT b FROM Branch b left join fetch b.commits where b.id = 1";
Query<Branch> query = session.createQuery(hql, Branch.class);
Branch branch = query.getSingleResult();

for (Commit commit : branch.commits) {
     System.out.println(commit.id);
     System.out.println(commit.getId());
}

Console output:

1
1
null
2
3
3
4
4

Is this behaviour as expected? Hibernate version: 6.2.5.Final

Hibernate uses proxies to delay the fetching of LAZY properties. You should always use getters when using proxies, and not directly access the fields, as that’s how Hibernate can tell when to initialize delayed data.

1 Like

Thanks for the answer. But isn’t it weird that a Commit in branch.commits becomes a proxy? Because a left join fetch is used for the commits so hibernate does know about it’s existence. But because commit.parent is loaded lazy, all references to that object become a proxy?

I did some debugging and came across the implementation of resolveEntityInstance in AbstractEntityInitializer. I don’t have knowledge of Hibernate’s internals so bare with me. But this method broadly takes the proxy reference if one exists for a given entity key. Except when entityInstanceFromExecutionContext can be used. But entityInstanceFromExecutionContext is null for my scenario.

I can imagine that this is not always desired behaviour. Leads to a lot of unexpected proxies.

protected void resolveEntityInstance(
			RowProcessingState rowProcessingState,
			LoadingEntityEntry existingLoadingEntry,
			Object entityIdentifier) {

		if ( EntityLoadingLogging.TRACE_ENABLED ) {
			EntityLoadingLogging.ENTITY_LOADING_LOGGER.tracef(
					"(%s) Beginning Initializer#resolveInstance process for entity (%s) : %s",
					StringHelper.collapse( this.getClass().getName() ),
					getNavigablePath(),
					entityIdentifier
			);
		}

		final PersistenceContext persistenceContext = rowProcessingState.getSession().getPersistenceContextInternal();
		final Object proxy = getProxy( persistenceContext );
		final Object entityInstanceFromExecutionContext = getEntityInstanceFromExecutionContext( rowProcessingState );
		if ( isProxyInstance( proxy ) ) {
			if ( entityInstanceFromExecutionContext != null ) {
				entityInstance = entityInstanceFromExecutionContext;
				registerLoadingEntityInstanceFromExecutionContext( rowProcessingState, entityInstance );
			}
			else {
				entityInstance = proxy;
			}
		}
		else {
			final Object existingEntity = persistenceContext.getEntity( entityKey );
			if ( existingEntity != null ) {
				entityInstance = existingEntity;
				if ( existingLoadingEntry == null && isExistingEntityInitialized( existingEntity ) ) {
					this.isInitialized = true;
					registerReloadedEntity( rowProcessingState, existingEntity );
					notifyResolutionListeners( entityInstance );
				}
			}
			else if ( entityInstanceFromExecutionContext != null ) {
				entityInstance = entityInstanceFromExecutionContext;
				registerLoadingEntityInstanceFromExecutionContext( rowProcessingState, entityInstance );
			}
			else {
				// look to see if another initializer from a parent load context or an earlier
				// initializer is already loading the entity
				entityInstance = resolveInstance( entityIdentifier, existingLoadingEntry, rowProcessingState );
				if ( isOwningInitializer && !isInitialized && identifierAssembler instanceof EmbeddableAssembler ) {
					// If this is the owning initializer and the returned object is not initialized,
					// this means that the entity instance was just instantiated.
					// In this case, we want to call "assemble" and hence "initializeInstance" on the initializer
					// for possibly non-aggregated identifier mappings, so inject the virtual id representation
					identifierAssembler.assemble( rowProcessingState );
				}
			}

			upgradeLockMode( rowProcessingState );
		}
	}

JPA requires that within a persistence context, there is only a single object for an entity i.e. there are no two objects in the object graph that point to the same row.

entityInstanceFromExecutionContext is something that is being loaded as part of “the current load operation”, which means that even if a proxy is created at some point while processing a row in that load operation, it can still be resolved to the real instance if it turns out to be loaded at a later stage of processing the row.

Ultimately, this is just what the managed persistence context implies. In any case, using fields directly is not supported, unless you enable extended enhancement, which is enabled by default in Quarkus.