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.
Chocolate.hbm.xml
I then executed a code to first remove a chocolate from a person's favorite list
and then delete the person itself
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 personstatic 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.
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:
ReplyDeletestatic 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
Good one man
ReplyDeleteFound a nice book on Spring & Hibernate. Have a look at this blog...
ReplyDeletehttp://amritendude.blogspot.in/2014/06/new-book-on-spring-4-and-hibernate-4.html
Sure dude, will check it out. Thanks !
Delete