11.4 持久層設計

章節試讀·支持作者從購買正版開始 『 閱讀首頁 』

作者、書名、出版社 關鍵字:


第11章 Spring 2.0實戰:Live在線書店(節選) 11.4 持久層設計

上一頁 返回書目 比價購買 下一頁
  
11.4 持久層設計
在第5章中,我們已經初步介紹了Hibernate這個功能強大的ORM框架,Live在線書店仍然採用Hibernate作爲持久化解決方案。DAO模式仍是持久層的標準模式,不同的是,我們不採用Spring提供的現成的DAO體系,而是設計一個類型安全的泛型DAO,通過泛型DAO,能夠將公共代碼以範型方式放入範型DAO超類中,從而進一步減少代碼量。

對於每一個Domain Object來說,至少要實現基本的CRUD操作,即Create(創建)、Retrieve(獲取)、Update(更新)和Delete(刪除)4種操作。我們將這4種操作全部以泛型方式實現,大大簡化了各個子類的編碼,同時還獲得了類型安全的特性。

泛型DAO的核心是定義一個GenericDao接口,申明CRUD操作。

public interface GenericDao<T> {

// 通過主鍵查詢T:

T query(Serializable id);

// 創建新的T:

void create(T t);

// 刪除T:

void delete(T t);

// 更新T:

void update(T t);

}

然後,提供一個默認的GenericHibernateDao實現類,實現所有的CRUD操作,但不必實現GenericDao接口。

public abstract class GenericHibernateDao<T> {

private final Class<T> clazz;

protected HibernateTemplate hibernateTemplate;

public GenericHibernateDao(Class<T> clazz) {

this.clazz = clazz;

}

public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {

this.hibernateTemplate = hibernateTemplate;

}

// 根據主鍵查詢T:

public T query(Serializable id) {

// 用get()而不用load()是因爲load()方法返回的是延遲加載的對象,

// 可能造成LazyInitializationException:

T t = (T)hibernateTemplate.get(clazz, id);

if(t==null)

throw new DataRetrievalFailureException("Object not found.");

return t;

}

// 創建T:

public void create(T t) {

hibernateTemplate.save(t);

}

// 刪除T:

public void delete(T t) {

hibernateTemplate.delete(t);

}

// 更新T:

public void update(T t) {

hibernateTemplate.update(t);

}

}

GenericHibernateDao受到Hibernate的唯一限制是必須獲得域對象的Class實例,由於無法直接調用T.class,因此,一個變通的方法是從構造方法的參數中傳入Class實例,對於子類的繼承會稍微麻煩一點。

對於真正的DAO接口,由GenericDao接口擴展,保證了類型安全。例如,對於BookDao,由於擴展自GenericDao<Book>,因此,定義的CRUD操作即爲類型安全的,此外,還可以定義其他查詢操作。

public class BookDaoImpl extends GenericHibernateDao<Book>

implements BookDao {

public BookDaoImpl() {

super(Book.class);

}

// 基本的CRUD操作已經實現了!

// 定義額外的query操作:

public List<Book> query(final Category c, final Page page) {

// TODO:

}

}

BookDao的實現類BookDaoImpl實現了BookDao接口,這是類型安全的。此外,BookDaoImpl還從GenericHibernateDao擴展而來,因此,基本的CRUD操作已經全部實現了,BookDaoImpl只需實現BookDao額外定義的查詢操作。由於Spring提供的HibernateTemplate已被注入到GenericHibernateDao中,因此,BookDaoImpl可以直接使用HibernateTemplate來實現額外定義的查詢操作。

這個泛型DAO的詳細模式如圖11-11所示。


泛型DAO模式的最大的好處是消除了每個DAO對象中重複的CRUD操作,這些重複的CRUD操作被統一放入GenericHibernateDao,以泛型方式實現了。

子類如果不希望繼承超類的某個方法,例如,CommentDaoImpl不希望客戶端去調用update()方法,就可以簡單地覆蓋它,直接拋出UnsupportedOperationException異常。

@Override

public void update(Comment t) {

throw new UnsupportedOperationException();

}

將覆寫的方法加上註解@Override,維護源代碼時,很容易知道該方法覆蓋了超類的相同簽名的方法。使用Eclipse的菜單命令“Source”→“Override/Implements Methods...”生成的方法簽名就會被自動標記爲@Override。這是在Java 5中的一種良好的編程習慣。

