How to Prevent Lazy Relationship From Loading

Viewed 1161

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?

2 Answers

I'm a schmuck. The answer was right there in the logs:

WARN 2170 --- org.hibernate.cfg.AnnotationBinder       : HHH000491: The [manager] association in the [demo.Person] entity uses both @NotFound(action = NotFoundAction.IGNORE) and FetchType.LAZY. The NotFoundAction.IGNORE @ManyToOne and @OneToOne associations are always fetched eagerly.

Removing the @NotFound annotation fixed the problem.

This could be an issue with the id class support or your use of @JoinFormula. Try using embeddable ids instead:

@Entity
public class Person {

    @EmbeddedId
    private BaseTenantKey id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(value = "tid", insertable = false, updatable = false, referencedColumnName = "tid"),
            @JoinColumn(name = "managerid", insertable = false, updatable = false, 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;
}

@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)
    @JoinColumns({
            @JoinColumn(value = "tid", insertable = false, updatable = false, referencedColumnName = "tid"),
            @JoinColumn(name = "created_by", referencedColumnName = "id",
                    nullable = false, updatable = false, columnDefinition = ENTITY_ID_DEF)
    })
    private Person createdBy;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumns({
            @JoinColumn(value = "tid", insertable = false, updatable = false, referencedColumnName = "tid")),
            @JoinColumn(name = "owner", referencedColumnName = "id", nullable = false,
                    columnDefinition = ENTITY_ID_DEF, foreignKey = @ForeignKey(name = "fk_campaign_person_owner"))
    })
    private Person owner;

    ...
}
Related