Using Hibernate Annotations to Map a Collection of Elements

Using Hibernate Annotations to Map a collection of Elements
An element is a fully dependent object, its life cycle is fully dependent on its owner (the collection owner). In Hibernate a collection of elements has to be annotated using the @CollectionOfElements annotation, currently the @OneToMany annotation performs the same task but Hibernate documentation (3.1beta8) deprecates the use of @OneToMany for a collection of elements. The documentation explicitly states that it will not be supported in future Hibernate annotations versions.

An element can be of any class, a java basic type– like String, or a user class – like Comment. In this blog entry I illustrate the uses a user defined type.


The Element Class

My element class (par.common.Comment) has two properties: comment and date. It doesn't have an id property. Here is the class

.....
@Embeddable
public class Comment implements Serializable {

 /**
  * SUID
  */
 private static final long serialVersionUID = 7096630990899203501L;

 private Timestamp date;

 private String comment;

 public String getComment() {
  return comment;
 }

 public void setComment(String comment) {
  this.comment = comment;
 }

 public Timestamp getDate() {
  return date;
 }

 public void setDate(Timestamp date) {
  this.date = date;
 }

}

The element (also known as component) class is only annotated with @Embeddable.

The Owner Class

The owner of the comment (in this example the Customer class) holds a collection of comments. The collection has to be annotated with @CollectionOfElements.

@Entity
@NamedQueries( {@NamedQuery(name="getAllCustomers", query="select c from Customer c")})
public class Customer implements Serializable {
 

 /**
  * SUID
  */
 private static final long serialVersionUID = -24023956885718903L;

 @Id
 @GeneratedValue(strategy = GenerationType.TABLE)
 private int id;

 @Version
 @Column(name = "OPTLOCK", nullable = false)
 private int version;

 @CollectionOfElements 
 private List comments = new ArrayList(10);

 .........
}

For a basic mapping this is enough. This mapping requires two database tables the CUSTOMER table and the CUSTOMER_COMMENTS table. We can override these names but notice that even in the database schema we can see that comments are dependent on customers. The CUSTOMER_COMMENTS table structure is as follows
  • CUSTOMER_ID – The owner entity
  • DATE – The date property
  • COMENT – The comment property
There is no comment id and no other way to identify a specific comment in the database, we can only find out which customer owns a comment.

Updating a Collection of Elements

When updating a collection of elements Hibernate deletes the entire collection and inserts the updated collection state, considering the following code below which selects all of the customers and remove the first comment from each customer. Before executing the test code the database was populated with one customer with three comments attached
  EntityManager entityManager = entityManagerF.createEntityManager();
  EntityTransaction entityTransaction = entityManager.getTransaction();
  entityTransaction.begin();


  Query query = entityManager.createQuery("select c from Customer c ");
  
  List l = query.getResultList();
  for (Customer c : l) {
   c.getComments().remove(0);
  }
  
  entityTransaction.commit();

Using Hibernate's show_sql option we can see the generated SQL commands

1 Hibernate: /* select c from Customer c */ select customer0_.id as id2_, customer0_.FIRST_NAME as FIRST2_2_, customer0_.LAST_NAME as LAST3_2_, customer0_.birthdate as birthdate2_, customer0_.REG_DATE as REG5_2_, customer0_.gender as gender2_, customer0_.OPTLOCK as OPTLOCK2_, customer0_.customerClass as customer8_2_, customer0_.ssn as ssn2_, customer0_.CC_NAME_ON_CARD as CC10_2_, customer0_.CC_CARD_NO as CC11_2_, customer0_.CC_EXPR_DATE as CC12_2_ from Customer customer0_
2 Hibernate: /* load collection par.common.Customer.comments */ select comments0_.Customer_id as Customer1_0_, comments0_.date as date0_, comments0_.comment as comment0_ from Customer_comments comments0_ where comments0_.Customer_id=?
3 Hibernate: /* delete collection par.common.Customer.comments */ delete from Customer_comments where Customer_id=?
4a Hibernate: /* insert collection row par.common.Customer.comments */ insert into Customer_comments (Customer_id, date, comment) values (?, ?, ?)
4b Hibernate: /* insert collection row par.common.Customer.comments */ insert into Customer_comments (Customer_id, date, comment) values (?, ?, ?)

1 – Selecting the customer
2 – Lazy fetching of the comments collection
3 – Deleting the entire comments collection
4a,4b – Insertion of the undeleted comments
As explained before – since Hibernate can't uniquely match a database row with a comment instance it deletes the entire collection and reinserts the collection's components.

Using an Index

The process of erasing the entire collection and reinsertion is quite a performance penalty, we can overcome this penalty by adding an index column to the collection. The index column identifies the position of an element within the collection. The position of the element in a collection can be used by Hibernate to match a database row and a java object. Adding an index column is done using the@IndexColumn annotation.
 @CollectionOfElements @IndexColumn(name="comment_index")
 private List comments = new ArrayList(10);


And Hibernate's output:

Hibernate: /* select c from Customer c */ select customer0_.id as id2_, customer0_.FIRST_NAME as FIRST2_2_, customer0_.LAST_NAME as LAST3_2_, customer0_.birthdate as birthdate2_, customer0_.REG_DATE as REG5_2_, customer0_.gender as gender2_, customer0_.OPTLOCK as OPTLOCK2_, customer0_.customerClass as customer8_2_, customer0_.ssn as ssn2_, customer0_.CC_NAME_ON_CARD as CC10_2_, customer0_.CC_CARD_NO as CC11_2_, customer0_.CC_EXPR_DATE as CC12_2_ from Customer customer0_
Hibernate: /* load collection par.common.Customer.comments */ select comments0_.Customer_id as Customer1_0_, comments0_.date as date0_, comments0_.comment as comment0_, comments0_.comment_index as comment4_0_ from Customer_comments comments0_ where comments0_.Customer_id=? Hibernate: /* delete collection row par.common.Customer.comments */ delete from Customer_comments where Customer_id=? And comment_index=?
Hibernate: /* update collection row par.common.Customer.comments */ update Customer_comments set date=?, comment=? Where Customer_id=? and comment_index=?

1 – Selecting the customer
2 – Lazy fetching of the comments collection
3 – Deleting the exactly one comment from the collection
4 – Updating all other comments (happens twice – one for each comment to update)
So why do we have exactly the same amount of database hits? Since the first comment in the collection was removed we have to update the index of all other comments. But this is only one scenario, other scenarios may reduce database hits, for example
  • If any other comment had been removed we would had have less database hits.
  • If we only update a comment one database hit will be enough



Please notice: I had recover this blog post from my old blog at http://www.jroller.com/eyallupu since jroller.com is no longer available.  As such the styling might be a bit wobbly ... If something seems 'too broken' please contact me and I'll adjust 

Comments

Popular posts from this blog

New in Spring MVC 3.1: CSRF Protection using RequestDataValueProcessor

Hibernate Exception - Simultaneously Fetch Multiple Bags

Hibernate/JPA Identity Generators