在持久層中,我們一共定義了5個DAO對象,其層次關係如圖11-12所示。


圖11-12

HibernateTemplate對象是注入到GenericHibernateDao<T>中的,因此,所有的實現類都可以直接引用。注意到我們沒有對FavoriteBook和OrderItem對象定義DAO操作,這兩個對象的相關操作分別被定義在BookDao和OrderDao中。

現在我們設計好了各個DAO組件,下一步就需要在Spring的XML配置文件中裝配起來。對於持久層來說,需要裝配的一共有以下組件。

(1)DataSource:通過Spring提供的DriverManagerDataSource,我們可以很容易地配置一個DataSource供開發和測試使用。在實際部署時,在服務器上配置好DataSource,然後應用JndiObjectFactoryBean獲得DataSource即可。

<bean id="dataSource"

class="org.springframework.jdbc.datasource.DriverManagerDataSource">

<property name="driverClassName" value="${jdbc.driver}" />

<property name="url" value="${jdbc.url}" />

<property name="username" value="${jdbc.username}" />

<property name="password" value="${jdbc.password}" />

</bean>

JDBC連接的配置信息放在外部jdbc.properties文件中,應用第3章介紹的PropertyPlaceholderConfigurer可以很容易地引入到Spring的配置文件中。

(2)SessionFactory:使用AnnotationSessionFactoryBean可以直接在Spring中配置一個SessionFactory,而不必使用Hibernate特有的hibernate.cfg.xml配置文件。

<bean id="sessionFactory" class="org.springframework.orm.hibernate3. annotation.AnnotationSessionFactoryBean">

<property name="dataSource" ref="dataSource" />

<property name="annotatedClasses">

<list>

<value>net.livebookstore.domain.Account</value>

<value>net.livebookstore.domain.Book</value>

<value>net.livebookstore.domain.Category</value>

<value>net.livebookstore.domain.Comment</value>

<value>net.livebookstore.domain.FavoriteBook</value>

<value>net.livebookstore.domain.Order</value>

<value>net.livebookstore.domain.OrderItem</value>

</list>

</property>

<property name="annotatedPackages">

<list>

<value>net.livebookstore.domain</value>

</list>

</property>

<property name="hibernateProperties">

<props>

<prop key="hibernate.dialect">

net.livebookstore.hibernate.CustomSQLDialect

</prop>

<prop key="hibernate.show_sql">true</prop>

<prop key="hibernate.jdbc.fetch_size">10</prop>

<prop key="hibernate.cache.provider_class">

org.hibernate.cache.HashtableCacheProvider

</prop>

</props>

</property>

<property name="eventListeners">

<map>

<entry key="pre-update">

<bean class="org.hibernate.validator.event.ValidatePreUpdate EventListener" />

</entry>

<entry key="pre-insert">

<bean class="org.hibernate.validator.event.ValidatePreInsert EventListener" />

</entry>

</map>

</property>

</bean>

(3)HibernateTemplate:由於我們的每個DAO組件並沒有從Spring的Hibernate DaoSupport中派生,因此,需要定義一個HibernateTemplate實例,然後注入到每個DAO組件中。

<bean id="hibernateTemplate"

class="org.springframework.orm.hibernate3.HibernateTemplate">

<property name="sessionFactory" ref="sessionFactory" />

<property name="fetchSize" value="10" />

</bean>

(4)HibernateTransactionManager:用於管理Hibernate事務,在這裏我們只需配置這個Bean,就可以直接使用聲明式事務管理。

<bean id="transactionManager"

class="org.springframework.orm.hibernate3.HibernateTransactionManager">

<property name="sessionFactory" ref="sessionFactory"/>

</bean>

(5)各DAO組件:由於我們直接在GenericHibernateDao中通過XDoclet註釋注入了HibernateTemplate:

public abstract class GenericHibernateDao<T> {

protected HibernateTemplate hibernateTemplate;

/**

* @spring.property name="hibernateTemplate" ref="hibernateTemplate"

*/

public void setHibernateTemplate(HibernateTemplate hibernateTemplate) {

this.hibernateTemplate = hibernateTemplate;

}

...

}

