JPA двунаправленные отношения
Если у меня двунаправленные отношения в моей DataModel, мое приложение обязано поддерживать ссылки в актуальном состоянии в коде Java.
Каков наилучший способ сделать это?
Например, бидир. Соотношение 1:N между А и В.
@Entity
class A {
@ManyToOne
private B b;
}
@Entity
class B {
@OneToMany(mappedBy="b")
private Collection<A> as;
}
Если я говорю B.addA(b), это не позволяет переменной b в A указывать на ссылку, которую я добавил. И если я вызываю A.setB(b), это не добавляет ссылку b на коллекцию в B.
Одним из возможных способов было бы вызвать setB AND addA в моем коде приложения.
Другой возможностью было бы написать метод setA(..) следующим образом:
public setB(B b) {
this.b = b;
if(!b.contains(this) {
b.add(this);
}
}
public addA(A a) {
if(!as.conatains(a)) {
as.add(a);
}
a.setB(this);
}
но иногда это вызывает некоторые исключения, такие как:
org.hibernate.LazyInitializationException: illegal access to loading collection
я думаю, потому что фреймворк в какой-то момент вызывает this setMethod и хочет загрузить ссылку "this"...?!? может кто-нибудь объяснить мне, почему это происходит? И как можно гарантировать, что у меня чистые двунаправленные отношения в моем Java-коде?
Спасибо
ОБНОВЛЕНИЕ: вот оригинальный код:
@Entity
class Cluster{
private Grid grid
//someother fields
@ManyToOne
public Grid getGrid() {
return grid;
}
public void setGrid(Grid grid) {
this.grid = grid;
if(!grid.getClusters().contains(this)) { //HERE AN EXCEPTION IS THROWN
grid.addCluster(this);
}
}
}
@Entity
class Grid {
private Collection<Cluster> clusters = new ArrayList<Cluster>();
//some other fields
@OneToMany(mappedBy = "grid", cascade = CascadeType.PERSIST, orphanRemoval = true)
public Collection<Cluster> getClusters() {
return clusters;
}
public void setClusters(Collection<Cluster> clusters) {
this.clusters = clusters;
}
public void addCluster(Cluster c) {
this.clusters.add(c);
c.setGrid(this);
}
}
В одном из моих запросов я получаю исключение, которое говорит, что что-то внутри метода setGrid не так... Если я удаляю строки, все в порядке... но тогда у меня нет моего двунаправленного текста...:/
Трассировка стека:
Exception in thread "main" javax.persistence.PersistenceException: org.hibernate.PropertyAccessException: Exception occurred inside setter of dst1.model.Cluster.grid
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1214)
at org.hibernate.ejb.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1147)
at org.hibernate.ejb.QueryImpl.getResultList(QueryImpl.java:255)
at dst1.Main.dst02b(Main.java:828)
at dst1.Main.main(Main.java:38)
Caused by: org.hibernate.PropertyAccessException: Exception occurred inside setter of dst1.model.Cluster.grid
at org.hibernate.property.BasicPropertyAccessor$BasicSetter.set(BasicPropertyAccessor.java:89)
at org.hibernate.tuple.entity.AbstractEntityTuplizer.setPropertyValues(AbstractEntityTuplizer.java:583)
at org.hibernate.tuple.entity.PojoEntityTuplizer.setPropertyValues(PojoEntityTuplizer.java:229)
at org.hibernate.persister.entity.AbstractEntityPersister.setPropertyValues(AbstractEntityPersister.java:3822)
at org.hibernate.engine.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:152)
at org.hibernate.loader.Loader.initializeEntitiesAndCollections(Loader.java:982)
at org.hibernate.loader.Loader.doQuery(Loader.java:857)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:274)
at org.hibernate.loader.Loader.loadEntity(Loader.java:2037)
at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:86)
at org.hibernate.loader.entity.AbstractEntityLoader.load(AbstractEntityLoader.java:76)
at org.hibernate.persister.entity.AbstractEntityPersister.load(AbstractEntityPersister.java:3268)
at org.hibernate.event.def.DefaultLoadEventListener.loadFromDatasource(DefaultLoadEventListener.java:496)
at org.hibernate.event.def.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:477)
at org.hibernate.event.def.DefaultLoadEventListener.load(DefaultLoadEventListener.java:227)
at org.hibernate.event.def.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:285)
at org.hibernate.event.def.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:152)
at org.hibernate.impl.SessionImpl.fireLoad(SessionImpl.java:1090)
at org.hibernate.impl.SessionImpl.internalLoad(SessionImpl.java:1038)
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:630)
at org.hibernate.type.EntityType.resolve(EntityType.java:438)
at org.hibernate.engine.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:139)
at org.hibernate.loader.Loader.initializeEntitiesAndCollections(Loader.java:982)
at org.hibernate.loader.Loader.doQuery(Loader.java:857)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:274)
at org.hibernate.loader.Loader.doList(Loader.java:2533)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2276)
at org.hibernate.loader.Loader.list(Loader.java:2271)
at org.hibernate.loader.hql.QueryLoader.list(QueryLoader.java:452)
at org.hibernate.hql.ast.QueryTranslatorImpl.list(QueryTranslatorImpl.java:363)
at org.hibernate.engine.query.HQLQueryPlan.performList(HQLQueryPlan.java:196)
at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1268)
at org.hibernate.impl.QueryImpl.list(QueryImpl.java:102)
at org.hibernate.ejb.QueryImpl.getResultList(QueryImpl.java:246)
... 2 more
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.hibernate.property.BasicPropertyAccessor$BasicSetter.set(BasicPropertyAccessor.java:66)
... 35 more
Caused by: org.hibernate.PropertyAccessException: Exception occurred inside setter of dst1.model.Cluster.grid
at org.hibernate.property.BasicPropertyAccessor$BasicSetter.set(BasicPropertyAccessor.java:89)
at org.hibernate.tuple.entity.AbstractEntityTuplizer.setPropertyValues(AbstractEntityTuplizer.java:583)
at org.hibernate.tuple.entity.PojoEntityTuplizer.setPropertyValues(PojoEntityTuplizer.java:229)
at org.hibernate.persister.entity.AbstractEntityPersister.setPropertyValues(AbstractEntityPersister.java:3822)
at org.hibernate.engine.TwoPhaseLoad.initializeEntity(TwoPhaseLoad.java:152)
at org.hibernate.loader.Loader.initializeEntitiesAndCollections(Loader.java:982)
at org.hibernate.loader.Loader.doQuery(Loader.java:857)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:274)
at org.hibernate.loader.Loader.loadCollection(Loader.java:2166)
at org.hibernate.loader.collection.CollectionLoader.initialize(CollectionLoader.java:62)
at org.hibernate.persister.collection.AbstractCollectionPersister.initialize(AbstractCollectionPersister.java:627)
at org.hibernate.event.def.DefaultInitializeCollectionEventListener.onInitializeCollection(DefaultInitializeCollectionEventListener.java:83)
at org.hibernate.impl.SessionImpl.initializeCollection(SessionImpl.java:1863)
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:369)
at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:111)
at org.hibernate.collection.AbstractPersistentCollection.readElementExistence(AbstractPersistentCollection.java:167)
at org.hibernate.collection.PersistentBag.contains(PersistentBag.java:262)
at dst1.model.Cluster.setGrid(Cluster.java:114)
... 40 more
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.hibernate.property.BasicPropertyAccessor$BasicSetter.set(BasicPropertyAccessor.java:66)
... 57 more
Caused by: org.hibernate.LazyInitializationException: illegal access to loading collection
at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:366)
at org.hibernate.collection.AbstractPersistentCollection.read(AbstractPersistentCollection.java:111)
at org.hibernate.collection.AbstractPersistentCollection.readElementExistence(AbstractPersistentCollection.java:167)
at org.hibernate.collection.PersistentBag.contains(PersistentBag.java:262)
at dst1.model.Cluster.setGrid(Cluster.java:114)
... 62 more
2 ответа
Hibernate и другие ORM на основе JPA используют для загрузки коллекций, которые определяют отношения именно тогда, когда это необходимо (отложенная загрузка). Я понимаю, что Hibernate вызывает это исключение, когда вы пытаетесь изменить коллекцию, которая еще не загружена или является промежуточным состоянием.
Hibernate использует прокси для обработки сущностей и понимает, что вы хотите использовать коллекцию при вызове метода get для этой конкретной коллекции.
Я бы реализовал ваш setGrid
метод действительно отличается, но сначала ваши сущности должны реализовать методы equals
а также hashCode
, Другие модификации будут:
Измените свои коллекции кластеров, чтобы быть набором. Набор не содержит повторяющихся экземпляров, поэтому вам не нужно этого делать contains
проверьте перед добавлением любого элемента в коллекцию:
Set<Cluster> clusters = new HashSet<Cluster>();
Затем измените ваш setGrid
метод, поэтому он вызывает add
метод самой коллекции, а не тот, который вы объявили:
setGrid(Grid grid) {
Grid oldGrid = this.grid;
this.grid = grid;
if (oldGrid != null) {
oldGrid.getClusters().remove(this);
}
if (grid != null) {
grid.getClusters().add(this);
}
}
И, наконец, немного измените реализацию вашего addCluster
Метод в классе Grid:
public void addCluster(Cluster c) {
//this.clusters.add(c); -- no needed anymore
c.setGrid(this);
}
Надеюсь это поможет
Это идея.
Я использую два слоя: "слой модели персистентности" и "слой модели домена".
Классы "уровня модели персистентности" имеют некоторые аннотации JPA, но не имеют никаких правил применения.
Классы "слоя модели домена" не имеют аннотаций JPA.
JPA / Hibernate знает классы "слоя модели персистентности", но не знает классов "слоя модели домена".
Классы в "слое персистентной модели" очень просты для JPA/Hibernate.
Так что проблемы, подобные этому вопросу, вряд ли возникнут.
Классы в "слое модели домена", в этом случае, несут ответственность за сохранение двунаправленной связи между A и B.(A#setB, B#addA)
Нет необходимости беспокоиться о воздействии ORM.
Есть пример кода.
"Слой персистентной модели" содержит А и В.
"слой модели предметной области" содержит MA и MB.
Экземпляр MA имеет экземпляр A, и MA делегирует свое состояние A.
/** persistence model layer */
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class A {
private Long id;
private B b;
public A(){
}
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@ManyToOne
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
import java.util.ArrayList;
import java.util.Collection;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entity
public class B {
private Long id;
private Collection<A> as = new ArrayList<A>();
public B(){
}
@Id
@GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@OneToMany(cascade=CascadeType.ALL, mappedBy="b", fetch=FetchType.LAZY)
public Collection<A> getAs() {
return as;
}
public void setAs(Collection<A> as) {
this.as = as;
}
}
/** domain model layer */
public class MA {
private A entity;
public MA(A a){
this.entity = a;
}
public A getEntity(){
return this.entity;
}
public MB getB(){
return new MB(entity.getB());
}
public void setB(MB mb){
if (mb != null && this.entity.getB() != mb.getEntity()){
this.entity.setB(mb.getEntity());
mb.addA(this);
}
return;
}
}
import java.util.ArrayList;
import java.util.List;
public class MB {
private B entity;
public MB(B b){
this.entity = b;
}
public B getEntity(){
return this.entity;
}
public void addA(MA ma){
if (ma != null && ! this.getEntity().getAs().contains(ma.getEntity())){
this.entity.getAs().add(ma.getEntity());
ma.setB(this);
}
return;
}
public List<MA> getAs(){
List<MA> resultList = new ArrayList<MA>();
for(A a : entity.getAs()){
resultList.add(new MA(a));
}
return resultList;
}
}
Лучше реализовать метод equals/hashCode.
Надеюсь у тебя будет подсказка.