Enterprise Java

Hibernate Performance Tips: Dirty Collection Effect

After 8 years developing server and embedded applications using Hibernate as ORM, squeezing my brain seeking solutions to improve Hibernate performance, reading blogs and attending conferences, I decided to share this knowledge acquired during these years with you.

This is the first post of many more posts to come:

Last year I went to Devoxx as speaker but also I attended Patrycja Wegrzynowicz conference about Hibernate Anti-Patterns. In that presentation Patrycja shows us an anti-pattern that shocks me because it proved to expect the unexpected.

We are going to see the effect it has when Hibernate detects a dirty collection and should re-create it.

Let’s start with the model we are going to use, only two classes related with one-to-many association:

@Entity
public class Starship {

 private Long id;
 @Id @GeneratedValue(strategy=GenerationType.SEQUENCE) public Long getId() {return id;}
 public void setId(Long id) {this.id = id;}

 private Date launched;
 @Temporal(TemporalType.DATE)  public Date getLaunched() {return launched;}
 public void setLaunched(Date launched) {this.launched = launched;}

 private String registry;
 @Column(unique=true, nullable=false) public String getRegistry() {return registry;}
 public void setRegistry(String registry) {this.registry = registry;}

 private StarshipClassEnum starshipClassEnum;
 @Enumerated public StarshipClassEnum getStarshipClassEnum() {return starshipClassEnum;}
 public void setStarshipClassEnum(StarshipClassEnum starshipClassEnum) {this.starshipClassEnum = starshipClassEnum;}


 private AffiliationEnum affiliationEnum;
 @Enumerated public AffiliationEnum getAffiliationEnum() {return affiliationEnum;}
 public void setAffiliationEnum(AffiliationEnum affiliationEnum) {this.affiliationEnum = affiliationEnum;}


 private Physics physics;
 @Embedded public Physics getPhysics() {return physics;}
 public void setPhysics(Physics physics) {this.physics = physics;}

 private List<Officer> officers = new ArrayList<Officer>();
 @OneToMany(cascade={CascadeType.ALL}) public List<Officer> getOfficers() {return Collections.unmodifiableList(officers);}
 protected void setOfficers(List<Officer> officers) {this.officers = officers;}
 public void addOfficer(Officer officer) {
  officer.setStarship(this);
  this.officers.add(officer);
 }

 public Starship() {
  super();
 }

 public Starship(String registry) {
  setRegistry(registry);
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result
    + ((registry == null) ? 0 : registry.hashCode());
  return result;
 }
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Starship other = (Starship) obj;
  if (registry == null) {
   if (other.registry != null)
    return false;
  } else if (!registry.equals(other.registry))
   return false;
  return true;
 }
}
@Entity
public class Officer {

 private Long id;
 @Id @GeneratedValue(strategy=GenerationType.SEQUENCE)public Long getId() {return id;}
 protected void setId(Long id) {this.id = id;}


 private String name;
 @Column(unique=true, nullable=false) public String getName() {return this.name;}
 public void setName(String name) {this.name = name;}


 private SpeciesEnum speciesEnum;
 @Enumerated public SpeciesEnum getSpeciesEnum() {return speciesEnum;}
 public void setSpeciesEnum(SpeciesEnum speciesEnum) {this.speciesEnum = speciesEnum;}


 private PlanetEnum homePlanet;
 @Enumerated public PlanetEnum getHomePlanet() {return homePlanet;}
 public void setHomePlanet(PlanetEnum homePlanet) {this.homePlanet = homePlanet;}


 private AffiliationEnum affiliationEnum;
 @Enumerated public AffiliationEnum getAffiliationEnum() {return affiliationEnum;}
 public void setAffiliationEnum(AffiliationEnum affiliationEnum) {this.affiliationEnum = affiliationEnum;}


 private RankEnum rank;
 @Enumerated @NotNull public RankEnum getRank() {return rank;}
 public void setRank(RankEnum rank) {this.rank = rank;}

 private Starship starship; 
 @ManyToOne public Starship getStarship() {return starship;}
 protected void setStarship(Starship starship) {this.starship = starship;}

 public Officer() {
  super();
 }

 public Officer(String name, RankEnum rank) {
  setName(name);
  setRank(rank);
 }
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((name == null) ? 0 : name.hashCode());
  return result;
 }
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Officer other = (Officer) obj;
  if (name == null) {
   if (other.name != null)
    return false;
  } else if (!name.equals(other.name))
   return false;
  return true;
 }

}

