Thursday, November 15, 2007

Generic DAO (2) 基本的 Generic DAO Implementation

上一篇提到的 Generic DAO 的效果, 旨在讓使用者不需要再重新 implement 每一個 DAO 大同小異的部份 : create, update, delete etc. 就連 finder methods, 也只要寫好 HQL 就好了, 不必再重覆每個finder 去 getSession(), createQuery() 等等.

這個 framework 的第一步, 就是 GenericDao(從原文抄來借用一下 :P ).

Generic DAO 大概的樣子就是
public interface GenericDao<T, PK extends Serializable> {
_ PK create(T newInstance);
_ T read(PK id);
_ void update(T transientObject);
_ void delete(T persistentObject);

在 Generic DAO 本身就有提供最基本的CRUD operation

public class GenericDaoHibernateImpl <T, PK extends Serializable>
___ implements GenericDao<T, PK>, FinderExecutor {
___ private Class<T> type;

___ public GenericDaoHibernateImpl(Class<T> type) {
_______ this.type = type;
___ }

___ public PK create(T o) {
_______ return (PK) getSession().save(o);
___ }

___ public T read(PK id) {
_______ return (T) getSession().get(type, id);
___ }

___ public void update(T o) {
_______ getSession().update(o);
___ }

___ public void delete(T o) {
_______ getSession().delete(o);
___ }

___ // Not showing implementations of getSession() and setSessionFactory()

(原本的 implementation 是在 Spring Config 中是在Spring config inject Hibernate 的 SessionFactory. 但我則建議用 JPA 的 EntityManager, 並利用 @PersistenceContext 讓 Spring 自動 inject EntityManager. 詳細做法之後會解釋.)

好了, 有了這個 Generic DAO implementation, 但怎樣才能沒有 implementation 也能跑? 這就要借助 Spring 的 ProxyFactoryBean. 利用 ProxyFactoryBean, Spring 會為我提供的 interfaces 自動 Generate 一個 implementation class. 比對前一篇的 Spring

<bean id="abstractDaoTarget"
_______ class="foo.GenericDaoHibernateImpl"
_______ abstract="true" />

<bean id="staffDao" class="org.springframework.aop.framework.ProxyFactoryBean">
_ <property name="proxyInterfaces" value="foo.StaffDao,foo.FinderExecutor">
_ <property name="target">
___ <bean parent="abstractDaoTarget">
_____ <constructor-arg>
_______ <value>foo.Staff</value>
_____ </constructor-arg>
___ </bean>
_ </property>

簡單的解釋, 上面的 config 會叫 Spring 生成一個 GenericDaoHibernateImpl, 然後再生一個 Proxy Bean 來包著之前的Generic DAO Impl, 此 Proxy 亦會額外 implement StaffDao 和 FinderExecutor 的 interface. 但問題是, StaffDao 和 FinderExecutor 的 method, 這個 Proxy Bean 怎樣 implement? 其實 Proxy Factory Bean 不會做任何有意義的 implementation. 這個 Dummy 的 object instance 旨在讓 Spring AOP 的 Interceptor 攔截再跑自己想要的 logic. 那些自動 generate 出來的沒用 method implementation 是不會用到的.

原本的 JGenericDao 做法順便把 Interceptor 塞給ProxyFactoryBean. 但這做法在 Spring 2.0.x, 如果同時有用到新的 AOP config 就會不正常運作, 所以我利用了 AOP 的 xml config 來提供 interceptor

<bean id="queryInterceptor"
___ class="foo.FinderIntroductionInterceptor" />

___ <aop:aspect ref="queryInterceptor">
_______ <aop:pointcut id="findQuery"
___________ expression="execution(* foo..*Dao.find*(..)) and this(foo.dao.finder.FinderExecutor)" />
_______ <aop:around pointcut-ref="findQuery" method="invokeFind" />
___ </aop:aspect>


這樣每逢跑 DAO 中以 find 開首的 methods, 我們的 queryInterceptor 就會攔截.

攔截了要做什麼? 借用 "Don't repeat the DAO!" 的 code, 就是攔截後, 不直接 invoke 原本的 method, 轉而 invoke target class 的 "executeFinder" method.

public class FinderIntroductionInterceptor implements IntroductionInterceptor {

___ public Object invokeFind(MethodInvocation methodInvocation) throws Throwable {

_______ FinderExecutor genericDao = (FinderExecutor) methodInvocation.getThis();

_______ String methodName = methodInvocation.getMethod().getName();
_______ if (methodName.startsWith("find")) {
___________ Object[] arguments = methodInvocation.getArguments();
___________ return genericDao.executeFinder(methodInvocation.getMethod(), arguments);
_______ } else {
___________ return methodInvocation.proceed();
_______ }
___ }

___ public boolean implementsInterface(Class intf) {
_______ return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf);
___ }

Generic DAO 的 executeFinder 做的, 則是基於 method name, 去找 named query 來 invoke. 如果 method 有 parameter pass in 的話, 就順便 bind 到 query.
public List<T> executeFinder(Method method, final Object[] queryArgs) {
____ final String queryName = queryNameFromMethod(method);
____ final Query namedQuery = getSession().getNamedQuery(queryName);
____ String[] namedParameters = namedQuery.getNamedParameters();
____ for(int i = 0; i < queryArgs.length; i++) {
____________ Object arg = queryArgs[i];
____________ Type argType = namedQuery.setParameter(i, arg);
_____ }
_____ return (List<T>) namedQuery.list();

public String queryNameFromMethod(Method finderMethod) {
____ return type.getSimpleName() + "." + finderMethod.getName();

這個便是 Generic DAO 最基本的功能.

但這個 Generic DAO 有很多欠缺的功能, 下回就是我自己基於這個 framework 作的新功能. 當中包括 annotated parameters, provided Query, annonatation-based QBC and QBE, 和 self-implemented finders.


Li said...

I tried the <aop:config> but it didn't work. I couldn't find the invokeFind method in your FinderIntroductionInterceptor. Can you post your solution in detail? Thanks.

Alien 亞里 炎 said...

Oops, I just found that I have a typo there... :P as I just type it for reference, haven't checked for correctness in some places :P

Will fix the example now :)