Thursday, January 30, 2014

Hibernate with generic DAO, H2 database and manual transactions

A lot of hibernate tutorials in internet use paradigm "one dao per entity, one service per dao". So, if we have 10 entities in our application we have to create 10 dao components(each for each entity) and 10 service components. Tutorials about using generics for daos and services are not very common, unfortunately. So, let's make one more generic dao tutorial! :)

Non generic architecture: one pair Dao/Service per POJO object:

Using generics: only one pair of DAO/Service objects for all POJO objects:


0. prject generating
For generating we can use maven-archtype-quickstart maven archtype:

mvn archetype:generate -DgroupId=com.demien.hibgeneric -DartifactId=manualtx -DarchtypeArtifactId=maven-archtype-quickstart -DinteractiveMode=false


1. adding dependencies
We need to add 3 dependecies to our project: Log4j - logger, Hibernate, H2db - for H2 database support.
<properties>
  <application.name>Demien :: generic hibernate with manux transactions </application.name>
  <hibernate.version>4.2.5.Final</hibernate.version>
  <h2.version>1.3.173</h2.version>
  <log4j.version>1.2.17</log4j.version>
 </properties>
 <dependencies>
  <!-- HIBERNATE -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-core</artifactId>
   <version>${hibernate.version}</version>
  </dependency>
  <!-- H2 DB -->
  <dependency>
   <groupId>com.h2database</groupId>
   <artifactId>h2</artifactId>
   <version>${h2.version}</version>
  </dependency>
  <!-- LOG4J -->
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>${log4j.version}</version>
  </dependency>
 </dependencies>


2. Hibernate configuration.
in src/main directory we have to create sub-directory "resources". Here will create file "hibernate.cfg.xml" with settings for hibernate:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">org.h2.Driver</property>
        <property name="hibernate.connection.url">jdbc:h2:~/test</property>
        <property name="hibernate.connection.password">sa</property>
        <property name="hibernate.connection.username">sa</property>
        <property name="hibernate.dialect">org.hibernate.dialect.H2Dialect</property>
        <property name="hibernate.hbm2ddl.auto">create</property>
     
        <property name="hibernate.current_session_context_class">thread</property>
     
        <mapping class="com.demien.hibgeneric.domain.Region" />
        <mapping class="com.demien.hibgeneric.domain.Country" />
    </session-factory>
</hibernate-configuration>

it's a simple configuration for working with H2 database. Beside information about connection with H2 database, you can see 2 entities in "mapping class" tags: Region and Country - we will create them little later.

3. Log4j configuration
To make logger work properly we have to create configuration file for it.
Location of this file is the same(src/main/resources), file name - log4j.properties. Content of file:
log4j.rootCategory=INFO, console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern= %p %c: %m%n

- we will use log level "INFO" for our application. Of course in  real application it's better to use WARNING or even ERROR level.

4. Entity creating.
Let's create two simple entity.
Nothing special here. Just 2 entities with JPA tags for declaring table and column information. Implementing of Serializable class here can be ommitted. I included it because in the future this entity might be using in services with serialisation.

4.1. Region entity

package com.demien.hibgeneric.domain;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity(name = "REGIONS")
public class Region implements Serializable {

 private static final long serialVersionUID = 8268800253932817168L;
 @Id
 @Column(name = "REGION_ID")
 private Integer regionId;
 @Column(name = "REGION_NAME")
 private String regionName;

 public Region() {
 }

 public Integer getRegionId() {
  return regionId;
 }

 public void setRegionId(Integer regionId) {
  this.regionId = regionId;
 }

 public String getRegionName() {
  return regionName;
 }

 public void setRegionName(String regionName) {
  this.regionName = regionName;
 }

 @Override
 public String toString() {
  return "[REGION_ID=" + getRegionId().toString() + " REGION_NAME="
    + getRegionName() + "]";
  // return "["+getRegionId().toString()+"] "+getRegionName();
 }

}


4.2 Country entity.
It has relationship many-to-one with region entity.

package com.demien.hibgeneric.domain;

import java.io.Serializable;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Entity;

@Entity(name = "COUNTRIES")
public class Country implements Serializable {

 private static final long serialVersionUID = 7566184847780781908L;
 @Id
 @Column(name = "COUNTRY_ID")
 private String countryId;

 @Column(name = "COUNTRY_NAME")
 private String countryName;

 @ManyToOne(cascade = CascadeType.ALL)
 @JoinColumn(name = "REGION_ID")
 private Region region;

 public Country() {
 }

 public String getCountryId() {
  return countryId;
 }

