EntityGraph integration for non-entities at the root level

I’ve been using EntityGraph extensively across my app to prevent N+1 queries, and it’s working well. I’ve even implemented my own repository for supplying EntityGraph’s at runtime to keep repository method counts low:

Page<T> findWithEntityGraph(
      Specification<T> spec, EntityGraphReference<T> entityGraph, Pageable pageable);

I’ve got a performance issue with one of my entities and a @Formula on one of it’s fields. I’m following the standard advice refactoring a DTO object I can use to load the base entity without the expensive field alongside the expensive field when I need it:

  @Query(
      """
          SELECT new com.example.VendorPaymentFinancials(
              vp,
              COALESCE((SELECT SUM(se.paymentApplied)
                        FROM ShipmentExpenseBE se
                        WHERE se.vendorPayment IN (ids), 0)
          )
          FROM VendorPaymentBE vp
          WHERE vp.id IN :ids
          """)
  List<VendorPaymentFinancials> findFinancialsByIds(@Param("ids") Collection<Long> ids);

The issue is that all the infrastructure I’ve built up around EntityGraphs doesn’t seem to be able to work as soon as I wrap the entity in the VendorPaymentFinancials record. EntityGraphs have become so important that I feel really hamstrung that I can’t use them for this type of query.

Is there anything that I’m missing here? How can I both deal with this common performance problem and continue to use EntityGraph’s?

Are you saying that the entity graph is not made use of when the entity to which it applies is passed as argument to a constructor via select new? What version of Hibernate ORM are you using? Can you 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.

Hi @beikov,

I’m leaning heavily on AI for explaining hibernate behavior (not my usage of Hibernate) since I’m not familiar with hibernate source, but this is fairly convincing explanation of the issue to me. I’ll try to confirm via a test case though.

  Actual Proof From Hibernate 6.6.49.Final Source
  
  How entity graph traversal works for a normal entity query

  The entity graph state lives in BaseSqmToSqlAstConverter (:476):
  private final EntityGraphTraversalState entityGraphTraversalState;
  
  It's initialized from queryOptions.getAppliedGraph() at constructor time (:583-596). It's then consumed in the fetch-building pipeline around line 8435:

  if ( entityGraphTraversalState != null ) {
      traversalResult = entityGraphTraversalState.traverse(
              fetchParent,
              fetchable,
              isKeyFetchable
      );
      // ... sets fetchTiming and joined based on graph
  }

  That code path runs when Hibernate is building Fetch nodes on top of a FetchParent — i.e., when a returned entity result has associations that need their fetch strategy decided.

  What visitDynamicInstantiation actually does

  BaseSqmToSqlAstConverter.java:1642:
  
  public DynamicInstantiation<?> visitDynamicInstantiation(SqmDynamicInstantiation<?> sqmDynamicInstantiation) {
      // ...
      for ( SqmDynamicInstantiationArgument<?> sqmArgument : sqmDynamicInstantiation.getArguments() ) {
          final SqmSelectableNode<?> selectableNode = sqmArgument.getSelectableNode();
          if ( selectableNode instanceof SqmPath<?> ) {
              prepareForSelection( (SqmPath<?>) selectableNode );
          }
          final DomainResultProducer<?> argumentResultProducer =
              (DomainResultProducer<?>) sqmArgument.accept( this );

          dynamicInstantiation.addArgument( sqmArgument.getAlias(), argumentResultProducer, this );
      }
      dynamicInstantiation.complete();
      return dynamicInstantiation;
  }

  entityGraphTraversalState is never referenced in this method. For each constructor argument, Hibernate calls prepareForSelection (which only handles table group registration and join types — no graph logic)
  and then resolves the argument's DomainResultProducer. The graph traversal that decides whether lazy associations become fetch joins is completely bypassed.

  Why those are the two different worlds

  The fetch-building pipeline (line 8435) is only reached when iterating the fetchables of a FetchParent — an entity that Hibernate is hydrating as a managed result. In a constructor expression, vp is a
  constructor argument, not a top-level entity result. Hibernate resolves it as a DomainResultProducer via sqmArgument.accept(this), which is a fundamentally different code path that produces column values for
  the constructor call, not a managed entity with an attached fetch tree.
  
  There is no path from visitDynamicInstantiation → entityGraphTraversalState.traverse(). The graph hint is initialized but never reached for the constructor arguments.

Well, that explanation is not correct. When creating the DomainResult, it will create the fetches for associations internally via org.hibernate.sql.results.graph.AbstractFetchParent#afterInitialize, which is where the entity graph will apply.

Since this is all very hand wavy, I need a reproducer to see what is actually going on. When you provide that, I can take a look.