Instance method always triggers Parent entity load

Thanks for the help understanding the behavior in my test case: Help writing test case for lazy loading - #5 by kevinm416 . Now that I understand the baseline behavior I can ask my real question.

I have Parent + Child entities and a ParentId typed id that I want to use to wrap Parent’s id for type safety.

I’ve updated my test case: Lazy-loading issue by kevinm416 · Pull Request #607 · hibernate/hibernate-test-case-templates · GitHub .

@Entity
public class Child {

  @Id
  @GeneratedValue
  long id;

  @TenantId
  @Column(name = "tenant_id", nullable = false, updatable = false)
  private Long tenantId;

  @ManyToOne(fetch = FetchType.LAZY, optional = false)
  @JoinColumn(name = "parent_id", nullable = false)
  public Parent parent;

  ...
@Entity
public class Parent {

  @Id
  @GeneratedValue
  long id;

  @Column
  String name;

  @TenantId
  @Column(name = "tenant_id", nullable = false, updatable = false)
  private Long tenantId;

  public long getId() {
    return id;
  }

  public ParentId getTypedId() {
    return new ParentId(getId());
  }
}
public class ParentId {
  private final long id;
  public ParentId(long id) {
    this.id = id;
  }
}

The test loads a Child, and then accesses child.getParent().getTypedId():

@Test
void hhh123Test() throws Exception {
  EntityManager entityManager = entityManagerFactory.createEntityManager();
  entityManager.getTransaction().begin();

   Parent parent = new Parent();
   entityManager.persist(parent);

   Child child = new Child().setParent(parent);
   entityManager.persist(child);

   entityManager.getTransaction().commit();
   entityManager.close();

   // LOAD ------------------------------------------------------------------
   EntityManager entityManager2 = entityManagerFactory.createEntityManager();
   entityManager2.getTransaction().begin();

   Child loaded = entityManager2.find(Child.class, child.getId());

   System.out.println();

   ParentId parentId = loaded.getParent().getTypedId(); // <---- Parent entity sql load
   System.out.println(parentId);

   entityManager2.getTransaction().commit();

   entityManager2.close();
 }

I’ve tried various different variations, like Hibernate annotations on methods vs fields and accessing the id field directly vs using getId(). In all situations that I tried, calling getTypedId() loads the Parent entity, and calling getId() does not.

Hibernate: 
    select
        c1_0.id,
        c1_0.parent_id,
        c1_0.tenant_id 
    from
        Child c1_0 
    where
        c1_0.id=? 
        and c1_0.tenant_id = ?

Hibernate: 
    select
        p1_0.id,
        p1_0.name,
        p1_0.tenant_id 
    from
        Parent p1_0 
    where
        p1_0.id=? 
        and p1_0.tenant_id = ?

Is there a way I can avoid this load and keep my getTypedId() method?

It seems like this should be possible, since the instance method getTypedId() is equivalent to this static method, which does not load the Parent from the DB:

public static ParentId getParentId(Parent parent) {
  return new ParentId(parent.get());
}

You might be able to do this by marking the method as final, but if that doesn’t work, you’d need to use bytecode enhancement to weave in the lazy loading aspects, so that field accesses themselves cause lazy loading instead of a proxy class method call.

Are there any resources or documentation that explains why marking the method final might work, or what lazy loading aspects are and how they work? I feel like if I had more info I could debug what is happening. Right now I’m just guessing and checking by running a unit test.

Hibernate ORM creates a proxy class that extends your entity class to support lazy loading. While doing that, it intercepts every method call to trigger lazy initialization and delegate the call to the actual entity instance. I don’t know if the documentation has a section that explains this, but the bytecode enhancement section goes into the details of the alternative model to support lazy loading.

If the method is marked as final, Hibernate ORM obviously can’t override the method (Java language rules) in a runtime generated proxy class and hence, it will not trigger lazy initialization.