因此,在各個DAO的定義處加上@spring.bean的註釋,再運行Ant,即可自動生成DAO組件的配置信息並自動注入HibernateTemplate。

11.4.1 與運算(&)的實現
由於Hibernate 3.2不支持“&”運算,但實際上大部分數據庫都支持“&”運算,例如,MySQL支持“a & b”,而HSQLDB是通過“BITAND(a, b)”函數提供的“&”運算。因此,我們需要擴展Hibernate 3.2,使其支持“&”運算,這樣才能根據Category對象獲得當前分類及其子類的所有書籍。

雖然Hibernate也支持直接執行原始的SQL語句,但是這樣就喪失了O/R Mapping的能力,並且需要更多的轉化工作。我們希望能直接在HQL語句中支持“&”運算。幸運的是,Hibernate框架的設計非常具有擴展性。Hibernate對不同數據庫的“方言”支持就可以解析某種數據庫的特定SQL函數,我們只需要利用Hibernate的自定義函數機制,自行編寫一個bitand()函數,將其解析爲數據庫對應的SQL語句,即可實現“&”運算。

Hibernate通過SQLFunction接口實現自定義SQL函數,我們定義一個BitAndFunction如下。

public class BitAndFunction implements SQLFunction {

// 根據需要返回SQL數據類型:

public Type getReturnType(Type type, Mapping mapping) {

return Hibernate.INTEGER;

}

public boolean hasArguments() {

return true;

}

public boolean hasParenthesesIfNoArguments() {

return true;

}

public String render(List args, SessionFactoryImplementor factory) throws QueryException {

if (args.size() != 2) {

throw new IllegalArgumentException("BitAndFunction requires 2 arguments!");

}

return args.get(0).toString() + " & " + args.get(1).toString();

}

}

對於HQL語句,上述自定義SQL函數會將“bitand(a,b)”翻譯成“a & b”,這樣,大多數支持“&”運算的數據庫就可以正確執行。

由於HSQLDB比較特殊,它不是通過“&”實現的與運算,而是提供了一個BITAND()函數,因此,再定義一個HsqlBitAndFunction。

public class HsqlBitAndFunction extends BitAndFunction {

public String render(List args, SessionFactoryImplementor factory) throws QueryException {

return "BITAND(" + args.get(0).toString() + ", " + args.get(1).toString() + ")";

}

}

現在,我們需要將自定義函數註冊到Hibernate中,最簡單的方法是從相應的方言派生一個自定義的CustomSQLDialect,然後在構造方法中註冊該BitAndFunction。

public class CustomSQLDialect extends HSQLDialect {

public CustomSQLDialect() {

super();

LogFactory.getLog(CustomSQLDialect.class).info("Register bitand function for bit-and operation. (e.g.: where a & b = :c)");

if(HSQLDialect.class.isAssignableFrom(getClass()))

registerFunction("bitand", new HsqlBitAndFunction());

else

registerFunction("bitand", new BitAndFunction());

}

}

在Spring的Hibernate相關配置中,將dialect指定爲CustomSQLDialect就可以實現“&”運算。

11.4.2 分頁的實現
分頁是查詢時最常見的操作。如果一次查詢的數據過多,就很有必要分頁顯示給用戶,一是因爲一次查詢數據量如果太大,例如,上萬條記錄,會對服務器造成很大的負擔;二是用戶希望看到的往往是最關心的少量數據,因此,應當儘量在前幾頁讓用戶看到他們最關心的數據。

實現分頁查詢的關鍵是獲得所有符合條件的記錄總數,這樣就能根據每頁的數量計算出頁數。爲此,我們設計一個Page對象,初始化每頁需要顯示的記錄數和要顯示的頁號。

