Don’t repeat the DAO!--使用Hibernate和Spring AOP購建一個範型型別安全的DAO

goldtime發表於2007-11-14

譯者:Nicholas @ Nirvana Studio
原文地址:http://www-128.ibm.com/developerworks/java/library/j-genericdao.html

使用Hibernate和Spring AOP購建一個範型型別安全的DAO
2006年五月12日

在採用了Java 5的範型之後,要實現一個基於範型型別安全的資料訪問物件(DAO)就變得切實可行了。在這篇文章裡,系統架構師Per Mellqvist展示了一個基於Hibernate的範型DAO實現。然後將介紹如何使用Spring AOP的introduction為一個類增加一個型別安全的介面以便於執行查詢。
[@more@]

對於大多數開發者來說,在系統中為每一個DAO編寫幾乎一樣的程式碼已經成為了一種習慣。同時大家也都認可這種重複就是“程式碼的味道”,我們中的大多數已經習慣如此。當然也有另外的辦法。你可以使用很多ORM工具來避免程式碼的重複編寫。舉個例子,用Hibernate,你可以簡單的使用session操作直接控制你的持久化領域物件。這種方式的負面影響就是丟失了型別安全。

為什麼你的資料訪問程式碼需要一個型別安全的介面?我認為它減少了程式設計錯誤,提高了生產率,尤其是在使用現代高階IDE的時候。首先,一個型別安全的介面清晰的制定了哪些領域物件具有持久化功能。其次,它消除了型別轉換帶來的潛在問題。最後,它平衡了IDE的自動完成功能。使用自動完成功能是最快的方式來記住對於適當的領域類哪些查詢是可用的。

在這篇文章裡,我將展示給大家如何避免一次次地重複編寫DAO程式碼,但同時還收益於型別安全的介面。事實上,所有內需要編寫的是為新的DAO編寫一個Hibernate對映檔案,一個POJO的Java介面,並且10行Spring配置檔案。

DAO實現

DAO模式對於任何Java開發人員來說都是耳熟能詳的。這個模式的實現相當多,所以讓我們仔細推敲一下我這篇文章裡面對於DAO實現的一些假設:

  • 所有系統中的資料庫訪問都是透過DAO來完成封裝
  • 每一個DAO例項對一個主要的領域物件或者實體負責。如果一個領域物件具有獨立的生命週期,那麼它需要具有自己的DAO。
  • DAO具有CRUD操作
  • DAO可以允許基於criteria方式的查詢而不僅僅是透過主鍵查詢。我將這些成為finder方法或者finders。這個finder的返回值通常是DAO所負責的領域物件的集合。

範型DAO介面

範型DAO的基礎就是CRUD操作。下面的介面定義了範型DAO的方法:

public interface GenericDao 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實現上面的介面是非常簡單的。也就是呼叫一下Hibernate的方法和增加一些型別轉換。Spring負責session和transaction管理。