In previous classes, we should pay attention in three important points:

  • we are annotating at property level instead of field level.
  • @OneToMany and @ManyToOne uses default options (apart from cascade definition)
  • officers getter on Starship class returns an immutable list.

To test model configuration, we are going to create a test which creates and persists one Starship and seven Officers, and in different Transaction and EntityManager finds created Starship.

@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class StarshipPersistenceTests {

 @Inject
 private EntityManagerFactory entityManagerFactory;

 @Test
 public void testSaveOrderWithItems() throws Exception {

  Starship starship = createData();
  findStarship(starship);

 }

 private Starship createData() {
  EntityManager entityManager = entityManagerFactory.createEntityManager();
  EntityTransaction transaction = entityManager.getTransaction();
  transaction.begin();

  Physics physics = physics().height(137.5D).length(642.5D)
    .power("Wrap reactor").width(467.0D).build();

  Calendar launched = Calendar.getInstance();
  launched.set(2363, 9, 4);

  Starship starship = starship().registry("NCC-1701-D").physics(physics)
    .launched(launched.getTime())
    .starshipClass(StarshipClassEnum.GALAXY)
    .affiliation(AffiliationEnum.STARFLEET).build();

  Officer jeanLucPicard = officer().name("Jean-Luc Picard")
    .rank(RankEnum.CAPTAIN).affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.EARTH).speciment(SpeciesEnum.HUMAN)
    .build();
  starship.addOfficer(jeanLucPicard);

  Officer williamRiker = officer().name("William Riker")
    .rank(RankEnum.COMMANDER)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.EARTH).speciment(SpeciesEnum.HUMAN)
    .build();
  starship.addOfficer(williamRiker);

  Officer data = officer().name("Data")
    .rank(RankEnum.LIEUTENANT_COMMANDER)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.OMICRON_THETA)
    .speciment(SpeciesEnum.ANDROID).build();
  starship.addOfficer(data);

  Officer geordiLaForge = officer().name("Geordi La Forge")
    .rank(RankEnum.LIEUTENANT)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.EARTH).speciment(SpeciesEnum.HUMAN)
    .build();
  starship.addOfficer(geordiLaForge);

  Officer worf = officer().name("Worf").rank(RankEnum.LIEUTENANT)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.QONOS).speciment(SpeciesEnum.KLINGON)
    .build();
  starship.addOfficer(worf);

  Officer beverlyCrusher = officer().name("Beverly Crusher")
    .rank(RankEnum.COMMANDER)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.EARTH).speciment(SpeciesEnum.HUMAN)
    .build();
  starship.addOfficer(beverlyCrusher);

  Officer deannaTroi = officer().name("Deanna Troi")
    .rank(RankEnum.COMMANDER)
    .affiliation(AffiliationEnum.STARFLEET)
    .homePlanet(PlanetEnum.BETAZED).speciment(SpeciesEnum.BETAZOID)
    .build();
  starship.addOfficer(deannaTroi);

  entityManager.persist(starship);

  transaction.commit();
  entityManager.close();
  return starship;
 }

 private void findStarship(Starship starship) {

  EntityManager entityManager = this.entityManagerFactory.createEntityManager();
  EntityTransaction transaction = entityManager.getTransaction();
  transaction.begin();
  System.out.println("Before Find");
  Starship newStarship = entityManager.find(Starship.class, starship.getId());
  System.out.println("After Find Before Commit");
  transaction.commit();
  System.out.println("After commit");
  entityManager.close();

 }
}

Now that we have created this test, we can run it and we are going to observe Hibernate console output.

Hibernate: insert into Starship (affiliationEnum, launched, height, length, power, width, registry, starshipClassEnum, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)

Before Find Starship By Id

Hibernate: select starship0_.id as id1_0_, starship0_.affiliationEnum as affiliat2_1_0_, starship0_.launched as launched1_0_, starship0_.height as height1_0_, starship0_.length as length1_0_, starship0_.power as power1_0_, starship0_.width as width1_0_, starship0_.registry as registry1_0_, starship0_.starshipClassEnum as starship9_1_0_ from Starship starship0_ where starship0_.id=?

After Find Starship By Id and Before Commit