public class Page {

public static final int DEFAULT_PAGE_SIZE = 10;

private int pageIndex;

private int pageSize;

private int totalCount;

private int pageCount;

public Page(int pageIndex, int pageSize) {

if(pageIndex<1)

pageIndex = 1;

if(pageSize<1)

pageSize = 1;

this.pageIndex = pageIndex;

this.pageSize = pageSize;

}

public Page(int pageIndex) {

this(pageIndex, DEFAULT_PAGE_SIZE);

}

public int getPageIndex() { return pageIndex; }

public int getPageSize() { return pageSize; }

public int getPageCount() { return pageCount; }

public int getTotalCount() { return totalCount; }

public int getFirstResult() { return (pageIndex-1)*pageSize; }

public boolean getHasPrevious() { return pageIndex>1; }

public boolean getHasNext() { return pageIndex<pageCount; }

public void setTotalCount(int totalCount) {

this.totalCount = totalCount;

pageCount = totalCount / pageSize + (totalCount%pageSize==0 ? 0 : 1);

if(totalCount==0) {

if(pageIndex!=1)

throw new IndexOutOfBoundsException("Page index out of range.");

}

else {

if(pageIndex>pageCount)

throw new IndexOutOfBoundsException("Page index out of range.");

}

}

public boolean isEmpty() {

return totalCount==0;

}

}

Page在初始化時就確定了pageSize和pageIndex屬性,待查詢到記錄總數後,通過setTotalCount()方法就可以確定頁數,從而通過getFirstResult()返回第一行記錄的起始位置。

爲了在Hibernate中實現分頁查詢,還需要幾個輔助方法。我們將這幾個輔助方法放到GenericHibernateDao中,以方便子類調用。

queryForObject()方法用於執行一次查詢,並返回一個唯一結果。

