Don’t repeat the DAO!
譯者:Nicholas @ Nirvana Studio
原文地址:http://www-128.ibm.com/developerworks/java/library/j-genericdao.html
使用Hibernate和Spring AOP購建一個范型類型安全的DAO
2006年五月12日
在采用了Java 5的范型之后,要實(shí)現(xiàn)一個基于范型類型安全的數(shù)據(jù)訪問對象(DAO)就變得切實(shí)可行了。在這篇文章里,系統(tǒng)架構(gòu)師Per Mellqvist展示了一個基于Hibernate的范型DAO實(shí)現(xiàn)。然后將介紹如何使用Spring AOP的introduction為一個類增加一個類型安全的接口以便于執(zhí)行查詢。
對于大多數(shù)開發(fā)者來說,在系統(tǒng)中為每一個DAO編寫幾乎一樣的代碼已經(jīng)成為了一種習(xí)慣。同時大家也都認(rèn)可這種重復(fù)就是“代碼的味道”,我們中的大多數(shù)已經(jīng)習(xí)慣如此。當(dāng)然也有另外的辦法。你可以使用很多ORM工具來避免代碼的重復(fù)編寫。舉個例子,用Hibernate,你可以簡單的使用session操作直接控制你的持久化領(lǐng)域?qū)ο蟆_@種方式的負(fù)面影響就是丟失了類型安全。
為什么你的數(shù)據(jù)訪問代碼需要一個類型安全的接口?我認(rèn)為它減少了編程錯誤,提高了生產(chǎn)率,尤其是在使用現(xiàn)代高級IDE的時候。首先,一個類型安全的接口清晰的制定了哪些領(lǐng)域?qū)ο缶哂谐志没δ堋F浯危祟愋娃D(zhuǎn)換帶來的潛在問題。最后,它平衡了IDE的自動完成功能。使用自動完成功能是最快的方式來記住對于適當(dāng)?shù)念I(lǐng)域類哪些查詢是可用的。
在這篇文章里,我將展示給大家如何避免一次次地重復(fù)編寫DAO代碼,但同時還收益于類型安全的接口。事實(shí)上,所有內(nèi)需要編寫的是為新的DAO編寫一個Hibernate映射文件,一個POJO的Java接口,并且10行Spring配置文件。
DAO實(shí)現(xiàn)
DAO模式對于任何Java開發(fā)人員來說都是耳熟能詳?shù)摹_@個模式的實(shí)現(xiàn)相當(dāng)多,所以讓我們仔細(xì)推敲一下我這篇文章里面對于DAO實(shí)現(xiàn)的一些假設(shè):
- 所有系統(tǒng)中的數(shù)據(jù)庫訪問都是通過DAO來完成封裝
- 每一個DAO實(shí)例對一個主要的領(lǐng)域?qū)ο蠡蛘邔?shí)體負(fù)責(zé)。如果一個領(lǐng)域?qū)ο缶哂歇?dú)立的生命周期,那么它需要具有自己的DAO。
- DAO具有CRUD操作
- DAO可以允許基于criteria方式的查詢而不僅僅是通過主鍵查詢。我將這些成為finder方法或者finders。這個finder的返回值通常是DAO所負(fù)責(zé)的領(lǐng)域?qū)ο蟮募稀?
范型DAO接口
范型DAO的基礎(chǔ)就是CRUD操作。下面的接口定義了范型DAO的方法:
public interface GenericDao <T, PK extends Serializable> { ? /** Persist the newInstance object into database */ PK create(T newInstance); ? /** Retrieve an object that was previously persisted to the database using * the indicated id as primary key */ T read(PK id); ? /** Save changes made to a persistent object. */void update(T transientObject); ? /** Remove an object from persistent storage in the database */void delete(T persistentObject); }
實(shí)現(xiàn)這個接口
使用Hibernate實(shí)現(xiàn)上面的接口是非常簡單的。也就是調(diào)用一下Hibernate的方法和增加一些類型轉(zhuǎn)換。Spring負(fù)責(zé)session和transaction管理。
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); } ? publicvoid update(T o){ getSession().update(o); } ? publicvoid delete(T o){ getSession().delete(o); } ? // Not showing implementations of getSession() and setSessionFactory()}
Spring 配置
最后,Spring配置,我創(chuàng)建了一個GenericDaoHibernateImpl的實(shí)例。GenericDaoHibernateImpl的構(gòu)造器必須被告知領(lǐng)域?qū)ο蟮念愋停@樣DAO實(shí)例才能為之負(fù)責(zé)。這個同樣需要Hibernate運(yùn)行時知道這個對象的類型。下面的代碼中,我將領(lǐng)域類Person傳遞給構(gòu)造器并且將Hibernate的session工廠作為一個參數(shù)用來實(shí)例化DAO:
<bean id="personDao"class="genericdao.impl.GenericDaoHibernateImpl"> <constructor-arg> <value> genericdaotest.domain.Person</value></constructor-arg><propertyname="sessionFactory"><refbean="sessionFactory"/></property></bean>
可用的范型DAO
我還沒有全部完成,但我現(xiàn)在已經(jīng)有了一個可供作的代碼。下面的代碼展示了范型DAO如何使用:
public void someMethodCreatingAPerson(){ ... GenericDao dao = (GenericDao) beanFactory.getBean("personDao"); // This should normally be injected ? Person p = new Person("Per", 90); dao.create(p); }
這時候,我有一個范型DAO有能力進(jìn)行類型安全的CRUD操作。同時也有理由編寫GenericDaoHibernateImpl的子類來為每個領(lǐng)域?qū)ο笤黾硬樵児δ堋5沁@篇文章的主旨在于展示如何完成這項(xiàng)功能而不是為每個查詢編寫明確的代碼,然而,我將會使用多個工具來介紹DAO的查詢,這就是Spring AOP和Hibernate命名查詢。
Spring AOP介紹
你可以使用Spring AOP提供的introduction功能將一個現(xiàn)存的對象包裝到一個代理里面來增加新的功能,定義它需要實(shí)現(xiàn)的新接口,并且將之前所有不支持的方法委派到一個處理機(jī)。在我的DAO實(shí)現(xiàn)里面,我用introduction將一定數(shù)量的finder方法增加到現(xiàn)存的范型DAO類里面。因?yàn)閒inder方法針對特定的領(lǐng)域?qū)ο螅运鼈儽粦?yīng)用到表明接口的范型DAO中。
<bean id="finderIntroductionAdvisor"class="genericdao.impl.FinderIntroductionAdvisor"/> ? <beanid="abstractDaoTarget"class="genericdao.impl.GenericDaoHibernateImpl"abstract="true"><propertyname="sessionFactory"><refbean="sessionFactory"/></property></bean> ? <beanid="abstractDao"class="org.springframework.aop.framework.ProxyFactoryBean"abstract="true"><propertyname="interceptorNames"><list><value>finderIntroductionAdvisor</value></list></property></bean>
在上面的配置中,我定義了三個Spring bean,第一個bean,F(xiàn)inderIntroductionAdvisor,處理那些introduce到DAO中但是不屬于GenericDaoHibernateImpl類的方法。一會我再介紹Advisor bean的詳細(xì)情況。
第二個bean定義為“abstract”。在Spring中,這個bean可以被其他bean重用但是它自己不會被實(shí)例化。不同于抽象屬性,bean的定義簡單的指出了我需要一個GenericDaoHibernateImpl的實(shí)例同時需要一個SessionFactory的引用。注意GenericDaoHibernateImpl類只定義了一個構(gòu)造器接受領(lǐng)域類作為參數(shù)。因?yàn)檫@個bean是抽象的,我可以無限次的重用并且設(shè)定合適的領(lǐng)域類。
最后,第三個,也是最有意思的是bean將GenericDaoHibernateImpl的實(shí)例包裝進(jìn)了一個代理,給予了它執(zhí)行finder方法的能力。這個bean定義同樣是抽象的并且沒有指定任何接口。這個接口不同于任何具體的實(shí)例。
擴(kuò)展通用DAO
每個DAO的接口,都是基于GenericDAO接口的。我需要將為特定的領(lǐng)域類適配接口并且將其擴(kuò)展包含我的finder方法。
public interface PersonDao extends GenericDao<Person, Long> { List<Person> findByName(String name); }
上面的代碼清晰的展示了通過用戶名查找Person對象列表。所需的Java實(shí)現(xiàn)類不需要包含任何的更新操作,因?yàn)檫@些已經(jīng)包含在了通用DAO里。
配置PersonDao
因?yàn)镾pring配置依賴之前的那些抽象bean,所以它變得很緊湊。我需要指定DAO負(fù)責(zé)的領(lǐng)域類,并且我需要告訴Spring我這個DAO需要實(shí)現(xiàn)的接口。
<bean id="personDao"parent="abstractDao"> <property name="proxyInterfaces"> <value> genericdaotest.dao.PersonDao</value></property><propertyname="target"><beanparent="abstractDaoTarget"><constructor-arg><value>genericdaotest.domain.Person</value></constructor-arg></bean></property></bean>
你可以這樣使用:
public void someMethodCreatingAPerson(){ ... PersonDao dao = (PersonDao) beanFactory.getBean("personDao"); // This should normally be injected ? Person p = new Person("Per", 90); dao.create(p); ? List<Person> result = dao.findByName("Per"); // Runtime exception}
上面的代碼是使用類型安全接口PersonDao的一種正確途徑,但是DAO的實(shí)現(xiàn)并沒有完成。當(dāng)調(diào)用findByName()的時候?qū)е铝艘粋€運(yùn)行時異常。這個問題是我還沒有findByName()。剩下的工作就是指定查詢語句。要完成這個,我使用Hibernate命名查詢。
Hibernate命名查詢
使用Hibernate,你可以定義任何HQL查詢在映射文件里,并且給它一個名字。你可以在之后的代碼里面方便的通過名字引用這個查詢。這么做的一個優(yōu)點(diǎn)就是能夠在部署的時候調(diào)節(jié)查詢而不需要改變代碼。正如你一會將看到的,另一個好處就是實(shí)現(xiàn)一個“完整”的DAO而不需要編寫任何Java實(shí)現(xiàn)代碼。
<hibernate-mapping package="genericdaotest.domain"> <class name="Person"> <id name="id"> <generator class="native"/> </id> <property name="name"/> <property name="weight"/> </class> ? <queryname="Person.findByName"><![CDATA[select p from Person p where p.name = ? ]]></query></hibernate-mapping>
上面的代碼定義了領(lǐng)域類Person的Hibernate映射文件,有兩個屬性:name和weight。Person是一個具有上面屬性的簡單的POJO。這個文件同時包含了一個查詢,通過提供的name屬性從數(shù)據(jù)庫查找Person實(shí)例。Hibernate為命名查詢提供了不真實(shí)的命名空間功能。為了便于討論,我將所有的查詢名字的前綴變成領(lǐng)域類的的名稱。在現(xiàn)實(shí)場景中,使用完整的類名,包含包名,是一個更好的主意。
總覽
你已經(jīng)看到了為任何領(lǐng)域?qū)ο髣?chuàng)建并配置DAO的所需步驟了。這三個簡單的步驟就是:
- 定義一個接口繼承GenericDao并且包含任何所需的finder方法
- 在映射文件中為每個領(lǐng)域類的finder方法增加一個命名查詢。
- 為DAO增加10行Spring配置
可重用的DAO類
Spring advisor和interceptor的功能比較瑣碎,事實(shí)上他們的工作都引用回了GenericDaoHibernateImpl類。所有帶有“find”開頭的方法都被傳遞給DAO的單一方法executeFinder()。
public class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor {public FinderIntroductionAdvisor(){super(new FinderIntroductionInterceptor()); }} ? publicclass FinderIntroductionInterceptor implements IntroductionInterceptor { ? publicObject invoke(MethodInvocation methodInvocation)throwsThrowable{ ? 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(); }} ? publicboolean implementsInterface(Class intf){return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf); }}
executeFinder() 方法
上面的代碼唯一缺的就是executeFinder的實(shí)現(xiàn)。這個代碼觀察被調(diào)用的類的名字和方法,并且將他們與Hibernate的查詢名相匹配。你可以使用一個FinderNamingStrategy來激活其他方式的命名查詢。默認(rèn)的實(shí)現(xiàn)查找一個名為“ClassName.methodName”的查詢,ClassName是除包名之外的類名。
public List<T> executeFinder(Method method, finalObject[] queryArgs){finalString queryName = queryNameFromMethod(method); finalQuery 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(); } ? publicString queryNameFromMethod(Method finderMethod){return type.getSimpleName() + "." + finderMethod.getName(); }
總結(jié)
在Java 5之前,Java語言并不支持代碼同時具有類型安全和范性的特性;你不得不二者選一。在這篇文章里,你可以看到使用Java 5范型支持并且結(jié)合Spring和Hibernate(和AOP)一起來提高生產(chǎn)力。一個范型類型安全的DAO類非常容易編寫,所有你需要做的就是一個接口,一些命名查詢,并且10行Spring配置,并且可以極大的減少錯誤,同時節(jié)省時間。
posted on 2007-03-05 11:51 風(fēng)人園 閱讀(197) 評論(0) 編輯 收藏 所屬分類: DAO