Search This Blog

Monday 12 December 2011

Creating a many to many association - UniDirectional

After one to many, its time to try a many-to-many association. This association is rarely used , instead being replaced with 2 many-to-one associations. For this example I decided to create two entities- people and chocolate (ya am still on chocolates only :) ). Many people like mannnnnnnnnnnnnnnny chocolates. As we are considering unidirectional relation, the mapping can be seen from the People class only.
public class Chocolate {
    private Integer id;
    private String name;
    private String brand;
//setters and getters
}
The Person entity includes the association to chocolates:
public class People {
    private Integer id;
    private String name;
    private Set<Chocolate> favouriteChocolates = new HashSet<Chocolate>();
    
    public synchronized void addChocolates(final Chocolate chocolate) {
        this.getFavouriteChocolates().add(chocolate);
    }
    
    public synchronized void removeChocolate(final Chocolate chocolate) {
        this.getFavouriteChocolates().remove(chocolate);
    }
//setters and getters
}
The mapping files for the two are as below:
Chocolate.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.association.many_to_many.unid">
    <class name="Chocolate" table="CHOCOLATE">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>

        <property name="brand" type="string">
            <column name="BRAND" />
        </property>
        
    </class>
</hibernate-mapping>
People.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.association.many_to_many.unid">
    <class name="People" table="PEOPLE">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="identity" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>

        <set name="favouriteChocolates" table="CHOCOLATE_FAN" cascade="all">
            <key column="FAN_ID" foreign-key="CHOCOLATE_FAN_FK2"/>
            <many-to-many class="Chocolate" column="CHOCOLATE_ID" foreign-key="CHOCOLATE_FAN_FK1"/>
        </set>
    </class>
</hibernate-mapping>
The set element here includes a many-to-many element. The column attribute specifies the column that maps to chocolates. Unlike the previous example no "unique" attribute has been set here. On start up the logs would indicate the following SQL was fired:
create table CHOCOLATE (
     ID integer not null auto_increment,
     NAME varchar(255),
     BRAND varchar(255),
     primary key (ID)
 )
 create table CHOCOLATE_FAN (
     FAN_ID integer not null,
     CHOCOLATE_ID integer not null,
     primary key (FAN_ID, CHOCOLATE_ID)
 )
 create table PEOPLE (
     ID integer not null auto_increment,
     NAME varchar(255),
     primary key (ID)
 )
 alter table CHOCOLATE_FAN 
     add index CHOCOLATE_FAN_FK2 (FAN_ID), 
     add constraint CHOCOLATE_FAN_FK2 
     foreign key (FAN_ID) 
     references PEOPLE (ID)
 
 alter table CHOCOLATE_FAN 
     add index CHOCOLATE_FAN_FK1 (CHOCOLATE_ID), 
     add constraint CHOCOLATE_FAN_FK1 
     foreign key (CHOCOLATE_ID) 
     references CHOCOLATE (ID)
The code below creates a Chocolate and a Fan and links the two. It also create another chocolate record.
static void create() {
    Chocolate chocolate1 = new Chocolate();
    chocolate1.setName("Eclairs");
    chocolate1.setBrand("Cadburys");

    Chocolate chocolate2 = new Chocolate();
    chocolate2.setName("Melody");
    chocolate2.setBrand("Parles");

    People people1 = new People();
    people1.setName("Naina");
    people1.addChocolates(chocolate1);//cascade should save this

    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    session.save(people1);
    session.save(chocolate2);
    t.commit();
    System.out.println("The Person with name "
            + people1.getName() + " was created with id "
            + people1.getId());
    System.out.println("Chocolate1 saved with id " + chocolate1.getId()
            + " and Chocolate2 saved with id " + chocolate2.getId());
}
As can be seen from the logs , in the first save the following SQL was generated:
3015 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        PEOPLE
        (NAME) 
    values
        (?)
...
3078 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE
        (NAME, BRAND) 
    values
        (?, ?)
...
3203 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE_FAN
        (FAN_ID, CHOCOLATE_ID) 
    values
        (?, ?)
For the second save operation, where a chocolate was saved independently the SQL generated is:
3109 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE
        (NAME, BRAND) 
    values
        (?, ?)
