Thursday, December 17, 2009

Service Layer and Transactions: Sprin...

Service Layer and Transactions: Spring AOP used to apply declarative transactions 


(belongs to this series of articles)

Building on top of the reusable DAO framework that introduces generic DAOs for the simplest CRUD operations (create, update, delete), we are now implementing the Service layer. Why do we need the service layer at all? Indeed, as the framework does not, and can not, implement any non-trivial business methods in the service layer, why would one want to create it at all? After all, as would be seen, the service layer does simple pass-through to DAOs for the generic CRUD operations. The answer, or answers, are as follows:

  • we need to set the transaction boundary somewhere, and the DAOs seem to be the wrong place to do that, because giving them the extra concern of managing transactions would mean they wouldn't be easily usable when we have to aggregate several data object operations in one transaction (e.g. updating the main record and dependent records in a one-to-many would have a difficulty re-using the DAO for the other part of the relationship, because it would then be used within a transaction. This could be handled via declarative transaction (e.g. setting transaction level to "required"), but making DAOs transaction-agnostic seems to be a more flexible approach

  • setting the transaction boundary is better done where we define the business operations, because they are more coarse-grained than the "retrieve-save-delete" level for single domain objects; business operations (like "create invoice" as opposed to "create update invoice item") would tend to group several data access operations and would, in the most wide-spread case, be the natural place to demarcate transactions (this is especially true with Spring's declarative transactions that typically are set at the method level) 
In fact, what is being proposed is just one of the possible transaction strategies (more on this in this series of articles); however, this -- defining the transactions declaratively in the service layer using Spring configuration -- seems to be the simplest and most flexible option.

Here's how we could define the Generic Crud Service interface:

/**
 * 
 */
package com.example.crud.service;

import java.io.Serializable;
import java.util.Collection;

import com.example.domain.DomainObject;

/**
 * Generic CRUD service. Methods throw unchecked exceptions for errors; no
 * assumptions as to the type of the exception is made.
 * 
 * @author maxim
 * 
 * @param <T>
 *            domain object type
 * @param <PK>
 *            primary key type
 */
public interface GenericCrudService<T extends DomainObject<PK>, PK extends Serializable> {
 /**
  * 
  * @return a collection of all the entities (can be large)
  * @throws @{@link RuntimeException} unchecked exception for any error
  */
 public Collection<T> getAll();

 /**
  * Update persistent entity
  * 
  * @param emp
  *            data object (contains primary key)
  * @return updated entity (contains fields that could change during update,
  *         e.g. version)
  * @throws @{@link RuntimeException} unchecked exception for any error
  */
 public T update(T emp);

 /**
  * Deletes by primary key
  * 
  * @param id
  *            primary key
  * @throws @{@link RuntimeException} unchecked exception if doesn't exist or
  *         error
  */
 public void delete(PK id);

 /**
  * 
  * @param id
  *            primary key value
  * @return one object
  * @throws @{@link RuntimeException} unchecked exception if doesn't exist or
  *         error
  */
 public T get(PK id);

 /**
  * Inserts new entity
  * 
  * @param emp
  * @throws @{@link RuntimeException} unchecked exception for any error (e.g.
  *         integrity constraint violation)
  */
 public void insert(T emp);

 /**
  * Generic pagination
  * 
  * @param pageSize
  *            page size
  * @param firstRecord
  *            first record (starts with 0)
  * @param order
  *            field name to indicate order to the persistence layer
  *            (typically an entity field name)
  * @param asc
  *            true if ascending, else descending sort order
  * @return a collection containing single page of data, size <= pageSize
  * @throws @{@link RuntimeException} unchecked exception for errors
  */
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc);
 /**
  * Generic pagination
  * 
  * @param pageSize
  *            page size
  * @param firstRecord
  *            first record (starts with 0)
  * @param order
  *            field name to indicate order to the persistence layer
  *            (typically an entity field name)
  * @param asc
  *            true if ascending, else descending sort order
  * @return a collection containing single page of data, size <= pageSize
  * @throws @{@link RuntimeException} unchecked exception for errors
  */
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc, String criteria, Object ... objects );

 /**
  * Get total number of entities in persistent store
  * 
  * @return number
  * @throws @{@link RuntimeException} unchecked exception
  */
 public Long getTotal();

}


... and the implementation:

/**
 * 
 */
package com.example.crud.service;

import java.io.Serializable;
import java.util.Collection;

import com.example.crud.dao.GenericDao;
import com.example.crud.dao.GenericDaoConsumer;
import com.example.domain.DomainObject;

/**
 * Implements {@link GenericCrudService} with a {@link GenericDao}
 * 
 * @author maxim
 * 
 */
