1

I have the following entities:

class A {
  @Id
  String aId;
  @OneToMany(fetch = FetchType.LAZY)
  List<B> bsOfA
  @OneToMany(fetch = FetchType.LAZY)
  List<C> csOfA
  
  public A(String id) {
    this.aId = id;
  }
}
class B {
  @Id
  String aId;
  @Id
  String bId;
  @ManyToOne(fetch = FetchType.LAZY)
  A aOfB;
  @ManyToOne(fetch = FetchType.LAZY)
  C cOfB                 // this is some member of csOfA

  public B(String id, B sourceB) {
    this.aId = id;
    this.bId = sourceB.bId;
  }
}
class C {
  @Id
  String aId;
  @Id
  String cId;
  @OneToMany(fetch = FetchType.LAZY)
  A aOfC;
  @ManyToOne(fetch = FetchType.LAZY)
  List<B> linkedBsToC;
  public C(String id, C sourceC) {
    this.aId = id;
    this.cId = sourceC.cId;
  }
}

Now I want to create a copy of A so I proceed as follows

A sourceA = aRepo.getbyId(sourceAId);
A copyA = new A(newAId);

List<B> bsOfA = new ArrayList<>(sourceA.getBsOfA.size());
sourceA.getBsOfA().forEach(b -> bsOfA.add(new B(newAId, b));
copyA.setBsOfA(bsOfA)

List<C> csOfA = new ArrayList<>(sourceA.getCsOfA.size());
// problematic part
sourceA.getCsOfA().forEach(c -> csOfA.add(new C(newAId, c));  
copyA.setCsOfA(csOfA)

Now the issue is: during this copy operation, I am observing that when constructor of C is called, the input c is a hibernate_interceptor object that has null attributes (like in this other post: Data inside hibernate interceptor object but null under entity variables - Can't save to repository).

To remedy this situation, I have 3 choices:

  1. Swap the position of initializing copyA's bsOfA and csOfA:
List<C> csOfA = new ArrayList<>(sourceA.getCsOfA.size());
sourceA.getCsOfA().forEach(c -> csOfA.add(new C(newAId, c));  
copyA.setCsOfA(csOfA)

List<B> bsOfA = new ArrayList<>(sourceA.getBsOfA.size());
sourceA.getBsOfA().forEach(b -> bsOfA.add(new B(newAId, b));
copyA.setBsOfA(bsOfA)
  1. Set FetchType.EAGER for cOfB
  2. Modify the constructors
  public C(String id, C sourceC) {
    this.aId = id;
    this.cId = sourceC.getCId();   // How is this different from sourceC.id ?!
  }

Can someone please enlighten me on this issue? I will most definitely opt for #3 because it seems the easiest to adjust but I still can't figure out what is wrong with the original code. Many thanks in advance :)

5
  • What exactly your issue is? BTW: hibernate_interceptor you getting is proxy class. Commented Sep 16 at 6:09
  • The issue is the sourceC object accessed like sourceC.cId=NULL but sourceC.getCId() retrieves the correct, non-NULL ID. Commented Sep 16 at 7:30
  • How it is an issue? Why do you try to access fields directly instead of using getters? As far as I see all works as intended. Commented Sep 16 at 7:32
  • I just thought that it was weird how sourceC.cId returns the non-NULL ID when I swap the position of initializing copyA's bsOfA and csOfA as mentioned above. Commented Sep 16 at 8:07
  • 1
    You are doing getById which will return a lazy proxy. The proxy works on intercepting method calls, you are doing direct field access. So if you call getCId() the proxy will do a query to obtain the information, if you do direct field access it won't. Replacing the getById with a findById will remedy this as well as it will now fetch the full A object, and when accessing the collection will fetch the entities (as opposed to the proxy returned from getById). Commented Sep 16 at 8:38

1 Answer 1

2

The behavior you’re seeing is because of how Hibernate uses proxies for lazy-loaded associations.

When you call sourceA.getCsOfA(), Hibernate doesn’t always return a fully populated C instance. Instead it returns a proxy object with a hibernate_interceptor attached. That proxy only knows how to fetch the data if/when you call a method. If you access fields directly (sourceC.cId), Hibernate never gets a chance to initialize the proxy, so you see null.

public C(String id, C sourceC) {
    this.aId = id;
    this.cId = sourceC.getCId();  // calls through proxy → Hibernate loads the value
}

and this does not:

public C(String id, C sourceC) {
    this.aId = id;
    this.cId = sourceC.cId;       // bypasses proxy → null
}

So the difference is not between cId and getCId() on a “normal” object, but on a proxy. With proxies you must use getters (or initialize the entity/collection before copying).

About the three options you listed:

  1. Swapping the order of copying Bs and Cs – this only works because loading B forces Hibernate to touch its cOfB, which indirectly initializes some C proxies. That’s a side effect, not something you should rely on.

  2. Making cOfB eager – fixes the null problem, but it can explode into unnecessary joins and N+1 queries. It solves one issue but creates others.

  3. Using getters in the constructors – this is the correct fix. Hibernate proxies exist exactly for this use case, and getters are the hook Hibernate uses to trigger lazy loading.

So what’s wrong with the original code is that you’re directly accessing fields of a proxy. The safe approach is to either initialize the associations before copying (e.g. Hibernate.initialize(sourceA.getCsOfA())) or to always go through getters in your copy constructor.

Mapping should be:

@Entity
class A {
    @Id
    String aId;

    @OneToMany(mappedBy = "aOfB", fetch = FetchType.LAZY)
    List<B> bsOfA;

    @OneToMany(mappedBy = "aOfC", fetch = FetchType.LAZY)
    List<C> csOfA;

    public A(String id) {
        this.aId = id;
    }
}

@Entity
class B {
    @EmbeddedId
    BId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("aId")
    A aOfB;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({
        @JoinColumn(name="aId", referencedColumnName="aId"),
        @JoinColumn(name="cId", referencedColumnName="cId")
    })
    C cOfB;

    public B(String newAId, B sourceB) {
        this.id = new BId(newAId, sourceB.getId().getBId());
    }
}

@Entity
class C {
    @EmbeddedId
    CId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("aId")
    A aOfC;

    @OneToMany(mappedBy = "cOfB", fetch = FetchType.LAZY)
    List<B> linkedBsToC;

    public C(String newAId, C sourceC) {
        this.id = new CId(newAId, sourceC.getId().getCId());
    }
}

@Embeddable
class BId implements Serializable {
    String aId;
    String bId;
}

@Embeddable
class CId implements Serializable {
    String aId;
    String cId;
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you very much for the clear explanation :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.