[转]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的范型之后,要实现一个基于范型类型安全的数据访问对象(DAO)就变得切实可行了。在这篇文章里,系统架构师Per Mellqvist展示了一个基于Hibernate的范型DAO实现。然后将介绍如何使用Spring AOP的introduction为一个类增加一个类型安全的接口以便于执行查询。

对于大多数开发者来说,在系统中为每一个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<t></t> type;
 
    public GenericDaoHibernateImpl(Class<t></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运行时知道这个对象的类型。下面的代码中,我将领域类 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<person></person> findByName(String 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<person></person> 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 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() 方法

上面的代码唯一缺的就是executeFinder的实现。这个代码观察被调用的类的名字和方法,并且将他们与Hibernate的查询名相匹配。你可以使用一个FinderNamingStrategy来激活其他方式的命名查询。默认的实现查找一个名为 “ClassName.methodName”的查询,ClassName是除包名之外的类名。

public List<t></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></t>) namedQuery.list();
}
 
public String queryNameFromMethod(Method finderMethod) {
     return type.getSimpleName() + "." + finderMethod.getName();
}

总结

在Java 5之前,Java语言并不支持代码同时具有类型安全和范性的特性;你不得不二者选一。在这篇文章里,你可以看到使用Java 5范型支持并且结合Spring和Hibernate(和AOP)一起来提高生产力。一个范型类型安全的DAO类非常容易编写,所有你需要做的就是一个接口,一些命名查询,并且10行Spring配置,并且可以极大的减少错误,同时节省时间。

代码下载: j-genericdao.zip

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值