Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

A Note on Hibernate - One To Many

5.00/5 (3 votes)
24 Dec 2017CPOL4 min read 10.9K   57  
Hibernate one to many mappings

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

Image 1

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.

SQL
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.

Java
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; }
}
Java
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;
    }
}
Java
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.

XML
<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.

Java
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.

Java
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");
                }
            });    
        }
        
        // Initiate test data
        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");
            }
        }
        
        // Validate data is inserted
        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");
            }
        }
        
        // Validate data is updated
        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");
            }
        }
        
        // Validate child is deleted
        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");
            }
        }
        
        // Validate all data is deleted
        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.

Image 2

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)