Native query failing with JoinFormula in parent entity

I have a parent entity that needs to have a reference to the latest version of a child record.

To achieve this I added a lazy @ManyToOne property with an @JoinFormula that returns the latest associated child record.

When I attempt a native query for the entity I receive the exception shown below.

Hibernate Version: 5.6.5
Java 15
Spring Data 2.6.1

Why is it trying to populate a lazy loaded property? Is it a bug or am I doing something wrong?

java.lang.NullPointerException: Cannot invoke "String.toLowerCase(java.util.Locale)" because "columnName" is null
	at org.postgresql.jdbc.PgResultSet.findColumnIndex(PgResultSet.java:2939)
	at org.postgresql.jdbc.PgResultSet.findColumn(PgResultSet.java:2898)
	at org.postgresql.jdbc.PgResultSet.getObject(PgResultSet.java:2892)
	at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
	at org.hibernate.type.PostgresUUIDType$PostgresUUIDSqlTypeDescriptor$2.doExtract(PostgresUUIDType.java:89)
	at org.hibernate.type.descriptor.sql.BasicExtractor.extract(BasicExtractor.java:47)
	at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:257)
	at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:253)
	at org.hibernate.type.AbstractStandardBasicType.nullSafeGet(AbstractStandardBasicType.java:243)
	at org.hibernate.type.AbstractStandardBasicType.hydrate(AbstractStandardBasicType.java:329)
	at org.hibernate.type.ManyToOneType.hydrate(ManyToOneType.java:184)
	at org.hibernate.persister.entity.AbstractEntityPersister.hydrate(AbstractEntityPersister.java:3214)
	at org.hibernate.loader.Loader.loadFromResultSet(Loader.java:1887)
	at org.hibernate.loader.Loader.hydrateEntityState(Loader.java:1811)
	at org.hibernate.loader.Loader.instanceNotYetLoaded(Loader.java:1784)
	at org.hibernate.loader.Loader.getRow(Loader.java:1624)
	at org.hibernate.loader.Loader.getRowFromResultSet(Loader.java:748)
	at org.hibernate.loader.Loader.getRowsFromResultSet(Loader.java:1047)
	at org.hibernate.loader.Loader.processResultSet(Loader.java:998)
	at org.hibernate.loader.Loader.doQuery(Loader.java:967)
	at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:357)
	at org.hibernate.loader.Loader.doList(Loader.java:2868)
	at org.hibernate.loader.Loader.doList(Loader.java:2850)
	at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2682)
	at org.hibernate.loader.Loader.list(Loader.java:2677)
	at org.hibernate.loader.custom.CustomLoader.list(CustomLoader.java:338)
	at org.hibernate.internal.SessionImpl.listCustomQuery(SessionImpl.java:2181)
	at org.hibernate.internal.AbstractSharedSessionContract.list(AbstractSharedSessionContract.java:1204)
	at org.hibernate.query.internal.NativeQueryImpl.doList(NativeQueryImpl.java:177)
	at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1617)
	at org.hibernate.query.Query.getResultList(Query.java:165)
	at xx.xxx.xxx.assets.ui.api.insights.data.jpa.dao.AssetInsightCustomRepositories$AssetInsightCustomRepositoryImpl.findByTenantId(AssetInsightCustomRepositories.java:262)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:564)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:529)
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:639)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:163)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:138)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:80)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy184.findByTenantId(Unknown Source)
	at xxx.xxx.xxx.assets.ui.api.insights.service.InsightServices$AssetInsightActionServiceImpl.findByTenantId(InsightServices.java:418)
	at xxx.xxx.xxx.assets.ui.api.insights.service.InsightServices$AssetInsightActionServiceImpl$$FastClassBySpringCGLIB$$a8c13b21.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
	at xxx.xxx.xxx.assets.ui.api.insights.service.InsightServices$AssetInsightActionServiceImpl$$EnhancerBySpringCGLIB$$51310287.findByTenantId(<generated>)
	at xxx.xxx.xxx.assets.ui.api.insights.service.InsightServicesIT.testFindAssetInsightsByTenantList(InsightServicesIT.java:656)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:564)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:725)
	at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
	at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
	at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
	at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:214)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:210)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
	at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)

