Wednesday, September 17, 2014

Active record domain model in Java with Spring and Hibernate

1. Goal.

As a developer, I want to create ability to use just entity object for performing base CRUD operation in DB, like:
myObject.save()
myObject.update()
myObject.delete()

2. Problem. 

The main problem here is obvious: transaction management. What it we are executing in one transaction:
object1.save();
object2.save();
and after that we have to rollback our transaction ? In case of anemic domain model business logic located at another(service) level:

@Transactional
pubic void doSomething(Object1 object1, Object2 object2) {
       Session session=sessionFactory.getCurrentSession();
       service1.save();
       service2.save();
       session.rollback();
}

Using anemic mode, at service level we can just mark method as @Transactional,  execute whole list of operations and after that  - commit or rollback transaction.
Using rich domain model, we have to think about transaction management.

3. Application context file.

In application context file we have to define data source bean(with connection to H2 database), session factory(with one domain class) bean and  abstract model bean - main(parent) class for our rich domain model.

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd>">

    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.h2.Driver" />
        <property name="url" value="jdbc:h2:~/test" />
        <property name="username" value="sa" />
        <property name="password" value="sa" />
    </bean>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- Don't forget to list all your entity classes here -->
        <property name="annotatedClasses">
            <list>
                <value>com.demien.richdomain.model.User</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">create</prop>
            </props>
        </property>
    </bean>

    <bean id="AbstractModel" class="com.demien.richdomain.model.AbstractModel">
        <property name="sessionFactory" ref="sessionFactory" />
    </bean>

</beans>

4. Abstract model

This class could be much simpler, but because of logic related with transactions I added few more procedures: setAutoCommit, setSession, checkAndStartTransaction, checkAndCommitTransaction.

package com.demien.richdomain.model;

import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

public class AbstractModel<T, PK extends Serializable> {

    protected static SessionFactory sessionFactory;
    protected Session session;
    protected boolean autoCommit=true;

    private Class<T> cl;

    public AbstractModel() {
    }

    public AbstractModel(Class<T> cl) {
        this.cl=cl;
    }

    public static SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public static void setSessionFactory(SessionFactory sessionFactory) {
        AbstractModel.sessionFactory = sessionFactory;
    }

    protected Session getCurrentSession() {

        try {
            session=getSessionFactory().getCurrentSession();
        } catch (Exception e ) {

        }
        if (session == null) {
            session = getSessionFactory().openSession();
        }
        return session;
    }

    public Session getNewSession() {
        return getSessionFactory().openSession();
    }

    protected void setSession(Session session) {
        this.session=session;
    }

    protected void setAutoCommit(boolean autoCommit) {
        this.autoCommit=autoCommit;
    }

    public void checkAndStartTransaction() {
        if (autoCommit) {
            Session session = getCurrentSession();
            session.beginTransaction();
        }
    }

    public void checkAndCommitTransaction() {
        if (autoCommit) {
            session.getTransaction().commit();
        }
    }

    public T get(PK id) {
        Session session = getCurrentSession();
        checkAndStartTransaction();
        T element = (T) session.get(cl, id);
        checkAndCommitTransaction();
        return element;
    }

    public PK save() {
        Session session = getCurrentSession();
        checkAndStartTransaction();
        PK result=(PK)session.save(this);
        checkAndCommitTransaction();
        return result;
    }

    public void update() {
        Session session = getCurrentSession();
        checkAndStartTransaction();
        session.update(this);
        checkAndCommitTransaction();
    }

    public void delete() {
        Session session = getCurrentSession();
        checkAndStartTransaction();
        session.delete(this);
        checkAndCommitTransaction();
    }

    public List<T> query(String hsql, Map<String, Object> params) {

        Session session = getCurrentSession();
        checkAndStartTransaction();

        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();
        } else {
            query.executeUpdate();
        }
        checkAndCommitTransaction();
        return result;
    }


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

    public void deleteAll() {
        query("delete from "+cl.getName(),null);

    }

}

Now we can just extend from this class: all child classes will have ability to work with database. 

5.Domain class which extends AbstactModel

It's just a POJO class which extends AbstractModel.

package com.demien.richdomain.model;

import java.io.Serializable;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity(name = "USER")
public class User extends AbstractModel<User, Integer> implements Serializable {

    private static final long serialVersionUID = -7022793569839517729L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Integer id;
    
    @Column(name = "LOGIN")
    private String login;
    
    @Column(name = "PASSWORD")
    private String password;
    

    public User() {
        super(User.class);
    }
    
    public User(String login, String password) {
        this();
        this.setLogin(login);
        this.setPassword(password);
    }
    
    public User(Integer id, String login, String password) {
        this();
        this.setId(id);
        this.setLogin(login);
        this.setPassword(password);
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getLogin() {
        return login;
    }

    public void setLogin(String login) {
        this.login = login;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
    
    @Override
    public boolean equals(Object object) {
        if (!(object instanceof User)) {
            return false;
        } else {
            User user = (User) object;
            if (user.getId().equals(this.getId())
                    && user.getLogin().equals(this.getLogin())
                    && user.getPassword().equals(this.getPassword())) {
                return true;
            }
            return false;
        }
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", login=" + login + ", password=" + password
                + "]";
    }

}

6. Test with "simple transaction"

Let's test our class with "simple transaction": just one object associated with transaction.
    @Test
    public void simpleTest() {
        User user=new User("login", "password");
        int cnt0=user.getAll().size();

        Integer id=user.save();

        int cnt1=user.getAll().size();
        Assert.assertEquals(cnt0+1, cnt1);

        User user1=user.get(id);
        Assert.assertEquals(user, user1);

        user.setLogin("login_updated");
        user.update();

        user1.get(id);
        Assert.assertEquals("login_updated", user1.getLogin());
    }

So far, so good: it's very simple and easy to work with database by our object. 


7. Tests with "complex transaction"

If we want to use several objects associated with transaction logic is becoming much more complicated. We have to turn off transaction start(setAutoCommit method) in every object and also we have to provide all transaction object with one session object(setSession method).

  
    @Test
    public void transactionRollbackTest() {
        User user1=new User("login1","password1");
        User user2=new User("login2","password2");
        int cnt0=user1.getAll().size();

        user1.setAutoCommit(false);
        user2.setAutoCommit(false);

        Session session=user1.getNewSession();
        user1.setSession(session);
        user2.setSession(session);

        session.beginTransaction();
        user1.save();
        user2.save();
        session.getTransaction().rollback();

        int cnt1=user1.getAll().size();
        Assert.assertEquals(cnt0, cnt1);

    }

    @Test
    public void transactionCommitTest() {
        User user1=new User("login1","password1");
        User user2=new User("login2","password2");
        int cnt0=user1.getAll().size();

        user1.setAutoCommit(false);
        user2.setAutoCommit(false);

        Session session=user1.getNewSession();
        user1.setSession(session);
        user2.setSession(session);

        session.beginTransaction();
        user1.save();
        user2.save();
        session.getTransaction().commit();

        int cnt1=user1.getAll().size();
        Assert.assertEquals(cnt0+2, cnt1);

    }

8. Conclusion

As for me, active record model can be used in projects with simple transactions. But if there are a lot of complex transaction it's better to use anemic model with logic at service(not domain) level.  

Source code can be downloaded from here.

No comments:

Post a Comment