 public void setCountryId(String countryId) {
  this.countryId = countryId;
 }

 public String getCountryName() {
  return countryName;
 }

 public void setCountryName(String countryName) {
  this.countryName = countryName;
 }

 public Region getRegion() {
  return region;
 }

 public void setRegion(Region region) {
  this.region = region;
 }

 @Override
 public String toString() {

  return "[COUNTRY_ID=" + getCountryId().toString() + " COUNTRY_NAME="
    + getCountryName() + " REGION=" + region.toString() + "]";
  // return "["+getCountryId()+"] "+getCountryName();
 }

}

5. Generic dao
It's most interesting part of the post. Of couce, we can create dao without any interfaces, but using pair daoInterface+daoImplementation is VERY VERY common, so let's do it this way.

5.1. Dao interface - declaring methods(contract) of dao.

package com.demien.hibgeneric.dao;

import java.util.List;
import java.util.Map;

public interface IGenericDAO<T> {
    public T get(Class<T> cl, Integer id);
    public T save(T object);
    public void update(T object);
    public void delete(T object);
    public List<T> query(String hsql, Map<String, Object> params);

}

5.2. Dao interface implementation - dao itself.  

package com.demien.hibgeneric.dao;

import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

public class GenericDAOImpl<T> implements IGenericDAO<T> {
 private Logger LOGGER;
 private SessionFactory sessionFactory;

 public GenericDAOImpl(Class<T> cl, SessionFactory sessionFactory) {
  this.LOGGER = Logger.getLogger(cl.getName() + "GenericDAO");
  this.sessionFactory = sessionFactory;
  if (sessionFactory == null)
   throw new RuntimeException("Session factory is null!!!");
 }

 @Override
 public T get(Class<T> cl, Integer id) {
  LOGGER.info("STARTED - get");
  Session session = sessionFactory.getCurrentSession();
  session.beginTransaction();
  @SuppressWarnings("unchecked")
  T element = (T) session.get(cl, id);
  session.getTransaction().commit();
  LOGGER.info("FINISHED - get");
  return element;
 }

 @Override
 public T save(T object) {
  LOGGER.info("STARTED - save");
  Session session = sessionFactory.getCurrentSession();
  session.beginTransaction();
  session.save(object);
  session.getTransaction().commit();
  LOGGER.info("FINISHED - save");
  return object;
 }

 @Override
 public void update(T object) {
  LOGGER.info("STARTED - update");
  Session session = sessionFactory.getCurrentSession();
  session.beginTransaction();
  session.update(object);
  session.getTransaction().commit();
  LOGGER.info("FINISHED - update");
 }

 @Override
 public void delete(T object) {
  LOGGER.info("STARTED - delete");
  Session session = sessionFactory.getCurrentSession();
  session.beginTransaction();
  session.delete(object);
  session.getTransaction().commit();
  LOGGER.info("FINISHED - delete");
 }

 @SuppressWarnings("unchecked")
 @Override
 public List<T> query(String hsql, Map<String, Object> params) {
  LOGGER.info("STARTED - query");
  Session session = sessionFactory.getCurrentSession();
  session.beginTransaction();
  Query query = session.createQuery(hsql);
  if (params != null) {
   for (String i : params.keySet()) {
    query.setParameter(i, params.get(i));
   }
  }

  List<T> result = null;
  if ((hsql.toUpperCase().indexOf("DELETE") == -1)
    && (hsql.toUpperCase().indexOf("UPDATE") == -1)
    && (hsql.toUpperCase().indexOf("INSERT") == -1)) {
   result = query.list();
   LOGGER.info("FINISHED - query. Result size=" + result.size());
  } else {
   LOGGER.info("FINISHED - query. ");
  }
  session.getTransaction().commit();
  return result;
 }
}

As you can see, we use generics in out dao without any binding  to real entities!  

6. Generic service 
Here, the same thing as for previous step: interface+implementation. Service interface is extending dao interface to add some more useful methods.

6.1. Service Interface 

package com.demien.hibgeneric.service;
import java.util.List;
import com.demien.hibgeneric.dao.IGenericDAO;

public interface IGenericService<T> extends IGenericDAO<T> {
  List<T> getAll();
  void deleteAll();
}

6.2. Service
 Mostly service methods just calling correspondig mothods of dao. Mostly, except 2 methods extended in service interface: the use dao.query method this different sql statements.  
package com.demien.hibgeneric.service;
import java.util.List;
import java.util.Map;
import org.hibernate.SessionFactory;
import com.demien.hibgeneric.dao.GenericDAOImpl;
import com.demien.hibgeneric.dao.IGenericDAO;

