简体   繁体   中英

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...

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 ConcurrentModificationException s but instead (I think) you'll see ConstraintViolationException s.

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

Matt is correct but I thought I'd add some additional information about other ways to work around the problem.

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.

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

That should get you a little further down the road.

Gray's solution worked! Fortunately for us the JPA people seem to have been trying to implement collections as close to official Sun documentation on the proper use of List<> collections has indicated:

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.

I was all but pulling out my hair over this exception thinking it meant one @Stateless method could not call another @Stateless method from it's own class. This I thought odd as I was sure that I read somewhere that nested transactions are allowed. So when I did a search on this very exception, I found this posting and applied Gray's solution. Only in my case I happened to have two independent collections that had to be handled. As Gray indicated, according the Java spec on the proper way to remove from a member from a Java container, you need to use a copy of the original container to iterate with and then do your remove() on the original container which makes a lot of sense. Otherwise, the original container's link list algorithm gets confused.

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

Notice I only make a copy of the first collection ( p1.getFollowing() ) and not the second collection ( p2.getFollowers() ). That is because I only need to iterate from one collection even though I need to remove associations from both collections.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM