JPA ManyToMany ConcurrentModificationВыполнение исключений - PullRequest
2 голосов
/ 07 января 2011

У нас есть три сущности с двунаправленными сопоставлениями «многие ко многим» в «иерархии» A <-> B <-> C, например, так (упрощенно, конечно):

@Entity
Class A {
  @Id int id;
  @JoinTable(
    name = "a_has_b",
    joinColumns = {@JoinColumn(name = "a_id", referencedColumnName = "id")},
    inverseJoinColumns = {@JoinColumn(name = "b_id", referencedColumnName = "id")})
  @ManyToMany
  Collection<B> bs;
}

@Entity
Class B {
  @Id int id;
  @JoinTable(
    name = "b_has_c",
    joinColumns = {@JoinColumn(name = "b_id", referencedColumnName = "id")},
    inverseJoinColumns = {@JoinColumn(name = "c_id", referencedColumnName = "id")})
  @ManyToMany(fetch=FetchType.EAGER,
    cascade=CascadeType.MERGE,CascadeType.PERSIST,CascadeType.REFRESH})
  @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
  private Collection<C> cs;
  @ManyToMany(mappedBy = "bs", fetch=FetchType.EAGER,
    cascade={CascadeType.MERGE,CascadeType.PERSIST,  CascadeType.REFRESH})
  @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
  private Collection<A> as;
}

@Entity
Class C {
  @Id int id;
  @ManyToMany(mappedBy = "cs", fetch=FetchType.EAGER, 
    cascade={CascadeType.MERGE,CascadeType.PERSIST,  CascadeType.REFRESH})
  @org.hibernate.annotations.Fetch(FetchMode.SUBSELECT)
  private Collection<B> bs;
}

Здесь нет концепциисироты - сущности являются «автономными» с точки зрения приложения - и большую часть времени мы будем иметь пригоршню A: s, каждая с парой B: s (некоторые могут быть «общими»среди A: s) и около 1000 C: s, не все из которых всегда «используются» любым B. Мы пришли к выводу, что нам нужны двунаправленные отношения, поскольку всякий раз, когда экземпляр сущности удаляется, все ссылки (записи)в таблицах объединения) тоже должны быть удалены.Это делается следующим образом:

void removeA( A a ) {
  if ( a.getBs != null ) {
    for ( B b : a.getBs() ) {  //<--------- ConcurrentModificationException here
      b.getAs().remove( a ) ;
      entityManager.merge( b );
    }
  }
  entityManager.remove( a );
}

Если в коллекции, a.getBs() здесь, содержится более одного элемента, то создается ConcurrentModificationException.Некоторое время я бился головой, но не могу придумать разумного способа удалить ссылки, не вмешиваясь в коллекцию, из-за чего лежит Iterator злой.

Q1: Как яЯ должен был сделать это, учитывая текущую настройку ORM?(Если вообще ...)

Q2: Есть ли более разумный способ спроектировать OR-отображения, которые позволят JPA (предоставленной в данном случае Hibernate) позаботиться обо всем.Было бы просто здорово, если бы нам не пришлось включать эти I'll be deleted now, so everybody I know, listen carefully: you don't need to know about this! -циклы, которые все равно не работают, как есть ...

Ответы [ 3 ]

4 голосов
/ 07 января 2011

Насколько я могу судить, эта проблема не имеет ничего общего с ORM. Нельзя использовать синтаксический сахар foreach в Java для удаления элемента из коллекции.

Обратите внимание, что Iterator.remove - это только безопасный способ изменить коллекцию во время итерации; поведение не определено, если базовая коллекция модифицируется любым другим способом во время выполнения итерации.

Источник

Упрощенный пример проблемного кода:

List<B> bs = a.getBs();
for (B b : bs)
{
    if (/* some condition */)
    {
        bs.remove(b); // throws ConcurrentModificationException
    }
}

Вы должны использовать версию Iterator для удаления элементов во время итерации. Правильная реализация:

List<B> bs = a.getBs();
for (Iterator<B> iter = bs.iterator(); iter.hasNext();)
{
    B b = iter.next();
    if (/* some condition */)
    {
        iter.remove(); // works correctly
    }
}

Редактировать: Я думаю, это будет работать; непроверенный однако. Если нет, вы должны перестать видеть ConcurrentModificationException с, но вместо этого (я думаю) вы увидите ConstraintViolationException с.

void removeA(A a)
{
    if (a != null)
    {
        a.setBs(new ArrayList<B>()); // wipe out all of a's Bs
        entityManager.merge(a);      // synchronize the state with the database
        entityManager.remove(a);     // removing should now work without ConstraintViolationExceptions
    }
}
1 голос
/ 07 января 2011

Мэтт прав, но я решил добавить дополнительную информацию о других способах решения этой проблемы.

Проблема в том, что коллекции внутри A, B и C являются волшебными коллекциями Hibernate, поэтому при выполнении следующего оператора:

  b.getAs().remove( a );

это удаляет a из коллекции b, но также удаляет b из списка a, который оказывается коллекцией, перебираемой в цикле for. Это создает ConcurrentModificationException.

Решение Мэтта должно работать, если вы действительно удаляете все элементы в коллекции. Если вы, тем не менее, еще один обходной путь, это скопировать все буквы b в коллекцию, которая удаляет волшебную коллекцию Hibernate из процесса.

    for ( B b : new ArrayList<B>( a.getBs() )) {
       b.getAs().remove( a ) ;
       entityManager.merge( b );
    }

Это должно продвинуть вас чуть дальше.

0 голосов
/ 18 февраля 2015

Решение Грея сработало!К счастью для нас, сотрудники JPA, похоже, пытались реализовать коллекции так, как близко к официальной документации Sun по правильному использованию коллекций List<> указано:

Обратите внимание, что Iterator.remove является единственным безопаснымспособ изменить коллекцию во время итерации;поведение не определено, если базовая коллекция модифицируется любым другим способом во время выполнения итерации.

Я чуть не вырвал свои волосы из-за этого исключения, думая, что это может означать, что один метод @Stateless можетне вызывать другой @Stateless метод из своего собственного класса.Это мне показалось странным, так как я был уверен, что где-то читал, что вложенные транзакции разрешены.Поэтому, когда я провел поиск по этому исключению, я нашел эту публикацию и применил решение Грея.Только в моем случае у меня было две независимые коллекции, которые нужно было обработать.Как указал Грей, в соответствии со спецификацией Java о том, как правильно удалить элемент из контейнера Java, вам нужно использовать копию исходного контейнера для итерации, а затем выполнить remove() для исходного контейнера, что очень многосмысла.В противном случае алгоритм списка ссылок исходного контейнера будет сбит с толку.

    for ( Participant p2 : new ArrayList<Participant>( p1.getFollowing() )) {
        p1.getFollowing().remove(p2);
        getEm().merge(p1);
        p2.getFollowers().remove(p1);
        getEm().merge(p2);
    }

Обратите внимание, что я делаю только копию первой коллекции (p1.getFollowing()), а не второй коллекции (p2.getFollowers()).Это потому, что мне нужно выполнить итерацию только из одной коллекции, хотя мне нужно удалить ассоциации из обеих коллекций.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...