protected Object queryForObject(final String select, final Object[] values) {

HibernateCallback selectCallback = new HibernateCallback() {

public Object doInHibernate(Session session) {

Query query = session.createQuery(select);

if(values!=null) {

for(int i=0; i<values.length; i++)

query.setParameter(i, values[i]);

return query.uniqueResult();

}

};

return hibernateTemplate.execute(selectCallback);

}

queryForList(String, Object[], Page)方法實現一個分頁查詢。

protected List queryForList(final String select, final Object[] values, final Page page) {

HibernateCallback selectCallback = new HibernateCallback() {

public Object doInHibernate(Session session) {

Query query = session.createQuery(select);

if(values!=null) {

for(int i=0; i<values.length; i++)

query.setParameter(i, values[i]);

}

return query.setFirstResult(page.getFirstResult())

.setMaxResults(page.getPageSize())

.list();

}

};

return (List) hibernateTemplate.executeFind(selectCallback);

}

另一個queryForList(String, String, Object[], Page)重載方法實現了一個完整的分頁查詢,第一個查詢定義瞭如何獲得記錄總數,然後填充到Page對象,再根據第二個查詢獲得指定頁的記錄。

protected List queryForList(final String selectCount, final String select, final Object[] values, final Page page) {

Long count = (Long)queryForObject(selectCount, values);

page.setTotalCount(count.intValue());

if(page.isEmpty())

return Collections.EMPTY_LIST;

return queryForList(select, values, page);

}

例如,要查詢一本書的評論,由於評論可能很多,因此需要進行分頁顯示。queryComments()方法實現了這個功能。

public List<Comment> queryComments(Book book, Page page) {

return queryForList(

"select count(*) from Comment as c where c.book=?",

"select c from Comment as c where c.book=? order by c.createdDate desc",

new Object[] {book},

page

);

}

第一個“select count(*) from ...”查詢定義瞭如何獲得評論的總數,第二個“select from ...”查詢根據Page對象的pageIndex和pageSize取出相應頁面的評論。爲了便於使用,這些輔助方法均定義在GenericHibernateDao中,子類可以方便地調用它們。

需要注意的是,與Hibernate 3.1及其以前的版本不同,從Hibernate 3.2開始,使用count()等SQL函數返回的數據類型從Integer改爲Long,這是爲了兼容JPA標準。

對於Hibernate來說,還提供了Criteria查詢,通過DetachedCriteria,可以先定義查詢,然後關聯Session執行查詢。Criteria可以通過投影操作方便地獲得記錄的總數,但是,投影操作和查詢的Order條件是衝突的。爲了實現通過一條DetachedCriteria同時得到記錄總數和對應頁數的記錄,可以通過反射實現。爲此,封裝一個PaginationCriteria。

class PaginationCriteria {

private static Field orderEntriesField = getField(Criteria.class, "orderEntries");

public static List query(Criteria c, Page page) {

// first we must detect if any Order defined:

// Hibernate code: private List orderEntries = new ArrayList();

List _old_orderEntries = (List)getFieldValue(orderEntriesField, c);

boolean restore = false;

if(_old_orderEntries.size()>0) {

restore = true;

setFieldValue(orderEntriesField, c, new ArrayList());

}

c.setProjection(Projections.rowCount());

int rowCount = ((Long)c.uniqueResult()).intValue();

page.setTotalCount(rowCount);

if(rowCount==0) {

// no need to execute query:

return Collections.EMPTY_LIST;

}

// query:

if(restore) {

// restore order conditions:

setFieldValue(orderEntriesField, c, _old_orderEntries);

}

return c.setFirstResult(page.getFirstResult())

.setMaxResults(page.getPageSize())

.setFetchSize(page.getPageSize())

.list();

}

private static Field getField(Class clazz, String fieldName) {

try {

return clazz.getDeclaredField(fieldName);

}

catch (Exception e) {

throw new RuntimeException(e);

}

}

private static Object getFieldValue(Field field, Object obj) {

field.setAccessible(true);

try {

return field.get(obj);

}

catch (Exception e) {

throw new RuntimeException(e);

}

}

private static void setFieldValue(Field field, Object target, Object value) {

field.setAccessible(true);

try {

field.set(target, value);

}

catch (Exception e) {

throw new RuntimeException(e);

}

}

}

然後,就可以通過一個DetachedCriteria實現分負查詢。

protected List queryForList(final DetachedCriteria dc, final Page page) {

HibernateCallback callback = new HibernateCallback() {

public Object doInHibernate(Session session) {

Criteria c = dc.getExecutableCriteria(session);

if(page==null)

return c.list();

return PaginationCriteria.query(c, page);

}

};

return hibernateTemplate.executeFind(callback);

}

我們定義的DAO對象全部是線程安全的,因此,可以在Spring中只定義一個實例,然後放心地在多個組件之間共享。所謂線程安全是指多個線程可以安全地執行某個對象實例的全部方法。由於方法的參數和方法內定義的局部變量的引用都存儲在線程的堆棧中,因此各個線程之間互不影響。只有實例的字段是共享的,因此,只要保證實例的字段一經初始化就不再變化,這個實例就是線程安全的。

GenericHibernateDao定義的字段只有Class和HibernateTemplate,其中,Class被定義爲final類型,而HibernateTemplate一旦初始化完畢就不會更改,因此,這是一個線程安全的類,其子類只要保證各自的字段在運行期不變,也都是線程安全的。

11.4.3 調試HQL語句
由於我們使用了Hibernate作爲持久化機制,因此,在DAO中,大量使用了HQL查詢。如何調試這些HQL語句是應用程序開發中必須要解決的,否則,我們只能一遍一遍地修改代碼,再編譯,重新運行。

調試HQL語句有兩個方法,一是打開Hibernate的“hibernate.show_sql”功能,就可以在控制檯看到Hibernate最終生成的所有SQL語句,不過這仍然不太直觀,因此,Hibernate還提供了HibernateTools插件,可以在Eclipse中方便地調試HQL。

將HibernateTools-3.2.0.beta7.zip解壓到Eclipse的安裝目錄,然後重新啓動Eclipse,選擇菜單“Window”→“Show View”→“Other...”,在彈出的對話框中找到Hibernate組,展開就可以看到HibernateTools插件提供的視圖,如圖11-13所示。

還可以直接選擇菜單“Window”→“Open Perspective”→“Other...”,打開Hibernate Console視角,如圖11-14所示。

在“Hibernate Configurations”視圖中單擊按鈕添加一個Hibernate配置,在彈出的“Create Hibernate Console Configuration”對話框中填入如下內容。

Name:livebookstore;

Configuration file:\livebookstore\conf\unused\hibernate.cfg.xml;

選中“Enable hibernate ejb3/annotations”。

在Classpath中添加目錄/livebookstore/web/WEB-INF/classes和jar文件/livebookstore/ lib/core/hsqldb.jar。

然後,在Hibernate Configurations視圖中選中livebookstore,單擊右鍵,在彈出的快捷菜單中選擇“HQL Editor”,即可輸入HQL語句並查看運行結果,如圖11-15所示。


圖11-15


上一頁 返回書目 比價購買 下一頁
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章