Sunday, March 20, 2011

Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch

Introduction

In this tutorial we'll expand our previous project Spring - Hibernate: One-To-Many Association by explicitly specifying the join table and join columns. We will also declare the Cascade type and Fetch strategy for the @OneToMany annotation.

Spring MVC 3 and Hibernate Tutorials Series
Spring - Hibernate: Many-To-One Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch
Spring - Hibernate: Many-To-One Association
Spring - Hibernate: One-To-Many Association
Spring MVC 3, Hibernate Annotations, MySQL Integration Tutorial
Spring MVC 3, Hibernate Annotations, HSQLDB Integration Tutorial

Changes

Using a diff tool, we can easily visualize the changes from the previous project and our updated project. Notice only Person.java, CreditCardService.java, and PersonService.java have changed. Everything else have remained the same.


Development

Similar with the original project, we'll split our development in three layers: Domain, Service, and Controller.

Domain Layer

For the domain layer, only Person class has changed by adding extra properties to the @OneToMany annotation.

Person.java
package org.krams.tutorial.domain;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;

/**
 * Represents a person entity
 * 
 *  @author Krams at {@link http://krams915@blogspot.com}
 */
@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

 private static final long serialVersionUID = -5527566248002296042L;
 
 @Id
 @Column(name = "ID")
 @GeneratedValue
 private Integer id;
 
 @Column(name = "FIRST_NAME")
 private String firstName;
 
 @Column(name = "LAST_NAME")
 private String lastName;
 
 @Column(name = "MONEY")
 private Double money;

 @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
 @JoinTable(
            name="PersonCreditCards",
            joinColumns = @JoinColumn( name="PERSON_ID"),
            inverseJoinColumns = @JoinColumn( name="CREDIT_ID")
    )
 private Set<CreditCard> creditCards;
 
 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public Double getMoney() {
  return money;
 }

 public void setMoney(Double money) {
  this.money = money;
 }

 public Set<CreditCard> getCreditCards() {
  return creditCards;
 }

 public void setCreditCards(Set<CreditCard> creditCards) {
  this.creditCards = creditCards;
 }
}

Pay extra attention to the @OneToMany annotation:

@Entity
@Table(name = "PERSON")
public class Person implements Serializable {
 ...
 @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER)
 @JoinTable(
            name="PersonCreditCards",
            joinColumns = @JoinColumn( name="PERSON_ID"),
            inverseJoinColumns = @JoinColumn( name="CREDIT_ID")
    )
 private Set<CreditCard> creditCards;
 ...
}
We have explicitly declared the join table name and the join column names.

Using phpymyadmin's database designer, the Hibernate auto-generated relationship between Person and CreditCard looks as follows:


Again using phpymyadmin, the auto-generated tables looks as follows:


Notice we have also specified the Cascade and Fetch strategies as well.

What does CascadeType.ALL do?
CascadeType.PERSIST: cascades the persist (create) operation to associated entities persist() is called or if the entity is managed
CascadeType.MERGE: cascades the merge operation to associated entities if merge() is called or if the entity is managed
CascadeType.REMOVE: cascades the remove operation to associated entities if delete() is called
CascadeType.REFRESH: cascades the refresh operation to associated entities if refresh() is called
CascadeType.DETACH: cascades the detach operation to associated entities if detach() is called

CascadeType.ALL: all of the above

Source: Hibernate Annotations Reference Guide

What does FetchType.EAGER do?
You have the ability to either eagerly or lazily fetch associated entities. The fetch parameter can be set to FetchType.LAZY or FetchType.EAGER. EAGER will try to use an outer join select to retrieve the associated object, while LAZY will only trigger a select when the associated object is accessed for the first time. @OneToMany and @ManyToMany associations are defaulted to LAZY and @OneToOne and @ManyToOne are defaulted to EAGER.

Source: Hibernate Annotations Reference Guide

Notice @OneToMany are defaulted to LAZY.

Service Layer

Specifying the Cascade and Fetch strategies with @OneToMany annotation requires us to change our service classes to take advantage of those settings.

PersonService.java
package org.krams.tutorial.service;