public class GenericDaoHibernateImpl extends Serializable>
    implements GenericDao, FinderExecutor {
    private Class type;
 
    public GenericDaoHibernateImpl(Class 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執行時知道這個物件的型別。下面的程式碼中,我將領域類Person傳遞給構造器並且將Hibernate的session工廠作為一個引數用來例項化DAO:

 id="personDao" class="genericdao.impl.GenericDaoHibernateImpl">
        >
            >genericdaotest.domain.Person>
        >
         name="sessionFactory">
             bean="sessionFactory"/>
        >
>

可用的範型DAO

我還沒有全部完成,但我現在已經有了一個可供作的程式碼。下面的程式碼展示了範型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有能力進行型別安全的CRUD操作。同時也有理由編寫GenericDaoHibernateImpl的子類來為每個領域物件增加查詢功能。但是這篇文章的主旨在於展示如何完成這項功能而不是為每個查詢編寫明確的程式碼,然而,我將會使用多個工具來介紹DAO的查詢,這就是Spring AOP和Hibernate命名查詢。

Spring AOP介紹

你可以使用Spring AOP提供的introduction功能將一個現存的物件包裝到一個代理裡面來增加新的功能,定義它需要實現的新介面,並且將之前所有不支援的方法委派到一個處理機。在我的DAO實現裡面,我用introduction將一定數量的finder方法增加到現存的範型DAO類裡面。因為finder方法針對特定的領域物件,所以它們被應用到表明介面的範型DAO中。

 id="finderIntroductionAdvisor" class="genericdao.impl.FinderIntroductionAdvisor"/>
 
 id="abstractDaoTarget"
        class="genericdao.impl.GenericDaoHibernateImpl" abstract="true">
         name="sessionFactory">
             bean="sessionFactory"/>
        >
>
 
 id="abstractDao"
        class="org.springframework.aop.framework.ProxyFactoryBean" abstract="true">
         name="interceptorNames">
            >
                >finderIntroductionAdvisor>
            >
        >
>

在上面的配置中,我定義了三個Spring bean,第一個bean,FinderIntroductionAdvisor,處理那些introduce到DAO中但是不屬於GenericDaoHibernateImpl類的方法。一會我再介紹Advisor bean的詳細情況。

第二個bean定義為“abstract”。在Spring中,這個bean可以被其他bean重用但是它自己不會被例項化。不同於抽象屬性,bean的定義簡單的指出了我需要一個GenericDaoHibernateImpl的例項同時需要一個SessionFactory的引用。注意GenericDaoHibernateImpl類只定義了一個構造器接受領域類作為引數。因為這個bean是抽象的,我可以無限次的重用並且設定合適的領域類。

最後,第三個,也是最有意思的是bean將GenericDaoHibernateImpl的例項包裝進了一個代理,給予了它執行finder方法的能力。這個bean定義同樣是抽象的並且沒有指定任何介面。這個介面不同於任何具體的例項。

擴充套件通用DAO

每個DAO的介面,都是基於GenericDAO介面的。我需要將為特定的領域類適配介面並且將其擴充套件包含我的finder方法。

public interface PersonDao extends GenericDao {
    List findByName( name);
}

上面的程式碼清晰的展示了透過使用者名稱查詢Person物件列表。所需的Java實現類不需要包含任何的更新操作,因為這些已經包含在了通用DAO裡。

配置PersonDao

因為Spring配置依賴之前的那些抽象bean,所以它變得很緊湊。我需要指定DAO負責的領域類,並且我需要告訴Spring我這個DAO需要實現的介面。

 id="personDao" parent="abstractDao">
     name="proxyInterfaces">
        >genericdaotest.dao.PersonDao>
    >
     name="target">
         parent="abstractDaoTarget">
            >
                >genericdaotest.domain.Person>
            >
        >
    >
>

你可以這樣使用:

public void someMethodCreatingAPerson() {
    ...
    PersonDao dao = (PersonDao)
     beanFactory.getBean("personDao"); // This should normally be injected
 
    Person p = new Person("Per", 90);
    dao.create(p);
 
    List result = dao.findByName("Per"); // Runtime exception
}

上面的程式碼是使用型別安全介面PersonDao的一種正確途徑,但是DAO的實現並沒有完成。當呼叫findByName()的時候導致了一個執行時異常。這個問題是我還沒有findByName()。剩下的工作就是指定查詢語句。要完成這個,我使用Hibernate命名查詢。

Hibernate命名查詢

使用Hibernate,你可以定義任何HQL查詢在對映檔案裡,並且給它一個名字。你可以在之後的程式碼裡面方便的透過名字引用這個查詢。這麼做的一個優點就是能夠在部署的時候調節查詢而不需要改變程式碼。正如你一會將看到的,另一個好處就是實現一個“完整”的DAO而不需要編寫任何Java實現程式碼。

 package="genericdaotest.domain">
      name="Person">
          name="id">
              class="native"/>
         >
          name="name" />
          name="weight" />
     >
 
      name="Person.findByName">
         
     >
>

上面的程式碼定義了領域類Person的Hibernate對映檔案,有兩個屬性:name和weight。Person是一個具有上面屬性的簡單的POJO。這個檔案同時包含了一個查詢,透過提供的name屬性從資料庫查詢Person例項。Hibernate為命名查詢提供了不真實的名稱空間功能。為了便於討論,我將所有的查詢名字的字首變成領域類的的名稱。在現實場景中,使用完整的類名,包含包名,是一個更好的主意。

總覽

你已經看到了為任何領域物件建立並配置DAO的所需步驟了。這三個簡單的步驟就是:

  1. 定義一個介面繼承GenericDao並且包含任何所需的finder方法
  2. 在對映檔案中為每個領域類的finder方法增加一個命名查詢。
  3. 為DAO增加10行Spring配置

可重用的DAO類

Spring advisor和interceptor的功能比較瑣碎,事實上他們的工作都引用回了GenericDaoHibernateImpl類。所有帶有“find”開頭的方法都被傳遞給DAO的單一方法executeFinder()。

public class FinderIntroductionAdvisor extends DefaultIntroductionAdvisor {
    public FinderIntroductionAdvisor() {
        super(new FinderIntroductionInterceptor());
    }
}
 
public class FinderIntroductionInterceptor implements IntroductionInterceptor {
 
    public  invoke(MethodInvocation methodInvocation) throws  {
 
        FinderExecutor genericDao = (FinderExecutor) methodInvocation.getThis();
 
         methodName = methodInvocation.getMethod().getName();
        if (methodName.startsWith("find")) {
            [] arguments = methodInvocation.getArguments();
            return genericDao.executeFinder(methodInvocation.getMethod(), arguments);
        } else {
            return methodInvocation.proceed();
        }
    }
 
    public boolean implementsInterface( intf) {
        return intf.isInterface() && FinderExecutor.class.isAssignableFrom(intf);
    }
}

executeFinder() 方法

上面的程式碼唯一缺的就是executeFinder的實現。這個程式碼觀察被呼叫的類的名字和方法,並且將他們與Hibernate的查詢名相匹配。你可以使用一個FinderNamingStrategy來啟用其他方式的命名查詢。預設的實現查詢一個名為“ClassName.methodName”的查詢,ClassName是除包名之外的類名。

public List executeFinder( method, final [] queryArgs) {
     final  queryName = queryNameFromMethod(method);
     final  namedQuery = getSession().getNamedQuery(queryName);
     [] namedParameters = namedQuery.getNamedParameters();
     for(int i = 0; i < queryArgs.length; i++) {
              arg = queryArgs[i];
              argType =  namedQuery.setParameter(i, arg);
      }
      return (List) namedQuery.list();
}
 
public  queryNameFromMethod( finderMethod) {
     return type.getSimpleName() + "." + finderMethod.getName();
}

總結

在Java 5之前,Java語言並不支援程式碼同時具有型別安全和範性的特性;你不得不二者選一。在這篇文章裡,你可以看到使用Java 5範型支援並且結合Spring和Hibernate(和AOP)一起來提高生產力。一個範型型別安全的DAO類非常容易編寫,所有你需要做的就是一個介面,一些命名查詢,並且10行Spring配置,並且可以極大的減少錯誤,同時節省時間。

程式碼下載: j-genericdao.zip ae4

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/778728/viewspace-982413/,如需轉載,請註明出處,否則將追究法律責任。

相關文章