public class GenericServiceImpl<T> implements IGenericService<T> {
 private IGenericDAO<T> dao;
 private Class<T> cl;

 public GenericServiceImpl(Class<T> cl, SessionFactory sessionFactory) {
  this.cl=cl;
  dao=new GenericDAOImpl<T>(cl, sessionFactory);
 }

 @Override
 public T get(Class<T> cl, Integer id) {
  //LOGGER.trace("STARTED - get");
  return (T) dao.get(cl, id);
 }

 @Override
 public T save(T object) {
  return (T)dao.save(object);
 }

 @Override
 public void update(T object) {
  dao.update(object);  
 }

 @Override
 public void delete(T object) {
  dao.delete(object);  
 }

 @Override
 public List<T> query(String hsql, Map<String, Object> params) {
  return (List<T>)dao.query(hsql, params);
 }

 @Override
 public List<T> getAll() {
  return query("from "+cl.getName(), null);
 }

 @Override
 public void deleteAll() {
  query("delete from "+cl.getName(),null);
  
 }
}
The same as with dao: just generics, no binding with real entity classes.


7. HibernateUtil class 
What a hibernate based program without HibernateUtil class? :)
It's a regular "helper" class for working with Hibernate. We will use getSessionFactory method from it.
package com.demien.hibgeneric;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.ServiceRegistryBuilder;

public class HibernateUtil {
    private static final ServiceRegistry serviceRegistry;
    private static final SessionFactory ourSessionFactory;
    
    static {
        try {
            Configuration configuration = new Configuration();
            configuration.configure();
            serviceRegistry = new ServiceRegistryBuilder().applySettings(configuration.getProperties()).buildServiceRegistry();
            ourSessionFactory = configuration.buildSessionFactory(serviceRegistry);
        } catch (Throwable ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    
    public static SessionFactory getSessionFactory() {
     return ourSessionFactory;
    }
    
    public static Session getSession() throws HibernateException {
        return ourSessionFactory.openSession();
    }
}

8. Main application class
And now we finally ready to execute out application. In listing we will create 2 services for 2 different entities using ONLY ONE GENERIC SERVICE CLASS! Of course, nothing can stop you for using more 2 (as many as you want)  with one service. Now in case of changing(for instance, changing transaction strategy) in DB part - we will have to change ONLY ONE DAO CLASS.

package com.demien.hibgeneric;

import java.util.List;

import com.demien.hibgeneric.service.GenericServiceImpl;
import com.demien.hibgeneric.service.IGenericService;
import com.demien.hibgeneric.domain.Country;
import com.demien.hibgeneric.domain.Region;

public class App {
 public static void main(String[] args) {
  Region region=null;
  Country country=null;
  
  IGenericService<Region> regionService = new GenericServiceImpl<Region>(
    Region.class, HibernateUtil.getSessionFactory());
  IGenericService<Country> countryService = new GenericServiceImpl<Country>(
    Country.class, HibernateUtil.getSessionFactory());
  regionService.deleteAll();

  region = new Region();
  region.setRegionId(1);
  region.setRegionName("Africa");
  regionService.save(region);
  
  country=new Country();
  country.setCountryId("EGY");
  country.setCountryName("Egypt");
  country.setRegion(region);
  countryService.save(country);
  
  country=new Country();
  country.setCountryId("TUN");
  country.setCountryName("Tunis");
  country.setRegion(region);
  countryService.save(country);
  
  
  region = new Region();
  region.setRegionId(2);
  region.setRegionName("America");
  regionService.save(region);
  
  country=new Country();
  country.setCountryId("CAN");
  country.setCountryName("Canada");
  country.setRegion(region);
  countryService.save(country);
  
  country=new Country();
  country.setCountryId("USA");
  country.setCountryName("USA");
  country.setRegion(region);
  countryService.save(country);

  List<Country> countryList = countryService.getAll();
  if (countryList != null) {
   for (Country c : countryList) {
    System.out.println(c.toString());
   }
  }

 }
}
9. Result
All works fine !
[COUNTRY_ID=EGY COUNTRY_NAME=Egypt REGION=[REGION_ID=1 REGION_NAME=Africa]]
[COUNTRY_ID=TUN COUNTRY_NAME=Tunis REGION=[REGION_ID=1 REGION_NAME=Africa]]
[COUNTRY_ID=CAN COUNTRY_NAME=Canada REGION=[REGION_ID=2 REGION_NAME=America]]
[COUNTRY_ID=USA COUNTRY_NAME=USA REGION=[REGION_ID=2 REGION_NAME=America]]

p.s.
final directory structure

sources can be downloaded from here.