`

Don’t repeat the DAO!

阅读更多
原文地址: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 <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实现上面的接口是非常简单的。也就是调用一下Hibernate的方法和增加一些类型转换。Spring负责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);
    }
 
    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:
<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

我还没有全部完成,但我现在已经有了一个可供作的代码。下面的代码展示了范型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中。
<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>

在上面的配置中,我定义了三个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<Person, Long> {
    List<Person> findByName(String name);
}

    }

上面的代码清晰的展示了通过用户名查找Person对象列表。所需的Java实现类不需要包含任何的更新操作,因为这些已经包含在了通用DAO里。
配置PersonDao

因为Spring配置依赖之前的那些抽象bean,所以它变得很紧凑。我需要指定DAO负责的领域类,并且我需要告诉Spring我这个DAO需要实现的接口。
<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>

你可以这样使用:
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的实现并没有完成。当调用findByName()的时候导致了一个运行时异常。这个问题是我还没有findByName()。剩下的工作就是指定查询语句。要完成这个,我使用Hibernate命名查询。
Hibernate命名查询

使用Hibernate,你可以定义任何HQL查询在映射文件里,并且给它一个名字。你可以在之后的代码里面方便的通过名字引用这个查询。这么做的一个优点就是能够在部署的时候调节查询而不需要改变代码。正如你一会将看到的,另一个好处就是实现一个“完整”的DAO而不需要编写任何Java实现代码。
<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>

上面的代码定义了领域类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> 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语言并不支持代码同时具有类型安全和范性的特性;你不得不二者选一。在这篇文章里,你可以看到使用Java 5范型支持并且结合Spring和Hibernate(和AOP)一起来提高生产力。一个范型类型安全的DAO类非常容易编写,所有你需要做的就是一个接口,一些命名查询,并且10行Spring配置,并且可以极大的减少错误,同时节省时间。
http://www.nirvanastudio.org/java/dont-repeat-the-dao.html
分享到:
评论

相关推荐

    外文翻译计算机外文翻译Don't repeat the DAO!

    ### 外文翻译计算机外文翻译“不要重复DAO!”——基于Java 5泛型的类型安全数据访问对象实现 #### 概述 在计算机领域的软件开发过程中,数据访问层(Data Access Layer)的设计与实现至关重要。传统的数据访问...

    mysql自动生成mybits配置,实体,dao

    MySQL和MyBatis是Java开发中常用的数据库管理和持久层框架。`mybatis-generator`是一个强大的工具,能够根据MySQL...同时,这也是一种良好的实践,遵循了DRY(Don't Repeat Yourself)原则,避免了重复的手动编码工作。

    基于Druid的SqlParser模块解析create table语句创建java POJO和DAO类的效率工具.zip

    在Java开发中,数据库操作是不可或缺的一部分,而Druid是一个非常流行的数据库连接池组件,它提供了丰富的监控功能...这体现了Java编程中的“Don't Repeat Yourself”(DRY)原则,符合现代软件开发的高效、敏捷理念。

    mybatis模板生成器

    通过这种方式,MyBatis模板生成器帮助开发者遵循DRY(Don't Repeat Yourself)原则,避免重复编写相似的代码,使得代码更加规范且易于维护。同时,它还支持多种数据库,具备良好的兼容性和可扩展性,是MyBatis用户...

    STRUTS——SQL添加删除

    - 遵循DRY(Don't Repeat Yourself)原则,避免代码重复,提高代码复用性。 - 保持代码整洁,遵循编码规范,使代码易于理解和维护。 以上就是关于"STRUTS——SQL添加删除"的相关知识点,涵盖了Struts框架的基础,...

    java代码生成器

    Java代码生成器是一种工具,它能够自动化创建基于数据库表结构的DAO、Service、Controller和...同时,它也鼓励开发者遵循良好的编程实践,如模块化和DRY(Don't Repeat Yourself)原则,使得代码更加规范和易于维护。

    JAVA设计模式之事务处理.pdf

    1. 代码重复:每个事务方法都需要类似的事务管理代码,这违反了DRY(Don't Repeat Yourself)原则。 2. 过度耦合:业务代表对象`BookStoreManagerImpl`包含了过多的事务控制细节,使得职责过于复杂。 3. 嵌套事务...

    SSM框架使用Generator生成实体类,映射文件

    SSM框架,全称为Spring、SpringMVC和MyBatis的集成框架,是Java开发Web应用的常用技术栈。在SSM项目开发中,手动编写实体类、DAO...同时,这也遵循了DRY(Don't Repeat Yourself)原则,使得代码更加规范和易于维护。

    mybatis generator插件

    总的来说,MyBatis Generator是一个非常实用的工具,它帮助开发者遵循“DRY”(Don't Repeat Yourself)原则,减少重复劳动,专注于业务逻辑的实现。虽然在安装过程中可能会遇到一些问题,但一旦设置好,它将大大...

    A code generator for iBATIS

    通过深入理解iBATIS框架和这个代码生成工具,开发者可以更高效地进行数据库相关的开发工作,同时也能更好地遵循DRY(Don't Repeat Yourself)原则,提高代码质量。对于大型项目或团队开发来说,这样的工具是非常有...

    JSP数据库通用模块开发与系统移植

    这样的模块设计应遵循DRY(Don't Repeat Yourself)原则,避免代码重复,提高代码复用性和可维护性。模块化设计可以将数据库操作封装为服务,供其他应用组件调用。 4. **数据访问对象(DAO)模式**:DAO是设计模式...

    Mybatis-generator.rar

    总结来说,Mybatis Generator是Mybatis框架的一个强大辅助工具,它通过自动生成代码,降低了开发工作量,提高了开发效率,同时也遵循了DRY(Don't Repeat Yourself)原则,减少了因为手动编写重复代码而可能产生的...

    parancoe-reference-guide.pdf

    Parancoe 是一个旨在简化 Web 应用发布的项目,它强调“约定优于配置”(Convention Over Configuration, CoC)和“不要重复自己”(Don't Repeat Yourself, DRY)的原则。该项目由 JUG Padova(Java User Group ...

    Java实现图书馆管理系统(源码)

    13. **设计原则与最佳实践**:遵循SOLID原则(单一职责、开闭、里氏替换、接口隔离、依赖倒置),以及DRY(Don't Repeat Yourself)原则,保持代码的整洁和可维护。 以上是Java实现图书馆管理系统可能涉及的技术和...

    Web应用开发规范.pdf

    - **CSS**:用于样式控制,应遵循模块化和DRY(Don't Repeat Yourself)原则,使用预处理器如Sass或Less可以提高可维护性。 - **JS**:前端脚本,负责交互逻辑,可使用模块化工具如CommonJS或ES6模块,遵循AMD...

    Java学生成绩管理系统源代码

    14. **设计原则与最佳实践**:遵循SOLID(单一职责、开闭、里氏替换、接口隔离、依赖倒置)原则和DRY(Don't Repeat Yourself)原则,使代码结构清晰、可读性强。 15. **文档编写**:良好的注释和项目文档,如...

    通讯录web版

    同时,项目可能遵循一定的代码规范和设计原则,比如DRY(Don't Repeat Yourself)和SOLID,以保证代码的整洁和可读性。 综上所述,"通讯录Web版"项目展示了如何利用JSP、Servlet和JDBC等Java技术构建一个功能齐全的...

    jsp编程技巧.rar

    - 遵循DRY(Don't Repeat Yourself)原则,避免代码冗余。 这个压缩包内的"JSP编程技巧"文档很可能详细阐述了这些概念,而"readme.txt"和"说明.htm"可能是对这些技巧的进一步解释和示例。通过深入学习和实践这些...

    专题资料(2021-2022年)Java项目组开发规范.doc

    代码应遵循DRY(Don't Repeat Yourself)原则,避免重复代码。在编写代码时,应考虑异常处理、错误报告和日志记录,以确保系统的健壮性。 总的来说,《Java项目组开发规范》不仅关注代码的编写,还强调团队协作和...

Global site tag (gtag.js) - Google Analytics