The query:

    public List<AssetInsightEntity> findByTenantId(final String tenantId) {

            var findByTenantId = """

                    SELECT
                      asset_insight.*
                    FROM
                      asset_insight
                      JOIN asset USING (asset_id)
                    WHERE
                      asset.tenant_id = :tenantId
                    """;

            return entityManager().createNativeQuery(findByTenantId, AssetInsightEntity.class)
                    .setParameter(TENANT_ID, tenantId)
                    .getResultList();
        }

In the org.hibernate.loader.custom.CustomLoader.loadFromResultSet method
cols = [[], [asset_id], [insight_id], [null]]

	//This is not very nice (and quite slow):
		final String[][] cols = persister == rootPersister ?
				getEntityAliases()[i].getSuffixedPropertyAliases() :
				getEntityAliases()[i].getSuffixedPropertyAliases( persister );

		final Object[] values = persister.hydrate(
				rs,
				id,
				object,
				rootPersister,
				cols,
				fetchAllPropertiesRequested,
				getPerPropertyEagerFetchEnabled( i ),
				session

Parent Entity

package xxx.xxx.xxx.assets.ui.api.insights.data.jpa.entity;

import static lombok.AccessLevel.*;

import java.util.List;
import java.util.UUID;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;

import xxx.xxx.xxx.assets.ui.api.assets.data.jpa.entity.AssetEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.JoinColumnOrFormula;
import org.hibernate.annotations.JoinFormula;
import org.hibernate.annotations.OrderBy;
import org.hibernate.annotations.Where;

/**
 * The Class AssetInsightEntity.
 */

@Entity(name = "asset_insight")
@Table(name = "asset_insight")
@Builder(toBuilder = true)
@AllArgsConstructor(access = PUBLIC)
@NoArgsConstructor(access = PROTECTED)
@Setter(value = PUBLIC)
@Getter
@ToString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class AssetInsightEntity {

    /** The insights id. */
    @Id
    @Column(nullable = false)
    @EqualsAndHashCode.Include
    UUID assetInsightId;

    /** The asset */
    @OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH },
        optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "asset_id")
    AssetEntity asset;

    /** The insight id. */
    @OneToOne(cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.DETACH },
        optional = false, fetch = FetchType.EAGER)
    @JoinColumn(name = "insight_id")
    InsightEntity insight;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "asset_insight_id")
    @Where(clause = "deleted_ts IS NULL")
    @OrderBy(clause = "actioned_ts DESC")
    @ToString.Exclude
    List<AssetInsightActionEntity> actions;

    @ManyToOne(fetch = FetchType.LAZY, targetEntity = AssetInsightActionEntity.class)
    @JoinColumnOrFormula(formula = @JoinFormula(value = """
            (SELECT
              lastAction.asset_insight_action_id
             FROM
               asset_insight_action lastAction
             WHERE
               lastAction.asset_insight_id = asset_insight_id
               AND lastAction.deleted_ts IS NULL
             ORDER BY
              lastAction.actioned_ts DESC
             LIMIT 1)
            """))
    @ToString.Exclude
    AssetInsightActionEntity lastAction;

}

Child Entity:

package xxx.xxx.xxx.assets.ui.api.insights.data.jpa.entity;

import static java.util.Comparator.*;
import static lombok.AccessLevel.*;

import java.time.OffsetDateTime;
import java.util.Comparator;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import xxx.xxx.xxx.assets.ui.api.insights.InsightActions;
import xxx.xxx.xxx.assets.ui.api.insights.InsightResolutionReason;
import com.vladmihalcea.hibernate.type.basic.PostgreSQLEnumType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;

/**
 * The Class AssetInsightActionEntity.
 */
