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.
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;
...
}
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.List
to a Set
to enable multiple fetch joins
later in the code.