import java.util.List;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service for processing Persons
 * 
 * @author Krams at {@link http://krams915@blogspot.com
 */
@Service("personService")
@Transactional
public class PersonService {

 protected static Logger logger = Logger.getLogger("service");
 
 @Resource(name="sessionFactory")
 private SessionFactory sessionFactory;
 
 /**
  * Retrieves all persons
  * 
  * @return a list of persons
  */
 public List<Person> getAll() {
  logger.debug("Retrieving all persons");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();

  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person");
  
  // Retrieve all
  return  query.list();
 }
 
 /**
  * Retrieves a single person
  */
 public Person get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing person
  return (Person) session.get(Person.class, id);
 }
 
 /**
  * Adds a new person
  */
 public void add(Person person) {
  logger.debug("Adding new person");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Persists to db
  session.save(person);
 }
 
 /**
  * Deletes an existing person
  * @param id the id of the existing person
  */
 public void delete(Integer id) {
  logger.debug("Deleting existing person");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
 
  // Retrieve record
  Person person = (Person) session.get(Person.class, id);
  
  // Delete person
  session.delete(person);
 }
 
 /**
  * Edits an existing person
  */
 public void edit(Person person) {
  logger.debug("Editing existing person");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing person via id
  Person existingPerson = (Person) session.get(Person.class, person.getId());
  
  // Assign updated values to this person
  existingPerson.setFirstName(person.getFirstName());
  existingPerson.setLastName(person.getLastName());
  existingPerson.setMoney(person.getMoney());

  // Save updates
  session.save(existingPerson);
 }
}

What exactly has changed from the original implementation to our new code? Here are the changes:

Get Method
old code
public Person get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing person
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+id);
  
  return (Person) query.uniqueResult();
 }

new code
public Person get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing person
  return (Person) session.get(Person.class, id);
 }

Delete Method
old code
public void delete(Integer id) {
  logger.debug("Deleting existing person");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+id);
 
  // Retrieve record
  Person person = (Person) query.uniqueResult();
  
  Set<:CreditCard> creditCards =person.getCreditCards();
  
  // Delete person
  session.delete(person);
  
  // Delete associated credit cards
  for (CreditCard creditCard: creditCards) {
   session.delete(creditCard);
  }
 }

new code
public void delete(Integer id) {
  logger.debug("Deleting existing person");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
 
  // Retrieve record
  Person person = (Person) session.get(Person.class, id);
  
  // Delete person
  session.delete(person);
 }

CreditCardService.java
package org.krams.tutorial.service;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.krams.tutorial.domain.CreditCard;
import org.krams.tutorial.domain.Person;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service for processing Credit Cards
 * 
 * @author Krams at {@link http://krams915@blogspot.com
 */
@Service("creditCardService")
@Transactional
public class CreditCardService {

 protected static Logger logger = Logger.getLogger("service");
 
 @Resource(name="sessionFactory")
 private SessionFactory sessionFactory;
 
 /**
  * Retrieves all credit cards
  */
 public List<CreditCard> getAll(Integer personId) {
  logger.debug("Retrieving all credit cards");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM Person as p WHERE p.id="+personId);
  
  Person person = (Person) query.uniqueResult();
  
  // Retrieve all
  return  new ArrayList<CreditCard>(person.getCreditCards());
 }
 
 /**
  * Retrieves all credit cards
  */
 public List<CreditCard> getAll() {
  logger.debug("Retrieving all credit cards");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Create a Hibernate query (HQL)
  Query query = session.createQuery("FROM CreditCard");
  
  // Retrieve all
  return  query.list();
 }
 
 /**
  * Retrieves a single credit card
  */
 public CreditCard get( Integer id ) {
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing credit card
  CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);
  
  // Persists to db
  return creditCard;
 }
 
 /**
  * Adds a new credit card
  */
 public void add(Integer personId, CreditCard creditCard) {
  logger.debug("Adding new credit card");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
 
  // Persists to db
  session.save(creditCard);
  
  // Add to person as well
  // Retrieve existing person via id
  Person existingPerson = (Person) session.get(Person.class, personId);
  
  // Assign updated values to this person
  existingPerson.getCreditCards().add(creditCard);

  // Save updates
  session.save(existingPerson);
 }
 
 /**
  * Deletes an existing credit card
  */
 public void delete(Integer id) {
  logger.debug("Deleting existing credit card");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
     // Delete reference to foreign key credit card first
  // We need a SQL query instead of HQL query here to access the third table
     Query query = session.createSQLQuery("DELETE FROM PersonCreditCards " +
       "WHERE CREDIT_ID="+id);
     
     query.executeUpdate();
     
  // Retrieve existing credit card
  CreditCard creditCard = (CreditCard) session.get(CreditCard.class, id);
  
  // Delete 
  session.delete(creditCard);
 }
 
 /**
  * Edits an existing credit card
  */
 public void edit(CreditCard creditCard) {
  logger.debug("Editing existing creditCard");
  
  // Retrieve session from Hibernate
  Session session = sessionFactory.getCurrentSession();
  
  // Retrieve existing credit card via id
  CreditCard existingCreditCard = (CreditCard) session.get(CreditCard.class, creditCard.getId());
  
  // Assign updated values to this credit card
  existingCreditCard.setNumber(creditCard.getNumber());
  existingCreditCard.setType(creditCard.getType());

  // Save updates
  session.save(existingCreditCard);
 }
}

What exactly has changed from the original implementation to our new code? Here are the changes:

Fetch Query
Since we have specified a Fetch strategy, we can now remove the LEFT JOIN FETCH from the HQL query.

