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:
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)
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 :)
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:
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.
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.
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;
}