Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JPA ManyToMany ConcurrentModificationException issues

We have three entities with bidirectional many-to-many mappings in a A <-> B <-> C "hierarchy" like so (simplified, of course):

@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;
}

There's no conecpt of an orphan - the entities are "standalone" from the application's point of view - and most of the time we're going to have a fistful of A:s, each with a couple of B:s (some may be "shared" among the A:s), and some 1000 C:s, not all of which are always "in use" by any B. We've concluded that we need bidirectional relations, since whenever an entity instance is removed, all links (entries in the join tables) have to be removed too. That is done like this:

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

If the collection, a.getBs() here, contains more than one element, then a ConcurrentModificationException is thrown. I've been banging my head for a while now, but can't think of a reasonable way of removing the links without meddling with the collection, which makes underlying the Iterator angry.

Q1: How am I supposed to do this, given the current ORM setup? (If at all...)

Q2: Is there a more reasonable way do design the OR-mappings that will let JPA (provided by Hibernate in this case) take care of everything. It'd be just swell if we didn't have to include those I'll be deleted now, so everybody I know, listen carefully: you don't need to know about this!-loops, which aren't working anyway, as it stands...

like image 800
Gustav Barkefors Avatar asked Jan 07 '11 15:01

Gustav Barkefors


2 Answers

This problem has nothing to do with the ORM, as far as I can tell. You cannot use the syntactic-sugar foreach construct in Java to remove an element from a collection.

Note that Iterator.remove is the only safe way to modify a collection during iteration; the behavior is unspecified if the underlying collection is modified in any other way while the iteration is in progress.

Source

Simplified example of the problematic code:

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

You must use the Iterator version to remove elements while iterating. Correct implementation:

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

Edit: I think this will work; untested however. If not, you should stop seeing ConcurrentModificationExceptions but instead (I think) you'll see ConstraintViolationExceptions.

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
    }
}
like image 86
Matt Ball Avatar answered Oct 28 '22 15:10

Matt Ball


If the collection, a.getBs() here, contains more than one element, then a ConcurrentModificationException is thrown

The issue is that the collections inside of A, B, and C are magical Hibernate collections so when you run the following statement:

b.getAs().remove( a );

this removes a from b's collection but it also removes b from a's list which happens to be the collection being iterated over in the for loop. That generates the ConcurrentModificationException.

Matt's solution should work if you are really removing all elements in the collection. If you aren't however another work around is to copy all of the b's into a collection which removes the magical Hibernate collection from the process.

// copy out of the magic hibernate collection to a local collection
List<B> copy = new ArrayList<>(a.getBs());
for (B b : copy) {
   b.getAs().remove(a) ;
   entityManager.merge(b);
}

That should get you a little further down the road.

like image 39
Gray Avatar answered Oct 28 '22 15:10

Gray