Background
This is a note on Hibernate one to many mappings. Before working on the example, I would like to clear a couple of commonly used terminologies in Hibernate.
SessionFactory vs. Session
- SessionFactory - A
SessionFactory
in Hibernate is an object that has all the configuration information on how to connect to a database.
- A
SessionFactory
is immutable; - The behavior of a
SessionFactory
is controlled by the properties supplied at the time of configuration; - Creating a
SessionFactory
is a slow and heavy-weight process; - An application usually has only a single
SessionFactory
; - The
SessionFactory
object is thread-safe.
- Session - A
session
in Hibernate is an object that communicates with the database.
- Each
session
has its own database connection; - A
session
is created by the openSession()
method of the SessionFactory
object; - Creating a
session
object is a light-weight process; - A
session
should be closed whenever the database operation is completed to release the database connection; - A
session
object is not thread-safe.
Transient vs. Persistent vs. Detached
According to the document on Hibernate Session, an entity object can have three states:
- An entity object is transient if it is created out side of a
Hibernate session
and it is never associated to a Hibernate session
; - An entity object is persistent if it has been associated to a
Hibernate session
;
- A transient object can be added to the
session
by the session.save()
or session.persist()
methods; - If an entity object is load/get from the database through a
Hibernate session
, it is in the persistent state.
- A persistent object becomes a detached object if the
session
is closed or the Session.evict() method is called on this object.
The FlushMode
The FlushMode represents a flushing strategy. The flush process synchronizes database state with session
state by detecting state changes and executing SQL statements. Hibernate has four flush modes.
ALWAYS
- The Session
is flushed before every query; AUTO
- The Session
is sometimes flushed before query execution in order to ensure that queries never return stale state; COMMIT
- The Session
is flushed when Transaction.commit()
is called; MANUAL
- The Session
is only ever flushed when Session.flush()
is explicitly called by the application.
The Example
The attached is a Maven project that performs a set of unit tests on the CRUD operations related to entities of one to many mappings. The example uses a MySQL database. If you are not familiar with MySQL, you can take a look at my earlier note. You can issue the following SQL to create the tables used in the tests.
DROP DATABASE IF EXISTS experimentA;
CREATE DATABASE experimentA;
USE experimentA;
CREATE TABLE Parent (
Id int(11) NOT NULL AUTO_INCREMENT,
Name varchar(45) NOT NULL,
PRIMARY KEY (Id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE Child (
Id int(11) NOT NULL AUTO_INCREMENT,
ParentID int(11) NOT NULL,
Name varchar(45) NOT NULL,
PRIMARY KEY (Id),
FOREIGN KEY (ParentID) REFERENCES Parent (Id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE GrandChild (
Id int(11) NOT NULL AUTO_INCREMENT,
ParentID int(11) NOT NULL,
Name varchar(45) NOT NULL,
PRIMARY KEY (Id),
FOREIGN KEY (ParentID) REFERENCES Child (Id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The [Child]
table has a foreign key reference on the [Parent]
table, and the [GrandChild]
table has a foreign key reference on the [Child]
table. The following are the corresponding Hibernate
entity classes.
package com.song.example.hibernate.entities;
import java.util.HashSet;
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.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "Parent")
public class Parent {
private Integer id;
private String name;
private Set<Child> children = new HashSet<Child>();
public Parent() {}
public Parent(String name) { this.name = name; }
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id")
public Integer getId() { return this.id; }
public void setId(Integer id) { this.id = id; }
@Column(name = "Name", length = 100)
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL)
public Set<Child> getChildren() { return this.children; }
public void setChildren(Set<Child> children) { this.children = children; }
}
package com.song.example.hibernate.entities;
import java.util.HashSet;
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.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
@Entity
@Table(name = "Child")
public class Child {
private Integer id;
private String name;
private Parent parent;
private Set<GrandChild> grandChildren = new HashSet<GrandChild>();
public Child() {}
public Child(String name, Parent parent) {
this.name = name;
this.parent = parent;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id")
public Integer getId() { return this.id; }
public void setId(Integer id) { this.id = id; }
@Column(name = "Name", length = 100)
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ParentID", nullable = false)
public Parent getParent() { return this.parent; }
public void setParent(Parent parent) { this.parent = parent; }
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent", cascade = CascadeType.ALL)
public Set<GrandChild> getChildren() { return this.grandChildren; }
public void setChildren(Set<GrandChild> grandChildren) {
this.grandChildren = grandChildren;
}
}
package com.song.example.hibernate.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "GrandChild")
public class GrandChild {
private Integer id;
private String name;
private Child parent;
public GrandChild() {}
public GrandChild(String name, Child parent) {
this.name = name;
this.parent = parent;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "Id")
public Integer getId() { return this.id; }
public void setId(Integer id) { this.id = id; }
@Column(name = "Name", length = 100)
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ParentID", nullable = false)
public Child getParent() { return this.parent; }
public void setParent(Child parent) { this.parent = parent; }
}
In order to create a Hibernate SessionFactory
, the following hibernate.cfg.xml file is added to the resources directory.
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">
com.mysql.cj.jdbc.Driver
</property>
<property name="connection.url">
jdbc:mysql://localhost/experimentA
</property>
<property name="connection.username">root</property>
<property name="connection.password">password</property>
<property name="dialect">
org.hibernate.dialect.MySQLDialect
</property>
<property name="cache.provider_class">
org.hibernate.cache.NoCacheProvider
</property>
<property name="org.hibernate.flushMode">MANUAL</property>
<property name="show_sql">false</property>
<property name="format_sql">false</property>
<mapping class="com.song.example.hibernate.entities.Parent" />
<mapping class="com.song.example.hibernate.entities.Child" />
<mapping class="com.song.example.hibernate.entities.GrandChild" />
</session-factory>
</hibernate-configuration>
The SessionFactoryInstance.java is used to create a singleton instance of the SessionFactory
. It is probably not the best way to create a SessionFactory
, but it saves me some typing for a small unit test project.
package com.song.example.hibernate;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public final class SessionFactoryInstance {
private SessionFactoryInstance(){}
public final static SessionFactory Instance
= new Configuration().configure().buildSessionFactory();
}
The SessionFactory
is used in the PCG_Test.java to perform the unit tests on the CRUD operations.
package com.song.example.hibernate;
import java.sql.Statement;
import org.hibernate.FlushMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.testng.Assert;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import com.song.example.hibernate.entities.Child;
import com.song.example.hibernate.entities.GrandChild;
import com.song.example.hibernate.entities.Parent;
public class PCG_Test {
private final SessionFactory sessionFactory
= SessionFactoryInstance.Instance;
private Parent parent = null;
private Child child = null;
private GrandChild grandChild = null;
@BeforeTest
public void Init() {
Assert.assertNotNull(sessionFactory);
try (Session session = sessionFactory.openSession()) {
FlushMode flushmode = session.getHibernateFlushMode();
Assert.assertEquals(flushmode, FlushMode.MANUAL);
session.doWork(connection -> {
try (Statement statement = connection.createStatement()) {
statement.execute("SET FOREIGN_KEY_CHECKS = 0");
statement.execute("TRUNCATE TABLE GrandChild;");
statement.execute("TRUNCATE TABLE Child;");
statement.execute("TRUNCATE TABLE Parent;");
statement.execute("SET FOREIGN_KEY_CHECKS = 1");
}
});
}
this.parent = new Parent("Parent");
this.child = new Child("Child", this.parent);
this.grandChild = new GrandChild("Grand child", this.child);
this.parent.getChildren().add(this.child);
this.child.getChildren().add(this.grandChild);
}
@Test
public void Insert_Test() {
try (Session session = sessionFactory.openSession()) {
Transaction tx = session.getTransaction();
try {
tx.begin();
session.save(parent);
session.flush();
tx.commit();
}
catch(Exception e) {
tx.rollback();
Assert.fail("Failed - Insert_Test");
}
}
try (Session session = sessionFactory.openSession()) {
Parent parent = session.get(Parent.class, this.parent.getId());
Child child = parent.getChildren().iterator().next();
GrandChild grandChild = child.getChildren().iterator().next();
Assert.assertEquals(parent.getName(), this.parent.getName());
Assert.assertEquals(child.getName(), this.child.getName());
Assert.assertEquals(grandChild.getName(), this.grandChild.getName());
}
}
@Test(dependsOnMethods = {"Insert_Test"})
public void Update_Test() {
final String newParentName = "New Parent Name";
final String newChildName = "New Child Name";
final String newGrandChildName = "New Grand Child Name";
try (Session session = sessionFactory.openSession()) {
Transaction tx = session.getTransaction();
Integer id = this.parent.getId();
try {
tx.begin();
Parent parent = session.get(Parent.class, id);
Child child = parent.getChildren().iterator().next();
GrandChild grandChild = child.getChildren().iterator().next();
parent.setName(newParentName);
child.setName(newChildName);
grandChild.setName(newGrandChildName);
session.flush();
tx.commit();
}
catch(Exception e) {
tx.rollback();
Assert.fail("Failed - Update_Test");
}
}
try (Session session = sessionFactory.openSession()) {
Parent parent = session.get(Parent.class, this.parent.getId());
Child child = parent.getChildren().iterator().next();
GrandChild grandChild = child.getChildren().iterator().next();
Assert.assertEquals(parent.getName(), newParentName);
Assert.assertEquals(child.getName(), newChildName);
Assert.assertEquals(grandChild.getName(), newGrandChildName);
}
}
@Test(dependsOnMethods = {"Update_Test"})
public void Delete_Child_Test() {
try (Session session = sessionFactory.openSession()) {
Transaction tx = session.getTransaction();
Integer id = this.parent.getId();
try {
tx.begin();
Parent parent = session.get(Parent.class, id);
Child child = parent.getChildren().iterator().next();
parent.getChildren().remove(child);
session.delete(child);
session.flush();
tx.commit();
}
catch(Exception e) {
tx.rollback();
Assert.fail("Failed - Delete_Child_Test");
}
}
try (Session session = sessionFactory.openSession()) {
Parent parent = session.get(Parent.class, this.parent.getId());
Assert.assertEquals(parent.getChildren().size(), 0);
}
}
@Test(dependsOnMethods = {"Delete_Child_Test"})
public void Delete_ALL_Test() {
try (Session session = sessionFactory.openSession()) {
Transaction tx = session.getTransaction();
Integer id = this.parent.getId();
try {
tx.begin();
Parent parent = session.get(Parent.class, id);
session.delete(parent);
session.flush();
tx.commit();
}
catch(Exception e) {
tx.rollback();
Assert.fail("Failed - Delete_ALL_Test");
}
}
try (Session session = sessionFactory.openSession()) {
Parent parent = session.get(Parent.class, this.parent.getId());
Assert.assertNull(parent);
}
}
}
- The
Init()
method clears all the data in the database to give the test a fresh start; - The
Insert_Test()
method inserts all the data into the database. The session.save()
is only applied to the parent object, but all the child and grand child objects are saved cascadingly; - The
Update_Test()
method updates the data that have already saved in the database; - The
Delete_Child_Test()
method deletes a child from the parent object; - The
Delete_ALL_Test()
method deletes all the objects from the database. The session.delete()
is only applied to the parent object, but all the child and grand child objects are deleted cascadingly.
Run the Unit Tests
To run the unit test, you need to have the MySQL server running. You can start the MySQL server in a Linux system by the following command:
sudo service mysql start
You can then run the unit tests by the Maven command.
mvn test
If you load the project into Eclipse, you can also run the unit test by the TestNG plug-in.
Points of Interest
- This is a note on Hibernate one to many mappings.
- I hope you like my posts and I hope this note can help you in one way or the other.
History
- 24th December, 2017: First revision