Search code examples
javaperformancehibernatebidirectional

Bi-direction becomes much slower than uni-direction


In a Java/Hibernate Application I have two classes Cat and Kitten in a bidirectional relation as depict below:

public class Cat {
  ...
  @OneToMany(mappedBy="cat", fetch = FetchType.LAZY)
  @OnDelete(action = OnDeleteAction.CASCADE)
  @LazyCollection(LazyCollectionOption.EXTRA)
  @Getter
  @Setter
  private List<Kitten> kittens = new LinkedList();
  public void addKitten(Kitten k) {
    kittens.add(k);
  }
  ...
}  

public class Kitten {
  ...   
  @ManyToOne(fetch=FetchType.LAZY)
  @Getter
  @Setter
  private Cat cat;
  ...
}

In a huge for-loop 20000 Kitten are added to different Cat entities which were created previously. The important code in the for-loop looks like this:

....
Kitten k = new Kitten();
k.setAttribut("foo");
k.setCat(currentCat);     // (a) line
currentCat.addKitten(k);  // (b) line
daoFactory.getKittenDao().save(k);
...

The code is working, but somehow the performance is very slow. In a previous iteration (uni-directional) the application was much faster. Since the final version should work on 1000000 Kitten there must be a way to improve. If I benchmark the time for the code above, it takes approximately 40 seconds. If I simulate uni-direction by removing line (a) or (b), it takes 10 seconds in both cases (but crahes later if I access the objects).

So my question is: Do I miss something, or is the internal maintenance of bidirectional relations very slow in Hibernate? Since the simulates uni-direction is much faster, I would expect a runtime of approximately 15 seconds for the bidirection.

Update:

The code saving the entities is inside a SAX-Parser DefaultHandler implementation. Thus, while parsing the XML structure, first a Cat is saved in the startElement() function and later on the Kitten will be saved in another startElement() call. Regarding the suggestions/questions by @Bartun: I had a look into batching. The problem is, by using DAOs and the startElement() functions, it is hard to tell, when exactly 50 Entities have been saved to flush the session. A counter could do the trick though. However, it does not explain the performance impact by establishing the bidirection. As Transaction management Spring @Transactional is used on the function starting the XML parsing.


Solution

  • I have reduced the processing time to a value I can live with. It not really solved my problem, but reduced the time significantly. If someone else has the same problem, here is the current code and a short explanation.

    public class Cat {
      ...
      @OneToMany(mappedBy="cat", fetch = FetchType.LAZY)
      @OnDelete(action = OnDeleteAction.CASCADE)
      @LazyCollection(LazyCollectionOption.EXTRA)
      @Getter
      @Setter
      private Set<Kitten> kittens = new HashSet();
      public void addKitten(Kitten k) {
         k.setCat(this);
         if (Hibernate.isInitialized(kittens)) kittens.add(k); //line X
      }
      ...
    }  
    
    public class Kitten {
      ...   
      @ManyToOne(fetch=FetchType.LAZY)
      @OnDelete(action = OnDeleteAction.CASCADE)
      @Getter
      @Setter
      private Cat cat;
      ...
    }
    
    • Most important is line X. In the original code, the entire list of kittens has been loaded from the database, each time a kitten has been added. As I found here, the kitten must not be added, if the list isn't inizialized yet. I know, this has to be done with care, since each kitten must be saved in the DB before the list is first initialized. Otherwise everything gets out of sync.
    • Not illustraited in the code above, I changed the structure of kitten persistence to enable batch inserts (Thx Bartun for the links). Now, all kittens are saved at the end of the parsing process using batching instead of saving each kitten on its own.
    • Also a small improvement was the change from a List to a Set to enable multiple fetch joins later in the code.