The final output is
3234 [main] DEBUG org.hibernate.impl.SessionImpl  - after transaction completion
The Person with name Naina was created with id 1
Chocolate1 saved with id 1 and Chocolate2 saved with id 2
On trying to link the above 2 chocolates with a different person
static void createRelations() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    People people2 = new People();
    people2.setName("Naresh");
    session.save(people2);
    t.commit();

    Chocolate chocolate2 = (Chocolate) session.get(Chocolate.class, 2);
    Chocolate chocolate1 = (Chocolate) session.get(Chocolate.class, 1);
    t = session.beginTransaction();
    people2.addChocolates(chocolate1);
    people2.addChocolates(chocolate2);
    t.commit();
}
The select query fired for chocolate is
2781 [main] DEBUG org.hibernate.SQL  - 
    select
        chocolate0_.ID as ID2_0_,
        chocolate0_.NAME as NAME2_0_,
        chocolate0_.BRAND as BRAND2_0_ 
    from
        CHOCOLATE chocolate0_ 
    where
        chocolate0_.ID=?
On calling save for the People object, the SQL generated is as below:
2625 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        PEOPLE
        (NAME) 
    values
        (?)
2828 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE_FAN
        (FAN_ID, CHOCOLATE_ID) 
    values
        (?, ?)
2843 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CHOCOLATE_FAN
        (FAN_ID, CHOCOLATE_ID) 
    values
        (?, ?)
The entries were made in the PEOPLE and the join table thanks to the cascade settings employed.
I then executed a code to first remove a chocolate from a person's favorite list
and then delete the person itself
static void deleteElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    People people2 = (People) session.get(People.class, 2);
    Chocolate chocolate2 = (Chocolate) session.get(Chocolate.class, 2);
    people2.removeChocolate(chocolate2);//only removal from join table
    t.commit();
        
    t = session.beginTransaction();
    session.delete(people2);//must fail
    t.commit();
    session.close();
    t.commit();
}
For the first transaction the logs indicate the query was fired successfully and the association broken.
2860 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        CHOCOLATE_FAN 
    where
        FAN_ID=? 
        and CHOCOLATE_ID=?
For the second however an exception occurred:
3000 [main] WARN  org.hibernate.util.JDBCExceptionReporter  - SQL Error: 1451, S
QLState: 23000
3000 [main] ERROR org.hibernate.util.JDBCExceptionReporter  - Cannot delete or u
pdate a parent row: a foreign key constraint fails (`collections`.`chocolate_fan
`, CONSTRAINT `CHOCOLATE_FAN_FK1` FOREIGN KEY (`CHOCOLATE_ID`) REFERENCES `choco
late` (`ID`))
Exception in thread "main" org.hibernate.exception.ConstraintViolationException:
 Could not execute JDBC batch update
    at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:71)
    at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java
:43)
As discussed earlier for cascade delete to work the relations must be broken completely before any deletions are attempted.
static void deleteElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    People people2 = (People) session.get(People.class, 2);
    people2.getFavouriteChocolates().clear();
    session.delete(people2);
    t.commit();
    session.close();
    t.commit();
}
This would first clear all relations in the join table before deleting the person.
2813 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        CHOCOLATE_FAN 
    where
        FAN_ID=?
2829 [main] DEBUG org.hibernate.SQL  - 
    delete 
    from
        PEOPLE 
    where
        ID=?
If we need a parent-child like relation, then we need to ensure that any mappings for the chocolates are removed first before the person and his linked chocolates are to be removed. In the next post we shall attempt a bidirectional many-to-many relation.

4 comments:

  1. Thank you this saved my day. I was having a similar issue where I only wanted the link table (CHOCOLATE_FAN) records to be deleted on deletion of PERSON, but it was trying to delete CHOCOLATE records as well. This tip helped:
    static void deleteElements() {
    Session session = sessionFactory.openSession();
    Transaction t = session.beginTransaction();
    People people2 = (People) session.get(People.class, 2);
    people2.getFavouriteChocolates().clear();
    session.delete(people2);
    t.commit();
    session.close();
    t.commit();
    }

    Rohit

    ReplyDelete
  2. Found a nice book on Spring & Hibernate. Have a look at this blog...

    http://amritendude.blogspot.in/2014/06/new-book-on-spring-4-and-hibernate-4.html

    ReplyDelete