I have a Person entity with a @ManyToOne self-relationship named "manager". In most cases, I do not need to know the Person's manager, so I've marked the relationship with Fetch.LAZY. However, in every case where I retrieve an entity with a Person field, JPA (with Hibernate Provider) loads the referenced Person...and that person's manager...and that person's manager, and so on recursively, until the entire management chain is loaded. All those queries are executed before TypedQuery.getResultList() returns control to my code, so it's not a matter of me inadvertently referencing the manager somewhere in the session and triggering a true lazy load. It appears the JPA fetch plan has determined that the whole management chain is necessary for some reason and loads them all.
I'm using JPA persistence-api 2.2 and hibernate-core 5.4.20
Here's an abbreviation of the Person class:
@Entity
@IdClass(BaseTenantKey.class)
public class Person {
@Id
@Column(name = "id", nullable = false, updatable = false, columnDefinition = ENTITY_ID_DEF)
private String id;
@Id
@Column(name = "tid", nullable = false, updatable = false, columnDefinition = TENANT_ID_DEF)
private String tid = "default";
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumnsOrFormulas(value = {
@JoinColumnOrFormula(formula = @JoinFormula(value = "tid", referencedColumnName = "tid")),
@JoinColumnOrFormula(column = @JoinColumn(name = "managerid", referencedColumnName = "id"))
})
@NotFound(action = NotFoundAction.IGNORE)
private Person manager;
...
}
@Embeddable
public class BaseTenantKey implements Serializable {
@Column(name = "id", nullable = false, updatable = false, columnDefinition = ENTITY_ID_DEF)
private String id;
@Column(name = "tid", nullable = false, updatable = false, columnDefinition = TENANT_ID_DEF)
private String tid;
}
An example entity with Person fields is the Campaign entity:
@Entity
public class Campaign {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "UUIDGenerator")
@Column(name = "id", nullable = false, updatable = false, columnDefinition = ENTITY_ID_DEF)
private String id;
@Column(name = "tid", columnDefinition = TENANT_ID_DEF)
private String tid = "default";
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumnsOrFormulas(value = {
@JoinColumnOrFormula(formula = @JoinFormula(value = "tid", referencedColumnName = "tid")),
@JoinColumnOrFormula(column = @JoinColumn(name = "created_by", referencedColumnName = "id",
nullable = false, updatable = false, columnDefinition = ENTITY_ID_DEF))
})
private Person createdBy;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumnsOrFormulas(value = {
@JoinColumnOrFormula(formula = @JoinFormula(value = "tid", referencedColumnName = "tid")),
@JoinColumnOrFormula(column = @JoinColumn(name = "owner", referencedColumnName = "id", nullable = false,
columnDefinition = ENTITY_ID_DEF, foreignKey = @ForeignKey(name = "fk_campaign_person_owner")))
})
private Person owner;
...
}
In the case of a Campaign fetch, I know the createdBy and owner fields will be referenced, so I fetch them eagerly with an entity graph:
final EntityGraph<Campaign> graph = getEntityManager().createEntityGraph(Campaign.class);
graph.addAttributeNodes(Campaign_.CREATED_BY, Campaign_.OWNER);
...
final TypedQuery<E> typedQuery = getEntityManager().createQuery(filterQuery);
typedQuery.setHint(GraphSemantic.FETCH.getJpaHintName(), graph);
typedQuery.setHint(QueryHints.HINT_READONLY, readOnly);
final List<E> result = typedQuery.getResultList();
I can see this has the intended result in that the generated SQL joins in my created_by and owner fields:
select
campaign0_.id as id1_0_0_,
person1_.id as id1_35_1_,
person1_.tid as tid2_35_1_,
person2_.id as id1_35_2_,
person2_.tid as tid2_35_2_,
campaign0_.created_at as created_2_0_0_,
campaign0_.modified_at as modified3_0_0_,
campaign0_.tid as tid4_0_0_,
campaign0_.created_by as created30_0_0_,
campaign0_.version as version5_0_0_,
...
campaign0_.tid as formula58_0_,
campaign0_.tid as formula59_0_,
person1_.created_at as created_3_35_1_,
person1_.modified_at as modified4_35_1_,
...
person1_.tid as formula44_1_,
person2_.created_at as created_3_35_2_,
person2_.modified_at as modified4_35_2_,
...
person2_.tid as formula44_2_
from
campaign campaign0_
left outer join
person person1_
on campaign0_.created_by=person1_.id
and campaign0_.tid=person1_.tid
left outer join
person person2_
on campaign0_.owner=person2_.id
and campaign0_.tid=person2_.tid limit ?
Now here is where the problem presents itself. If I turn on DEBUG for org.hibernate.internal.TwoPhaseLoad, I see that the createdBy and owner fields both resolve to the Person object with id=CF21FF65-3A8C-40AB-BFAC-A7FD197C9512, and according to the debug, the Person object they both point to is eagerly fetched (as expected) with an entity graph override. However, when Hibernate starts processing that object, it finds a manager reference (id=01ACB688-D415-42C3-81E7-91EEBBF26535) and immediately issues a query with without the explanatory text noted above :
o.h.engine.internal.TwoPhaseLoad : Resolving attributes for [com.company.domain.Campaign#79670EF1-872B-49D7-ACAC-67DA188249CA]
o.h.engine.internal.TwoPhaseLoad : Processing attribute `tid` : value = default
o.h.engine.internal.TwoPhaseLoad : Attribute (`tid`) - enhanced for lazy-loading? - false
o.h.engine.internal.TwoPhaseLoad : Processing attribute `createdBy` : value = BaseTenantKey(id=CF21FF65-3A8C-40AB-BFAC-A7FD197C9512, tid=default)
o.h.engine.internal.TwoPhaseLoad : Attribute (`createdBy`) - enhanced for lazy-loading? - false
o.h.engine.internal.TwoPhaseLoad : Overriding eager fetching using fetch graph. EntityName: com.company.domain.Campaign, associationName: createdBy, eager fetching: true
o.h.engine.internal.TwoPhaseLoad : Processing attribute `version` : value = 197
o.h.engine.internal.TwoPhaseLoad : Attribute (`version`) - enhanced for lazy-loading? - false
o.h.engine.internal.TwoPhaseLoad : Processing attribute `owner` : value = BaseTenantKey(id=CF21FF65-3A8C-40AB-BFAC-A7FD197C9512, tid=default)
o.h.engine.internal.TwoPhaseLoad : Attribute (`owner`) - enhanced for lazy-loading? - false
o.h.engine.internal.TwoPhaseLoad : Overriding eager fetching using fetch graph. EntityName: com.company.domain.Campaign, associationName: owner, eager fetching: true
o.h.engine.internal.TwoPhaseLoad : Done materializing entity [com.company.domain.Campaign#79670EF1-872B-49D7-ACAC-67DA188249CA]
o.h.engine.internal.TwoPhaseLoad : Resolving attributes for [com.company.domain.Person#component[id,tid]{id=CF21FF65-3A8C-40AB-BFAC-A7FD197C9512, tid=default}]
o.h.engine.internal.TwoPhaseLoad : Processing attribute `manager` : value = BaseTenantKey(id=01ACB688-D415-42C3-81E7-91EEBBF26535, tid=default)
o.h.engine.internal.TwoPhaseLoad : Attribute (`manager`) - enhanced for lazy-loading? - false
org.hibernate.SQL :
select
person0_.id as id1_35_0_,
person0_.tid as tid2_35_0_,
...
person0_.tid as formula44_0_
from
person person0_
where
person0_.id=?
and person0_.tid=?
o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [VARCHAR] - [01ACB688-D415-42C3-81E7-91EEBBF26535]
o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [VARCHAR] - [default]
In similar fashion, the three layers of management above Person (id=01ACB688-D415-42C3-81E7-91EEBBF26535) are also fetched before the TypedQuery.getResultList() is returned.
MY QUESTION IS THIS:
Why would an entity declared to be Fetch.LAZY be retrieved when it is not included in the entity graph applied with a GraphSemantic.FETCH?
NOTE:
If I ditch the EntityGraph, and just let things ride according to the JPA entity annotations, I get pretty much the same result, except an additional query for Person (id=CF21FF65-3A8C-40AB-BFAC-A7FD197C9512) immediately after the Campaign query. Similar to the above question, the two Campaign references to Person are both lazy, so why are they being fetched?