Hibernate: select officers0_.Starship_id as Starship1_1_2_, officers0_.officers_id as officers2_2_, officer1_.id as id0_0_, officer1_.affiliationEnum as affiliat2_0_0_, officer1_.homePlanet as homePlanet0_0_, officer1_.name as name0_0_, officer1_.rank as rank0_0_, officer1_.speciesEnum as speciesE6_0_0_, officer1_.starship_id as starship7_0_0_, starship2_.id as id1_1_, starship2_.affiliationEnum as affiliat2_1_1_, starship2_.launched as launched1_1_, starship2_.height as height1_1_, starship2_.length as length1_1_, starship2_.power as power1_1_, starship2_.width as width1_1_, starship2_.registry as registry1_1_, starship2_.starshipClassEnum as starship9_1_1_ from Starship_Officer officers0_ inner join Officer officer1_ on officers0_.officers_id=officer1_.id left outer join Starship starship2_ on officer1_.starship_id=starship2_.id where officers0_.Starship_id=?
Hibernate: delete from Starship_Officer where Starship_id=?
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)
Hibernate: insert into Starship_Officer (Starship_id, officers_id) values (?, ?)

After commit

See the number of queries executed during first commit (persisting objects) and during commit of second transaction (finding a Starship). In total and ignoring sequence generator, we can count 22 inserts, 2 selects and 1 delete, not bad when we are only creating 8 objects and 1 find by primary key.

At this point let’s examine why these SQL queries are executed:

First eight inserts are unavoidable; they are required by inserting data into database.

Next seven inserts are required because we have annotated getOfficers property without mappedBy attribute. If we look closely at Hibernate documentation, it points us that “Without describing any physical mapping, a unidirectional one to many with join table is used.”

Next group of queries are even stranger, the first select statement is to find Starship by id, but what are these deletes and inserts of data that we have already created?

During commit Hibernate validates whether collection properties are dirty by comparing object references. When a collection is marked as dirty, Hibernate needs to re-create whole collection, even containing the same objects. In our case when we are getting officers we are returning a different collection instance, concretely an unmodifiable list, so Hibernate considers officers collection as dirty.

Because a join table is used, Starship_Officer table should be re-created, deleting previous inserted tuples and inserting the new ones (although they have the same values).

Let’s try to fix this problem. We start by mapping a bidirectional one-to-many association, with many-to-one side as owning side.

private List<Officer> officers = new ArrayList<Officer>();
@OneToMany(mappedBy="starship", cascade={CascadeType.ALL}) public  List<Officer> getOfficers() {return Collections.unmodifiableList(officers);}
protected void setOfficers(List<Officer> officers) {this.officers = officers;}
public void addOfficer(Officer officer) {this.officers.add(officer);}

And now we rerun the same test again and we inspect the output again.

Hibernate: insert into Starship (affiliationEnum, launched, height, length, power, width, registry, starshipClassEnum, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)

Before Find Starship By Id

Hibernate: select starship0_.id as id1_0_, starship0_.affiliationEnum as affiliat2_1_0_, starship0_.launched as launched1_0_, starship0_.height as height1_0_, starship0_.length as length1_0_, starship0_.power as power1_0_, starship0_.width as width1_0_, starship0_.registry as registry1_0_, starship0_.starshipClassEnum as starship9_1_0_ from Starship starship0_ where starship0_.id=?

After Find Starship By Id and Before Commit

Hibernate: select officers0_.starship_id as starship7_1_1_, officers0_.id as id1_, officers0_.id as id0_0_, officers0_.affiliationEnum as affiliat2_0_0_, officers0_.homePlanet as homePlanet0_0_, officers0_.name as name0_0_, officers0_.rank as rank0_0_, officers0_.speciesEnum as speciesE6_0_0_, officers0_.starship_id as starship7_0_0_ from Officer officers0_ where officers0_.starship_id=?

After commit

Although we have reduced the number of SQL statements, from 25 to 10, we still have an unnecessary query, the ones just in commit section of second transaction. Why if officers are lazy by default (JPA specification), and we are not getting officers in transaction, Hibernate executes a select on Officers table? By the same reason as previously configuration, returned collection has different Java identifier, so Hibernate marks it as newly instantiated collection, but now obviously join table operations are no longer required. We have reduced the number of queries but we still have a performance problem. It is likely that we’ll need some other solution, and the solution is not the most obvious one, we are not going to return collection objects returned by Hibernate, we might expand on this later, but we are going to change annotations location.

What we are going to do is to change mapping location from property approach to use field mapping. Simply we are going to move all annotations to class attributes rather than on getters.

@Entity
public class Starship {

 @Id @GeneratedValue(strategy=GenerationType.SEQUENCE) 
 private Long id;
 public Long getId() {return id;}
 protected void setId(Long id) {this.id = id;}

 @Temporal(TemporalType.DATE) private Date launched;
 public Date getLaunched() {return launched;}
 public void setLaunched(Date launched) {this.launched = launched;}