@Entity(name = "asset_insight_action")
@Table(name = "asset_insight_action")
@Builder(toBuilder = true)
@AllArgsConstructor(access = PUBLIC)
@NoArgsConstructor(access = PROTECTED)
@Setter(value = PUBLIC)
@Getter
@ToString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@TypeDef(name = "pgsql_enum", typeClass = PostgreSQLEnumType.class)
public class AssetInsightActionEntity {

    /** The insights id. */
    @Id
    @Column(nullable = false)
    @EqualsAndHashCode.Include
    UUID assetInsightActionId;

    /** The insight id. */
    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @JoinColumn(name = "asset_insight_id", nullable = false)
    @ToString.Exclude
    AssetInsightEntity assetInsight;

    /** The action state */
    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    @Type(type = "pgsql_enum")
    InsightActions actionState;

    /** The actioned by. */
    @Column(nullable = false)
    UUID actionedBy;

    @Column(nullable = false, name = "actioned_ts")
    OffsetDateTime actionedTs;

    /** The reason. */
    @Column
    @Enumerated(EnumType.STRING)
    @Type(type = "pgsql_enum")
    InsightResolutionReason resolutionReason;

    /** The notes. */
    @Column
    String notes;

    /** The date action. */
    @Column(name = "deleted_ts")
    OffsetDateTime deletedTs;
}

Hibernate must somehow decide if this lazy property should be set to null or a proxy, so it requires a value for the join formula. In your native query you could just add null as lastAction I think.

Added null AS lastAction, same error.
Tried null AS "lastAction", same error.

   @Override
        public List<AssetInsightEntity> findByTenantId(final String tenantId) {

            var findByTenantId = """

                    SELECT
                      asset_insight.*
                      ,null AS lastAction
                    FROM
                      asset_insight
                      JOIN asset USING (asset_id)
                    WHERE
                      asset.tenant_id = :tenantId
                    """;

            return entityManager().createNativeQuery(findByTenantId, AssetInsightEntity.class)
                    .setParameter(TENANT_ID, tenantId)
                    .getResultList();
        }

Hmm, I think in 5.x you might have to use the same alias that Hibernate expects internally. Can you try that? The exception should tell you the alias that is missing.

The stacktrace in the original had a typo. The column name is null.

java.lang.NullPointerException: Cannot invoke "String.toLowerCase(java.util.Locale)" because "columnName" is null

I think the root issue is here:

			// temporary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
			boolean lazy = ! EnhancementHelper.includeInBaseFetchGroup(
					prop,
					bytecodeEnhancementMetadata.isEnhancedForLazyLoading(),
					(entityName) -> {
						final MetadataImplementor metadata = creationContext.getMetadata();
						final PersistentClass entityBinding = metadata.getEntityBinding( entityName );
						assert entityBinding != null;
						return entityBinding.hasSubclasses();
					},
					sessionFactoryOptions.isCollectionsInDefaultFetchGroupEnabled()
			);

Even for LAZY loaded collections it is reporting them as not LAZY.

Which seems to work “fine” as those have an explicit join column.

@JoinFormula does not have and explicit join column.

Am I encountering this issue [HHH-14500] - Hibernate JIRA?

Lazy in this context means that the field is lazily set, so no, that is not the issue that you are running into. Your problem simply is that Hibernate tries to fetch the FK for the association, but it seems that it is unable to for some reason. Can you share the full stack trace maybe?

Full stack trace is in the OP.

I think the root issue is that non-enhanced entities will always return true and be included in the base fetch, which is where the issue lies as it means the EntityMetamodel always assigns false to ALL elements in the propertyLaziness array.

AbstractEntityPersister.hydrate uses EntityMetamodel's propertyLaziness to determine which columns to reference in the resultset. Since lastAction doesn’t have a column name null is sent in the col list.


