DRY iBatis DAOs build an `UserDetailsService`

Don't repeat the DAO! with iBatis. Creating a spring-security UserDetailsService based on an iBatis DAO layer to use inside an OSGi environment. DRY Don't repeat yourself - an implementation based on iBatis. Inspired by Per Mellqvist's Don't repeat the DAO! I tried to implement an UserService based on Spring's SqlMapClientDaoSupport. In the near future the UserService will be used by this Pebble installation. So it feels natural to create some OSGi bundles:

  • util.dao - holding the DAO interfaces and the FinderIntroductionInterceptor
  • util.dao.ibatis - iBatis based implementation: IbatisCrudDaoImpl
  • util.hsqldb - provides a javax.sql.DataSource as OSGi service
  • userservice.model - yeah right, a User;-)
  • userservice.dao - holds the iBatis based persistence layer with the UserDao
  • userservice.service - the transactional service layer: UserService
  • userservice.security - a thin adapter implementation publishing the service as UserDetailsService inside the OSGi container

An implementation of FinderIntroductionInterceptor using annotations. Every call to a method named find* on a type implementing CrudDao will be intercepted and the finder code will be executed. In our case IbatisCrudDaoImpl.executeFinder() will be called.

@Aspect
public final class FinderIntroductionInterceptor implements Advice {

  @Around(value = "execution(* de.datenkollektiv.util.dao.CrudDao+.find*(..))")
  public Object invoke(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    FinderExecutor finderExecutor = (FinderExecutor) proceedingJoinPoint.getThis();
    // check if the called function is a finder method
    String methodName = proceedingJoinPoint.getSignature().getName();
    Object[] arguments = proceedingJoinPoint.getArgs();
    // execute introduced method
    return finderExecutor.executeFinder(methodName, arguments);
  }
}

Let's have a closer look at the iBatis based CrudDao: It extends Spring's SqlMapClientDaoSupport and implements the needed FinderExecutor as shown in Don't repeat the DAO! modified to use iBatis not Hibernate:

public class IbatisCrudDaoImpl<entity> extends SqlMapClientDaoSupport implements CrudDao<entity>, FinderExecutor<entity> {
  // Type of the entities to persist.
  private Class<entity> entityClass = null;
  ...
  public Entity create(final Entity newEntity) {
    getSqlMapClientTemplate().insert(entityName + ".create", newEntity);
    return newEntity;
  }
  ...
  public final List<entity> executeFinder(final String methodName, final Object[] arguments) {
    final String queryName = entityClass.getSimpleName() + "." + methodName;
    if (arguments.length == 0) {
      return getSqlMapClientTemplate().queryForList(queryName);
    }
    if (arguments.length == 1) {
      if (String.class.isAssignableFrom(arguments[0].getClass())) {
        return getSqlMapClientTemplate().queryForList(queryName, (String) arguments[0]);
      }
    }
    return getSqlMapClientTemplate().queryForList(queryName, arguments);
  }
}

Some of the Spring glue XML: No surprise compared with the Hibernate implementation mentioned above.

<bean id="abstractIbatisCrudDaoImpl" class="de.datenkollektiv.util.dao.impl.IbatisCrudDaoImpl" abstract="true">
  <property name="sqlMapClient">
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"
      p:configLocation="classpath:userservice-sql-map-config.xml">
      <property name="dataSource" ref="dataSource" />
    </bean>
  </property>
</bean>

<bean id="userDao" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="proxyInterfaces" value="de.datenkollektiv.userservice.dao.UserDao" />
  <property name="target">
    <bean parent="abstractIbatisCrudDaoImpl">
      <constructor-arg>
        <value>de.datenkollektiv.userservice.model.impl.UserImpl</value>
      </constructor-arg>
    </bean>
  </property>
</bean>

Where is the SQL? iBatis queries are defined in a sql map.

<sqlMap namespace="USERS">
  ...
  <insert id="UserImpl.create" parameterClass="de.datenkollektiv.userservice.model.impl.UserImpl">
    insert into USERS (USERNAME, EMAIL, PASSWORD, REGISTRATIONKEY, ENABLED, ROLES)
    values (#username:VARCHAR#, #email:VARCHAR#, #password:VARCHAR#, #registrationKey:VARCHAR#, #enabled:BOOLEAN#, #roles:VARCHAR#)
  </insert>
  ...
  <select id="UserImpl.findByUsername" parameterClass="java.lang.String" resultMap="abatorgenerated_IbatisUserImplResult">
    select USERNAME, EMAIL, PASSWORD, REGISTRATIONKEY, ENABLED, ROLES
    from USERS
    where USERNAME = #username:VARCHAR#
  </select>
  ...
</sqlMap>

I will skip the boring details of the user service/security layer. Only showing the short Spring dynamic modules snippet creating the spring-security adapter simply consuming and exposing OSGi services.

<osgi:reference id="userService" interface="de.datenkollektiv.userservice.service.UserService" />
<osgi:service ref="userDetailsService" interface="org.springframework.security.userdetails.UserDetailsService" />

<bean id="userDetailsService" class="de.datenkollektiv.userservice.security.UserDetailsServiceAdapter"
  autowire="constructor" />