        ...

        @OneToMany(mappedBy="starship", cascade={CascadeType.ALL}) 
 private List<Officer> officers = new ArrayList<Officer>();
 public List<Officer> getOfficers() {return Collections.unmodifiableList(officers);}
 protected void setOfficers(List<Officer> officers) {this.officers = officers;}
 public void addOfficer(Officer officer) {
  officer.setStarship(this);
  this.officers.add(officer);
 }

 public Starship() {
  super();
 }

        public Starship(String registry) {
  setRegistry(registry);
 }

 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result
    + ((registry == null) ? 0 : registry.hashCode());
  return result;
 }
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Starship other = (Starship) obj;
  if (registry == null) {
   if (other.registry != null)
    return false;
  } else if (!registry.equals(other.registry))
   return false;
  return true;
 } 
}
@Entity
public class Officer {

 @Id @GeneratedValue(strategy=GenerationType.SEQUENCE) private Long id;
 public Long getId() {return id;}
 protected void setId(Long id) {this.id = id;}


 @Column(unique=true, nullable=false) private String name;
 public String getName() {return this.name;}
 public void setName(String name) {this.name = name;}


 @Enumerated private SpeciesEnum speciesEnum;
 public SpeciesEnum getSpeciesEnum() {return speciesEnum;}
 public void setSpeciesEnum(SpeciesEnum speciesEnum) {this.speciesEnum = speciesEnum;}

        ...


        @ManyToOne private Starship starship; 
 public Starship getStarship() {return starship;}
 protected void setStarship(Starship starship) {this.starship = starship;}

 public Officer() {
  super();
 }

 public Officer(String name, RankEnum rank) {
  setName(name);
  setRank(rank);
 }
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((name == null) ? 0 : name.hashCode());
  return result;
 }
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Officer other = (Officer) obj;
  if (name == null) {
   if (other.name != null)
    return false;
  } else if (!name.equals(other.name))
   return false;
  return true;
 }
}

And finally we are going to run the test again, and see what’s happen:

Hibernate: insert into Starship (affiliationEnum, launched, height, length, power, width, registry, starshipClassEnum, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into Officer (affiliationEnum, homePlanet, name, rank, speciesEnum, starship_id, id) values (?, ?, ?, ?, ?, ?, ?)

Before Find
Hibernate: select starship0_.id as id1_0_, starship0_.affiliationEnum as affiliat2_1_0_, starship0_.launched as launched1_0_, starship0_.height as height1_0_, starship0_.length as length1_0_, starship0_.power as power1_0_, starship0_.width as width1_0_, starship0_.registry as registry1_0_, starship0_.starshipClassEnum as starship9_1_0_ from Starship starship0_ where starship0_.id=?

After Find Before Commit
After commit

Why using property mapping Hibernate runs queries during commit and using field mapping are not executed? When a Transaction is committed, Hibernate execute a flush to synchronize the underlying persistent store with persistable state held in memory. When property mapping is used, Hibernate calls getter/setter methods to synchronize data, and in case of getOfficers method, it returns a dirty collection (because of unmodifiableList call). On the other side when we are using field mapping, Hibernate gets directly the field, so collection is not considered dirty and no re-creation is required.

But we have not finished yet, I suppose you are wondering why we have not removed Collections.unmodifiableList from getter, returning Hibernate collection? Yes I agree with you that we finished quickly, and change would look like @OneToMany(cascade={CascadeType.ALL}) public List<Officer> getOfficers() {officers;} but returning original collection ends up with an encapsulation problem, in fact we are broken encapsulation!. We could add to mutable list anything we like; we could apply uncontrolled changes to the internal state of an object.

Using an unmodifiableList is an approach to use to avoid breaking encapsulation, but of course we could have used different accessors for public access and hibernate access, and not calling Collections.unmodifiableList method.

Considering what we have seen today, I suggest you to use always field annotations instead of property mapping, we are going to save from a plenty of surprises.

Hope you have found this post useful.

Screencast of example shown here:

Download code

Reference: Hibernate Performance Tips: Dirty Collection Effect from our JCG partner Alex Soto at the One Jar To Rule Them All blog.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
syed
syed
6 years ago

Thanks very much for this information. Keep it up.

the
the
6 years ago

Sorry, I have a question in the second section, after you use ‘mapBy’, if no join table is re-created, why doesn’t Hibernate update Officer table, now is getOfficers still dirty or not?

The
The
6 years ago

Can i know why after you set “mapBy”, getOfficers() is not dirty anymore?

Back to top button