對於大多數開發人員,爲系統中的每個 DAO 編寫幾乎相同的代碼到目前爲止已經成爲一種習慣。雖然所有人都將這種重複標識爲 “代碼味道”,但我們大多數都已經學會忍受它。其實有解決方案。可以使用許多 ORM 工具來避免代碼重複。例如,使用 Hibernate,您可以簡單地爲所有的持久域對象直接使用會話操作。這種方法的缺點是損失了類型安全。
爲什麼您要爲數據訪問代碼提供類型安全接口?我會爭辯說,當它與現代 IDE 工具一起使用時,會減少編程錯誤並提高生產率。首先,類型安全接口清楚地指明哪些域對象具有可用的持久存儲。其次,它消除了易出錯的類型強制轉換的需要(這是一個在查詢操作中比在 CRUD 中更常見的問題)。最後,它有效利用了今天大多數 IDE 具備的自動完成特性。使用自動完成是記住什麼查詢可用於特定域類的快捷方法。
在本文中,我將爲您展示如何避免再三地重複 DAO 代碼,而仍保留類型安全接口的優點。事實上,您需要爲每個新 DAO 編寫的只是 Hibernate 映射文件、無格式舊 Java 接口以及 Spring 配置文件中的 10 行。
DAO 實現
DAO 模式對任何企業 Java 開發人員來說都應該很熟悉。但是模式的實現各不相同,所以我們來澄清一下本文提供的 DAO 實現背後的假設:
- 系統中的所有數據庫訪問都通過 DAO 進行以實現封裝。
- 每個 DAO 實例負責一個主要域對象或實體。如果域對象具有獨立生命週期,它應具有自己的 DAO。
- DAO 負責域對象的創建、讀取(按主鍵)、更新和刪除(creations, reads, updates, and deletions,CRUD)。
- DAO 可允許基於除主鍵之外的標準進行查詢。我將之稱爲查找器方法 或查找器。查找器的返回值通常是 DAO 負責的域對象集合。
- DAO 不負責處理事務、會話或連接。這些不由 DAO 處理是爲了實現靈活性。
泛型 DAO 接口
泛型 DAO 的基礎是其 CRUD 操作。下面的接口定義泛型 DAO 的方法:
清單 1. 泛型 DAO 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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); } |
實現接口
用 Hibernate 實現清單 1 中的接口十分簡單,如清單 2 所示。它只需調用底層 Hibernate 方法和添加強制類型轉換。Spring 負責會話和事務管理。(當然,我假設這些函數已做了適當的設置,但該主題在 Hibernate 和 Springt 手冊中有詳細介紹。)
清單 2. 第一個泛型 DAO 實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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() } |
Spring 配置
最後,在 Spring 配置中,我創建了 GenericDaoHibernateImpl
的一個實例。必須告訴GenericDaoHibernateImpl
的構造函數
DAO 實例將負責哪個域類。只有這樣,Hibernate 才能在運行時知道由 DAO 管理的對象類型。在清單 3 中,我將域類 Person
從示例應用程序傳遞給構造函數,並將先前配置的 Hibernate 會話工廠設置爲已實例化的 DAO 的參數:
清單 3. 配置 DAO
1
2
3
4
5
6
7
8
|
< bean id = "personDao" class = "genericdao.impl.GenericDaoHibernateImpl" > < constructor-arg > < value >genericdaotest.domain.Person</ value > </ constructor-arg > < property name = "sessionFactory" > < ref bean = "sessionFactory" /> </ property > </ bean > |
可用的泛型 DAO
我還沒有完成,但我所完成的確實已經可以使用了。在清單 4 中,可以看到原封不動使用該泛型 DAO 的示例:
清單 4. 使用 DAO
1
2
3
4
5
6
7
8
|
public
void someMethodCreatingAPerson() { ... GenericDao
dao = (GenericDao) beanFactory.getBean("personDao");
// This should normally be injected Person
p = new Person("Per", 90); dao.create(p); } |
現在,我有一個能夠進行類型安全 CRUD 操作的泛型 DAO。讓子類 GenericDaoHibernateImpl
爲每個域對象添加查詢能力將非常合理。因爲本文的目的在於展示如何不爲每個查詢編寫顯式的 Java 代碼來實現查詢,但是,我將使用其他兩個工具將查詢引入 DAO,也就是 Spring
AOP 和 Hibernate 命名的查詢。
Spring AOP introductions
可以使用 Spring AOP 中的 introductions 將功能添加到現有對象,方法是將功能包裝在代理中,定義應實現的接口,並將所有先前未支持的方法指派到單個處理程序。在我的 DAO 實現中,我使用 introductions 將許多查找器方法添加到現有泛型 DAO 類中。因爲查找器方法是特定於每個域對象的,因此將其應用於泛型 DAO 的類型化接口。
Spring 配置如清單 5 所示:
清單 5. FinderIntroductionAdvisor 的 Spring 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
< bean id = "finderIntroductionAdvisor" class = "genericdao.impl.FinderIntroductionAdvisor" /> < bean id = "abstractDaoTarget" class = "genericdao.impl.GenericDaoHibernateImpl" abstract = "true" > < property name = "sessionFactory" > < ref bean = "sessionFactory" /> </ property > </ bean > < bean id = "abstractDao" class = "org.springframework.aop.framework.ProxyFactoryBean" abstract = "true" > < property name = "interceptorNames" > < list > < value >finderIntroductionAdvisor</ value > </ list > </ property > </ bean > |
在清單 5 的配置文件中,我定義了三個 Spring bean。第一個 bean 是 FinderIntroductionAdvisor,它處理引入到 DAO 的所有方法,這些方法在 GenericDaoHibernateImpl
類中不可用。我稍後將詳細介紹 Advisor bean。
第二個 bean 是 “抽象的”。在 Spring 中,這意味着該 bean 可在其他 bean 定義中被重用,但不被實例化。除了抽象特性之外,該 bean 定義只指出我想要 GenericDaoHibernateImpl
的實例以及該實例需要對SessionFactory
的引用。注意,GenericDaoHibernateImpl
類僅定義一個構造函數,該構造函數接受域類作爲其參數。因爲該
bean 定義是抽象的,所以我將來可以無數次地重用該定義,並將構造函數參數設置爲合適的域類。
最後,第三個也是最有趣的 bean 將 GenericDaoHibernateImpl
的 vanilla 實例包裝在代理中,賦予其執行查找器方法的能力。該 bean 定義也是抽象的,不指定希望引入到 vanilla DAO 的接口。該接口對於每個具體的實例是不同的。注意,清單 5 顯示的整個配置僅定義一次。
擴展 GenericDAO
當然,每個 DAO 的接口都基於 GenericDao
接口。我只需使該接口適應特定的域類並擴展該接口以包括查找器方法。在清單 6 中,可以看到爲特定目的擴展的 GenericDao
接口示例:
清單 6. PersonDao 接口
1
2
3
|
public
interface PersonDao extends GenericDao< Person ,
Long> { List< Person >
findByName(String name); } |
很明顯,清單 6 中定義的方法旨在按名稱查找 Person
。必需的 Java 實現代碼全部是泛型代碼,在添加更多 DAO 時不需要任何更新。
配置 PersonDao
因爲 Spring 配置依賴於先前定義的 “抽象” bean,因此它變得相當簡潔。我需要指出 DAO 負責哪個域類,並且需要告訴 Springs 該 DAO 應實現哪個接口(一些方法是直接使用,一些方法則是通過使用 introductions 來使用)。清單 7 展示了 PersonDAO
的
Spring 配置文件:
清單 7. PersonDao 的 Spring 配置
1
2
3
4
5
6
7
8
9
10
11
12
|
< bean id = "personDao" parent = "abstractDao" > < property name = "proxyInterfaces" > < value >genericdaotest.dao.PersonDao</ value > </ property > < property name = "target" > < bean parent = "abstractDaoTarget" > < constructor-arg > < value >genericdaotest.domain.Person</ value > </ constructor-arg > </ bean > </ property > </ bean > |
在清單 8 中,可以看到使用了這個更新後的 DAO 版本:
清單 8. 使用類型安全接口
1
2
3
4
5
6
7
8
9
10
|
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 } |
雖然清單 8 中的代碼是使用類型安全 PersonDao
接口的正確方法,但 DAO 的實現並不完整。調用findByName()
會導致運行時異常。問題在於我還沒有實現調用 findByName()
所必需的查詢。剩下要做的就是指定查詢。爲更正該問題,我使用了
Hibernate 命名查詢。
Hibernate 命名查詢
使用 Hibernate,可以在 Hibernate 映射文件 (hbm.xml) 中定義 HQL 查詢併爲其命名。稍後可以通過簡單地引用給定名稱來在 Java 代碼中使用該查詢。該方法的優點之一是能夠在部署時優化查詢,而無需更改代碼。您一會將會看到,另一個優點是無需編寫任何新 Java 實現代碼,就可以實現 “完整的” DAO。清單 9 是帶有命名查詢的映射文件的示例:
清單 9. 帶有命名查詢的映射文件
1
2
3
4
5
6
7
8
9
10
11
12
13
|
< hibernate-mapping package = "genericdaotest.domain" > < class name = "Person" > < id name = "id" > < generator class = "native" /> </ id > < property name = "name" /> < property name = "weight" /> </ class > < query name = "Person.findByName" > <![CDATA[select
p from Person p where p.name = ? ]]> </ query > </ hibernate-mapping > |
清單 9 定義了域類 Person
的 Hibernate 映射,該域類具有兩個屬性:name
和 weight
。Person
是具有上述屬性的簡單
POJO。該文件還包含一個在數據庫中查找 Person
所有實例的查詢,其中 “name” 等於提供的參數。Hibernate 不爲命名查詢提供任何真正的名稱空間功能。出於討論目的,我爲所有查詢名稱都加了域類的短(非限定)名稱作爲前綴。在現實世界中,使用包括包名稱的完全類名可能是更好的主意。
逐步概述
您已經看到了爲任何域對象創建和配置新 DAO 所必需的全部步驟。三個簡單的步驟是:
-
定義一個接口,它擴展
GenericDao
幷包含所需的任何查找器方法。 - 將每個查找器的命名查詢添加到域對象的 hbm.xml 映射文件。
- 爲 DAO 添加 10 行 Spring 配置文件。
查看執行查找器方法的代碼(只編寫了一次!)來結束我的討論。
可重用的 DAO 類
使用的 Spring advisor 和 interceptor 很簡單,事實上它們的工作是向後引用GenericDaoHibernateImplClass
。方法名以 “find” 打頭的所有調用都傳遞給 DAO 和單個方法executeFinder()
。
清單 10. FinderIntroductionAdvisor 的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor { public
FinderIntroductionAdvisor() { super(new
FinderIntroductionInterceptor()); } } public
class FinderIntroductionInterceptor implements IntroductionInterceptor { public
Object invoke(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); } } |
executeFinder() 方法
清單 10 的實現中惟一缺少的是 executeFinder()
實現。該代碼查看調用的類和方法的名稱,並使用配置上的約定將其與 Hibernate 查詢的名稱相匹配。還可以使用 FinderNamingStrategy
來支持其他命名查詢的方法。默認實現查找叫做
“ClassName.methodName
” 的查詢,其中 ClassName
是不帶包的短名稱。清單 11 完成了泛型類型安全 DAO 實現:
清單 11. executeFinder() 的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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(); } |
結束語
在 Java 5 之前,該語言不支持編寫既類型安全又 泛型的代碼,您必須只能選擇其中之一。在本文中,您已經看到一個結合使用 Java 5 泛型與 Spring 和 Hibernate(以及 AOP)等工具來提高生產率的示例。泛型類型安全 DAO 類相當容易編寫 —— 您只需要單個接口、一些命名查詢和爲 Spring 配置添加的 10 行代碼 —— 而且可以極大地減少錯誤並節省時間。
幾乎本文的所有代碼都是可重用的。儘管您的 DAO 類可能包含此處沒有實現的查詢和操作類型(比如,批操作),但使用我所展示的技術,您至少應該能夠實現其中的一部分。參閱 參考資料 瞭解其他泛型類型安全 DAO 類實現。
致謝
自 Java 語言中出現泛型以來,單個泛型類型安全 DAO 的概念已經成爲主題。我曾在 JavaOne 2004 中與 Don Smith 簡要討論了泛型 DAO 的靈活性。本文使用的 DAO 實現類旨在作爲示例實現,實際上還存在其他實現。
鏈接:https://www.ibm.com/developerworks/cn/java/j-genericdao.html