old code
Query query = session.createQuery("FROM Person as p LEFT JOIN FETCH  p.creditCards WHERE p.id="+personId);

new code
Query query = session.createQuery("FROM Person as p WHERE p.id="+personId);

When we specified FetchType.EAGER we're letting Hibernate load the associated records automatically:
@OneToMany(fetch=FetchType.EAGER)

Delete Query
Since we have explicitly specified the join table name and join column names, we have to adjust our SQL query.

old code
Query query = session.createSQLQuery("DELETE FROM PERSON_CREDIT_CARD " +
       "WHERE creditCards_ID="+id);

new code
Query query = session.createSQLQuery("DELETE FROM PersonCreditCards " +
       "WHERE CREDIT_ID="+id);

Configuration

We've completed the changes necessary for the project. The next step is to declare the configuration files. But since nothing has changed in the configuration files, we won't be posting them again here. Please see the original tutorial Spring - Hibernate: One-To-Many Association if you want to see the files.

Run the Application


Setup the database

Our application uses MySQL as its database. To run the application, make sure to setup the database first.

To create the database, follow these steps.
1. Open phpmyadmin (or any tool you prefer with)
2. Create a new database named mydatabase
3. Run the application to automatically create the database schema.

To populate the database with sample data, import the mydatabase.sql SQL script which is located under the WEB-INF folder of the application:


Access the main application

To access the main application, use the following URL:
http://localhost:8080/spring-hibernate-one-to-many-jointable/krams/main/record/list

You should see the following application:


Conclusion

That's it. We've successfully expanded our previous project Spring - Hibernate: One-To-Many Association by explicitly specifying the join table and join columns. We also declared the Cascade and Fetch strategies explicitly.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/spring-mvc-hibernate-annotations-integration-tutorial/

You can download the project as a Maven build. Look for the spring-hibernate-one-to-many-jointable.zip in the Download sections.

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring - Hibernate: One-To-Many Association - Explicitly Specify Join Table, Cascade, and Fetch ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

19 comments:

  1. Did you heard what Rob Matts said about that?

    cheap cialis

    ReplyDelete
  2. Hello ,

    "never use string contact to include parameters into your query always use placeholders, unless you want to be vulnerable to sql injection attacks"

    final String query = "DELETE FROM PersonCreditCards " +
    "WHERE CREDIT_ID="+id
    query.setParameter("id", id);
    something like that

    ReplyDelete
  3. @Anonymous, thanks for the comment. But remember tutorials aren't meant to be robust. They have a purpose and audience. I could point a lot of not to do with all of my guides but those are not the real points. It's interesting when people find faults with tutorials. It doesn't just happen to me but also to most tutorials that I've read.

    ReplyDelete
  4. Yea , you right , we are for the main meal -Spring- :)

    ReplyDelete
  5. Nice tutorial, thanks a lot!

    It is nearly that, what i need.

    But how would you map a bidirectional one-to-many using a join table and indexed list at the many end?

    Thanks in advance!

    ReplyDelete
  6. I really like what you have acquired here, really like what you’re saying and the way in which you say it. You make it entertaining and you still care for to keep it smart.

    web hosting in india

    ReplyDelete
  7. why is the extra delete from third table needed in delete() of creditcardservice. shouldn't hibernate be taking care of that ?

    ReplyDelete
  8. Hey. I really like Your post. But I am totally bad at "join" things in queries. Could You tell me, what would be a query for a method, that returns each CC's PersonId in all the CC's list (that I made on my own).
    So i can give CC's ID as a parameter for a method that returns Person ID.
    Thanks in advance. Hope You got it. :)

    ReplyDelete
    Replies
    1. I'm sorry but just like you I'm not an expert when it comes to "joins". I would suggest reading the docs about it. But my best advice is to try various joins and run a performance test.

      Delete
    2. Thanks for answer. I actually found out it finally, but I have different problem - how to save just one variable from query to return it - just Person's name. I keep getting errors there. query.uniqueResult() etc are not working. :)

      Delete
    3. Ok. I found it out on my own - maybe someone finds it useful. To get one thing out of DB (for example person's name), your write your query and then
      String pName = query.uniqueResult().toString();
      So easy, but it made me some problems at the beggining :D

      Delete
  9. hi all, i need do jpa query, into this query has two relations OneToMany with fetch lazy and the query that i want do can i get the two Set the same object for example select o from object o join fetch e.objectList1 object1 join fetch e.objectList2 where id = :idObject now this query doesn't work. I can do

    ReplyDelete
  10. good teaching skills! Thumbs Up for you!

    ReplyDelete
  11. Thanx for the post. I have a question: Why the records don't stay in the database when server restarts?

    ReplyDelete
  12. I have read your blog its very attractive and impressive. I like it your blog.

    Spring online training Spring online training Spring Hibernate online training Spring Hibernate online training Java online training

    spring training in chennai spring hibernate training in chennai

    ReplyDelete