public class GenericCrudDaoService<T extends DomainObject<PK>, PK extends Serializable>
  implements GenericCrudService<T, PK>, GenericDaoConsumer<T, PK> {

 protected GenericDao<T, PK> dao;

 /*
  * (non-Javadoc)
  * 
  * @see com.example.prototype.service.TService#deleteT(java.lang.PK)
  */
 @Override
 public void delete(PK id) {
  dao.remove(id);
 }

 /*
  * (non-Javadoc)
  * 
  * @see com.example.prototype.service.TService#getAllTs()
  */
 @Override
 public Collection<T> getAll() {
  return dao.findAll();
 }

 /*
  * (non-Javadoc)
  * 
  * @see com.example.prototype.service.TService#getT(java.lang.PK)
  */
 @Override
 public T get(PK id) {
  return dao.findById(id);
 }

 /*
  * (non-Javadoc)
  * 
  * @see
  * com.example.prototype.service.TService#insertT(com.example.prototype
  * .entities.T)
  */
 @Override
 public void insert(T m) {
  dao.create(m);
 }

 /*
  * (non-Javadoc)
  * 
  * @see
  * com.example.prototype.service.TService#updateT(com.example.prototype
  * .entities.T)
  */
 @Override
 public T update(T m) {
  return dao.update(m);
 }

 /**
  * @see GenericCrudService
  */
 @Override
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc) {
  return dao.getPage(pageSize, firstRecord, order, asc);
 }

 @Override
 public Long getTotal() {
  return dao.getTotalEntities();
 }

 @Override 
 public void setDao(GenericDao<T, PK> dao) {
  this.dao = dao;
 }

 public GenericDao<T, PK> getDao() {
  return dao;
 }

 @Override
 public Collection<T> getPage(int pageSize, int firstRecord, String order,
   boolean asc, String criteria, Object... objects) {
  return dao.getPage(pageSize, firstRecord, order, asc, criteria, objects);
 }

}


A simple class extending GenericCrudDaoService does not need to implement any operations (provided all it does is simple create-retrieve-update-delete:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.example.project.service;

import com.example.crud.service.GenericCrudDaoService;
import com.example.project.domain.UserData;

/**
 *
 * @author maxim
 */
public class UserDetailsDaoService extends GenericCrudDaoService<UserData, String> implements UserDetailsService {

}

As can be seen, we don't need to add any operations if we only need a CRUD service. This class is referenced in the Spring configuration quoted below.

Note that the source code doesn't have any transaction declarations either! This is because, instead of annotations, we are using Spring's AOP to apply transaction handling at the method level:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
 xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
 xmlns:aop="http://www.springframework.org/schema/aop"
 xsi:schemaLocation="
            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">


 <!--
  Service layer. Defines transaction processing. Services are
  implemented directly using GenericDaoService or its subclasses
 -->

 <context:annotation-config />

 <tx:annotation-driven />


       
 <bean id="userSvc" class="com.example.project.service.UserDetailsDaoService" >
  <property name="dao" ref="myUserDetailsService"></property>
 </bean>


 <!--
  These pointcuts work for the services that subclass generic services
  from com.example.crud.service
 -->
 <aop:config>
  <aop:pointcut id="otherServiceMethods"
   expression="execution(* com.example.project.service.*.*(..))" />
  <aop:pointcut id="updateServiceMethods"
   expression="execution(* com.example.project.service.*.update*(..))" />
  <aop:pointcut id="deleteServiceMethods"
   expression="execution(* com.example.project.service.*.delete*(..))" />
  <aop:pointcut id="insertServiceMethods"
   expression="execution(* com.example.project.service.*.insert*(..))" />
  <aop:pointcut id="otherGenericServiceMethods"
   expression="execution(* com.example.crud.service.*.*(..))" />
  <aop:pointcut id="updateGenericServiceMethods"
   expression="execution(* com.example.crud.service.*.update*(..))" />
  <aop:pointcut id="deleteGenericServiceMethods"
   expression="execution(* com.example.crud.service.*.delete*(..))" />
  <aop:pointcut id="insertGenericServiceMethods"
   expression="execution(* com.example.crud.service.*.insert*(..))" />
   
  <aop:advisor advice-ref="txAdvice" pointcut-ref="updateServiceMethods" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="updateGenericServiceMethods" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="deleteServiceMethods" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="deleteGenericServiceMethods" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="insertServiceMethods" />
  <aop:advisor advice-ref="txAdvice" pointcut-ref="insertGenericServiceMethods" />
  <aop:advisor advice-ref="supportsAdvice" pointcut-ref="otherServiceMethods" />
  <aop:advisor advice-ref="supportsAdvice" pointcut-ref="otherGenericServiceMethods" />
  
 </aop:config>

 <tx:advice id="txAdvice" transaction-manager="transactionManager">
  <tx:attributes>
   <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT"
    read-only="false" />
  </tx:attributes>
 </tx:advice>

 <tx:advice id="supportsAdvice" transaction-manager="transactionManager">
  <tx:attributes>
   <tx:method name="*" propagation="SUPPORTS" read-only="true" />
  </tx:attributes>
 </tx:advice>


</beans>

No comments: