ebean-orm / ebean-orm/ebean

Delete cascade N+1 problem

Open
#1,750 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
1.5k
Forks
267
Avg merge
3d 20h
Merged PRs (30d)
5

Description

Description

In a production app using JPA an object graph should be deleted using a cascaded delete on the root entity. This has been implemented using Ebean since JPA produces a gazillion of single delete queries. However the object graph causes Ebean to produce way more queries than expected and needed (but still better than JPA).

So for now the expected behavior below needs to be implemented manually in that app to avoid many queries.

Expected behavior

select t0.id from city t0 where country_id=?;
select t0.id, t0.mayor_id, t0.vice_mayor_id from city t0 where t0.id = any(?);
select t0.id from calendar t0 where person_id = any(?);

delete from event where (calendar_id) = any(?);
delete from calendar where id  = any(?);
delete from city where id = any(?);
delete from person where id = any(?);
delete from country where id=? and version=?;

Actual behavior

txn[1002] select t0.id from city t0 where country_id=? ; --bind(1)
txn[1002] select t0.id, t0.mayor_id, t0.vice_mayor_id from city t0 where t0.id = any(?); --bind(Array[3]={1,2,3})
txn[1002] delete from city where id=?; -- bind(1)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(1)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={1,2}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={1,2}) rows(2)
txn[1002] delete from person where id=?; -- bind(1)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(2)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={3,4}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={3,4}) rows(2)
txn[1002] delete from person where id=?; -- bind(2)
txn[1002] delete from city where id=?; -- bind(2)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(3)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={5,6}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={5,6}) rows(2)
txn[1002] delete from person where id=?; -- bind(3)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(4)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={7,8}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={7,8}) rows(2)
txn[1002] delete from person where id=?; -- bind(4)
txn[1002] delete from city where id=?; -- bind(3)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(5)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={9,10}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={9,10}) rows(2)
txn[1002] delete from person where id=?; -- bind(5)
txn[1002] select t0.id from calendar t0 where person_id=? ; --bind(6)
txn[1002] delete from event where (calendar_id) = any(?); -- bind(Array[2]={11,12}) rows(20)
txn[1002] delete from calendar where id  = any(?); -- bind(Array[2]={11,12}) rows(2)
txn[1002] delete from person where id=?; -- bind(6)
txn[1002] delete from country where id=? and version=?; -- bind(1,1)
Steps to reproduce

Consider the following entities:

@MappedSuperclass
public class BaseModel extends Model {

  @Version
  private long version = 0;
}

@Entity
public class Country extends BaseModel {

  @Id
  private Long id;

  @OneToMany(cascade = CascadeType.ALL)
  private List<City> cities = new ArrayList<>();

  public void addCity(City city) {
    cities.add(city);
  }
}

@Entity
public class City extends BaseModel {

  @Id
  private Long id;

  @OneToOne(cascade = CascadeType.ALL, optional = false)
  private Person mayor;

  @OneToOne(cascade = CascadeType.ALL, optional = false)
  private Person viceMayor;

  public City(Person mayor, Person viceMayor) {
    this.mayor = mayor;
    this.viceMayor = viceMayor;
  }
}

@Entity
public class Person extends BaseModel {

  @Id
  private Long id;

  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  private List<Calendar> calendars = new ArrayList<>();

  public void addCalendar(Calendar calendar) {
    calendars.add(calendar);
  }
}

@Entity
public class Calendar extends BaseModel {

  @Id
  private Long id;

  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  private List<Event> events = new ArrayList<>();

  public void addEvent(Event event) {
    events.add(event);
  }
}

@Entity
public class Event extends BaseModel {

  @Id
  private Long id;
}

And the following simple code to produce the above log

public static void main(String[] args) throws InterruptedException {
  // init Ebean 
  // setupDb(); 

  var country = new Country();
  for (int a = 0; a < 3; a++) {
    var mayor = createPerson();
    var viceMayor = createPerson();
    country.addCity(new City(mayor, viceMayor));
  }
  country.save();

  Thread.sleep(2000);

  country.deletePermanent();
}

private static Person createPerson() {
  var person = new Person();
  for (int a = 0; a < 2; a++) {
    var calendar = new Calendar();
    for (int b = 0; b < 10; b++) {
      calendar.addEvent(new Event());
    }
    person.addCalendar(calendar);
  }
  return person;
}

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The reproduction starts at country.deletePermanent() after country.save(), using the Country-to-City-to-Person-to-Calendar-to-Event cascade in the supplied main and createPerson methods. Trace Ebean's cascade-delete handling and compare emitted SQL with the expected batched statements; done means the graph is deleted without the repeated per-person selects and deletes shown in Actual behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, sql
Domain
backend, database
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.