/**
	 * Should the given property be included in the owner's base fetch group?
	 */
	public static boolean includeInBaseFetchGroup(
			Property bootMapping,
			boolean isEnhanced,
			InheritanceChecker inheritanceChecker,
			boolean collectionsInDefaultFetchGroupEnabled) {
		final Value value = bootMapping.getValue();

		if ( ! isEnhanced ) {
			if ( value instanceof ToOne ) {
				if ( ( (ToOne) value ).isUnwrapProxy() ) {
					BytecodeInterceptorLogging.MESSAGE_LOGGER.debugf(
							"To-one property `%s#%s` was mapped with LAZY + NO_PROXY but the class was not enhanced",
							bootMapping.getPersistentClass().getEntityName(),
							bootMapping.getName()
					);
				}
			}
			return true;
		}

As I suspected the issue is in boolean includeInBaseFetchGroup.

If I:

  • add the hibernate-enhance-maven-plugin with enableLazyInitialization
  • add a org.eclipse.m2e.lifecycle-mapping entry to my pom (so eclipse will run the plugin)
  • add @NotFound(action = NotFoundAction.IGNORE)
  • add @LazyToOne(LazyToOneOption.NO_PROXY)

It works as expected.

This seems unnecessary (and I think undocumented). Further since we use mapstruct for mapping Jpa Entities to DTOs while in Eclipse we have to explicitly recompile the project whenever we make a change to an entity, yuck.

In my opinion includeInBaseFetchGroup when not enhanced should check if the property was marked as lazy and/or check if the property is the result of JoinFormula without a column reference and if so return ‘false’.


    @ManyToOne(fetch = FetchType.LAZY, targetEntity = AssetInsightActionEntity.class)
    @JoinColumnOrFormula(formula = @JoinFormula(value = """
            (SELECT
            lastAction.asset_insight_action_id
            FROM
            asset_insight_action lastAction
            WHERE
            lastAction.asset_insight_id = asset_insight_id
            AND lastAction.deleted_ts IS NULL
            ORDER BY
            lastAction.actioned_ts DESC
            LIMIT 1)
            """))
    @ToString.Exclude
    @NotFound(action = NotFoundAction.IGNORE)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    AssetInsightActionEntity lastAction;

Good that you found a workaround for your use case, but note that the underlying problem really just is that the column name for a formula should be something other than null, which I’m pretty sure was fixed in Hibernate 6.0. I would appreciate if you could create a Hibernate issue and maybe even a PR with a test and fix.

Apart from this, I can recommend that you take a look into Blaze-Persistence Entity-Views for this use case which has first class support for use cases like this. Your DTOs could look like the following:

@EntityView(AssetInsightEntity.class)
public interface AssetInsightDto {
    @IdMapping
    UUID getAssetInsightId();
    @Mapping("actions[deletedTs IS NULL]")
    @Limit(limit = "1", order = "actionedTs DESC")
    AssetInsightActionDto getLastAction();
}
@EntityView(AssetInsightActionEntity.class)
public interface AssetInsightActionDto {
    @IdMapping
    UUID getAssetInsightActionId();
}

Which you can fetch like this

public List<AssetInsightDto> findByTenantId(final String tenantId) {
    var queryBuilder = criteriaBuilderFactory.create(entityManager(), AssetInsightEntity.class)
            .where("asset.tenantId").eq(tenantId);

    return entityViewManager.applySetting(EntityViewSetting.create(AssetInsightDto.class), queryBuilder)
            .getResultList();
}

And it will produce better SQL i.e. a lateral join for your case

SELECT
  asset_insight.*
  ,null AS lastAction
FROM
  asset_insight ai
  JOIN asset a ON ai.asset_id = a.asset_id
  LEFT JOIN LATERAL (
    SELECT
      aia.asset_insight_action_id
    FROM
      asset_insight_action aia
    WHERE
      aia.asset_insight_id = ai.asset_insight_id
      AND aia.deleted_ts IS NULL
    ORDER BY
      aia.actioned_ts DESC
    LIMIT 1
  ) lastAction(assetInsightActionId)
WHERE
  asset.tenant_id = :tenantId