Poor model save performance due to cascade change in 1.5.0 (probably)?
Is anyone else experiencing poor model save() performance? It seems to start happening when you get past a certain number of objects (hundreds) reachable by OneToMany or other relationships. In v1.4.4 and previous I do not think this was an issue, but we're seeing some requests time out because of this now (i.e. save() calls taking upwards of 30 seconds to complete).
I suspect it may have something to do with part of the change in #1113 but I'm not familiar enough with the saveAndCascade() code to be sure.
Right now our somewhat sad workaround is to add this method to most model objects and call it instead:
public void fastSave() {
String dbName = JPA.getDBName(this.getClass());
if (!em(dbName).contains(this)) {
em(dbName).persist(this);
PlayPlugin.postEvent("JPASupport.objectPersisted", this);
}
willBeSaved = true;
}
P.s. So glad to see people still working on Play 1! It's still my favorite web framework by far. Thank you!
Hi @megamattron , I don't think I experienced this issue so far and I'm afraid I do not know how to solve the slowness of such scenario. But from what I experienced, even if a request gets a timeout, the controller method will still execute properly. This means that you could use a continuation (with a Promise) to avoid a timeout, which may at least improve the user experience a bit.
@megamattron Could you enable sql logs (add jpa.debugSQL=true line to application.conf)?
Which SQLs does hibernate execute in your case?
I assume that you are getting the famous "N+1 selects" problem.
Hi @megamattron I am also experiencing this problem. As @asolntsev assumed, it is in fact the "N + 1 selects" problem. When saving a model, all relations with CascadeType.PERSIST will have a select statement for each record in the relationship. For instance:
@Entity
public class Parent extends Model {
public String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
public List<Child> children = new ArrayList<>();
public Parent(String name) {
this.name = name;
}
}
@Entity
public class Child extends Model {
public String name;
@ManyToOne
public Parent parent;
public Child(String name, Parent parent) {
this.name = name;
this.parent = parent;
}
}
Calling save() on a Parent will cause a select statement for each child in the relation, even though no children has been altered. Should this happen?
Should this happen?
I think yes. Because of Child can have listeners, methods which annotated @PreXXX/@PostXXX and they should be executed by JPA standard. Similar behaviour you can see while cascade deletion.
My experience matches that of @megamattron. Profiling showed that most of the time was spent flushing the persistence context (not the N+1 problem). I believe that the performance problems with GenericModel.save() are due to the semantics that it provides...that changes are not sent to the persistence layer until you invoke save(). This is different than the normal Hibernate semantics of any changes you make to a persisted entity will be flushed at some indeterminate time. To provide these improved semantics, JPABase disables flushing until the willBeSaved flag is set. Then, when save() is invoked, it sets the willBeSaved flag and flushes the entire persistence context. So the more entities you have in your entity manager, the longer each save() takes. Even without cascading, this is O(N^2) for creating and saving many independent entities.
I work around this by doing something similar to what megamattron does, although instead of adding a method to each class, I have an abstract class that derives from GenericModel and overrides _save() and a few other methods. All of my entities derive from this class. This restores the original Hibernate semantics and performance, which implies that some changes are persisted without invoking save(). For even better performance, I disable flushing completely in a read-only transaction and use manual flushing for transactions which modify entities.
I don't think this can be fixed without changing the semantics of save().