javahibernatespring-data-jpahibernate-mappingentitygraph

Use EntityGraph for query with optional ManyToOne relationship


I've got a Hibernate entity with an optional relationship to another entity:

@NamedEntityGraph(
    name = "expense-for-frontend",
    attributeNodes = {
      @NamedAttributeNode("shipment"),
    })
@Entity
public class Expense {
  ...
  @ManyToOne(fetch = FetchType.LAZY, optional = true)
  private Shipment shipment;
  ...
}

I want to be able to load Expense's regardless of whether they have Shipment's, but if they do have a Shipment it should come back pre-loaded to avoid DB round trips. The repository has a query along the lines of:

  @EntityGraph("expense-for-frontend")
  @Query(
      """
      SELECT e
      FROM Expense e
      LEFT JOIN Shipment s ON e.shipment.id = s.id
      WHERE (:status IS NULL OR e.status = :status)
      """)
  List<PaidShipmentExpenseBE> findAllFilteredShipmentExpensesWithPayment(
      @Param("status") @Nullable VendorPaymentStatus status);

The SQL this executes is:

select eb1_0.id, ...
from expense eb1_0
join shipment s1_0 on s1_0.id=eb1_0.shipment_id 
left join shipment sb1_0 on sb1_0.tenant_id = ? and eb1_0.shipment_id=sb1_0.id
...

The inner join against shipment is added by the @NamedAttributeNode("shipment") in the EntityGraph, and s1_0 is not used in the rest of the query. Because it adds an inner join, I can't retrieve any Expenses that are not linked to Shipments while using that EntityGraph.

I can't find any way to change the EntityGraph to allow for an optional ManyToOne relationship.

This seems like it would a very common situtation, and the nullability of the column is exposed via the standard jakarta persistence annotation @ManyToOne(fetch = FetchType.LAZY, optional = true), so I would assume that an EntityGraph would handle this situation correctly. Am I missing something?


Solution

  • You have an error in your query. The relationship LEFT JOIN Shipment s ON e.shipment.id = s.id in your query is redundant. You have already described the relationship between the Expense and Shipment entities using the @ManyToOne annotation, so Hibernate makes the first join. By adding another relationship to the query, you forced Hibernate to make another join. But to avoid accessing NULL in the next join, it had to get s1_0 using an inner join, and not the usual left join. So just remove LEFT JOIN Shipment s ON e.shipment.id = s.id from your query.

    Also, in the @ManyToOne(fetch = FetchType.LAZY, optional = true) expression, optional parameter is true by default, so it is not necessary to specify it.