过去三年之间,我们为客户构建的应用程序几乎没有一个可以离开数据存储方面的功能——大多数情况下,是一个关系型数据库。事实上,数据对于应用程序如此重要,能够对数据进行存储,读取,快速可靠的操作甚至更加重要。
本节中,我们将对数据存储层进行讨论,并集中对某些设计模式进行探讨,如数据访问对象(Data Access Object , DAO)。与域对象模型相同,DAO模式同样已经有大批相关文献对其进行讲解—通常这些文献仅仅与如何实现DAO本身相关。因为在之前的8,9,10三章中,我们已经就SpringBlog的DAO实现进行了讲述,这里就不再花费太多的章节。这里,我们就完成一个优秀的DAO设计的某些要点进行讨论。
为何需要数据访问层?
在我们公司,我们为一些开发人员安排了相应的培训计划。当我们召集他们并向他们展示我们的应用程序时,他们问的第一个问题往往是“为什么要为数据访问层投入这么大精力?”。对于大多数Java开发人员而言,对于这个问题的回答可能会非常平静,但往往也非常痛苦。我们花费了无数个不眠之夜,调试那些内嵌数据访问代码的琐碎JSP页面。将数据存储逻辑封装在一系列定义清晰的接口之后,这除了是优良设计经验的一部分,同时,也的确使得开发者的工作更加轻松自如。优秀的DAO在业务逻辑之外提供了一个隔离区,在这类开发人员可以集中精力将不同的域对象模型映射到数据库结构之中。将所有的数据访问逻辑置于同一个区域减少了代码在应用中的散布和重复——这将导致调试和维护的噩梦,更不要提程序员过早花白的双鬓。
通常,设计一个优良的数据访问层并不需要特别的努力,大多数情况下,实现的时间也远远小于我们将代码在应用中四处复制的时间,我们避免了不必要的重复。
设计中需要注意的几点
设计并实现一个数据访问层相当简单,实际上,最复杂的工作也就是编写数据访问代码本身。然而,当构建数据访问层时,我们必须将几个要点深深刻在脑海之中,这将有助于我们得到更简洁、更易于扩展的DAO实现。
域对象还是数据传输对象?
正如之前我们曾经提及的,学校里曾经告诉我们。当我们构建我们的数据访问层时,通常从业务层的服务对象入手,然后根据域对象建立相应的数据传输对象。坦白地说,我们并没有发现必须这样去做的理由;在我们的眼里,这个实践使得了J2EE模式的实际应用方式更加混淆不清。在我们的应用中,从来没有采用过这种设计技术,因为我们至今为止还未发现将域对象直接传递给DAO有什么问题。如果您发现哪些情况下的确需要采用这种技术,请告知我们,我们将在本书的下一版中进行改进。
更多的接口
以接口定义DAO,而非类。当在服务对象中编写与DAO协同工作的代码时,面向接口编程,而不是实现类。记住我们正在使用Spring,将DAO实现实例传入服务层将非常简单,而以接口对DAO进行定义也增加不了多少工作负担。
使用基础接口实现模式
正如我们之前对BlogPosting接口所做的那样,往往以一个抽象类作为接口的基础实现会非常有用。这样同一个DAO的不同实现就可以共享一些共用的功能,从而减少了代码重复情况的出现。为业务接口和DAO接口定义基类之间的区别就在于通过DAO接口我们往往无需对他们进行定义,因为Spring已经为我们做了这一切。例如,考察一下图11-2中的UML模型,这展示了SpringBlog DAO结构中的一个片断。

图 11-2. 在DAO实现中使用Spring基类
这里可以看到我们定义的EntryDao接口,并为其提供了一个基于Hibernate的EntryDao接口实现HibernateEntryDao。相对于从头实现这个接口而言,我们借助Spring中对Hibernate 提供的支持,通过扩展HibernateDaoSupport类加以实现。这个类包括了一些预定义的Hibernate专有属性,如SessionFactory对象,这使得基于依赖注入的DAO配置成为可能。所有的HibernateEntryDao 类都需要实现EntryDAO接口。
我们在第9章看到SpringBlog中Hibernate DAO的完整实现。
Spring同时也为其他ORM提供了类似的辅助类,如iBATIS (第10章), JDO, Apache’s OJB、以及传统的JDBC。
当使用JDBC支持类JdbcDaoSupport,时,我们会发现需要再引入一层抽象类以实现一些数据库原生特性。例如,当我们构建SpringBlog的JDBC DAO时,发现我们可以通过标准SQL实现大部分特性,但是某些新加入的主键需要我们使用数据库特定的功能。为了达到这一目标,我们为每个DAO创建了一个抽象实现,其中包含了核心功能的实现,然后我们为针对不同数据库的DAO提供相应的实现版本。图11-3 展示了这个层次的大体结构

图 11-3. 在JDBC DAO中为数据库差异提供支持
这里我们可以看到我们定义了JdbcAbstractEntryDao,类,它实现了EntryDAO,同时扩展了 JdbcDaoSupport 类。这是个抽象类,在为EntryDAO接口提供一个基础实现之外,它定义了一个抽象方法getIdentitySql()。代码清单 11-12 展示了JdbcAbstractEntryDao的几个部分,为了清晰起见,所有的实现代码都被暂时移除。
代码清单 11-12. 为数据库原生功能定义抽象钩子方法
package com.apress.prospring.data.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springframework.jdbc.object.SqlUpdate;
import com.apress.prospring.data.EntryDao;
import com.apress.prospring.domain.Entry;
public abstract class JdbcAbstractEntryDao extends JdbcDaoSupport implements
EntryDao, InitializingBean {
protected abstract String getIdentitySql();
}
记住, JdbcAbstractEntryDao 包含了大多数实现代码。无论何时,这些实现代码需要查询出最近一次由数据库产生的主键值,它通过调用getIdentitySql() 执行返回命令。通过这种方式,我们可以提供针对特定数据库的实现版本。这也就是代码清单 11-13中所展示的JdbcMysqlEntryDao 类所做的事情。
代码清单 11-13. 实现数据库原生功能
package com.apress.prospring.data.jdbc;
public class JdbcMysqlEntryDao extends JdbcAbstractEntryDao {
protected String getIdentitySql () {
return "select LAST_INSERT_ID()";
}
}
注意,这里我们并没有移除任何代码,这个类所有的代码都在此列出。现在如果我们需要将EntryDAO转移到另外一个数据库系统,我们所需要做的就是创建一个新类,由JdbcAbstractEntryDao继承而来,并对getIdentitySql() 方法提供适当的实现。
DAO 粒度
当决定如何组织我们的DAO接口时,请一定避免DAO/域对象以及DAO/数据库表的方式。有时,这些结构在构思阶段显得非常自然,但是并不一定意味着这些结构都是最好的。我们在项目中尝尝发现的一个大问题就是“每张表一个DAO”。当我们定义了这样的结构,我们就不得不以DAO来对连接表进行描述,而这样做仅仅是为了将多对多关系中的另外两张表相互连接。同时,我们常常发现我们需要将一个域对象传递给许多不同的DAO,以使得数据能正确保存。
这是数据库驱动DAO设计的一个典型案例。这也是我们在实践中发现的一个不好的主意。DAO的目的在于将域对象与数据库相映射。因为我们的应用需要与域对象大量交互,而不是数据库。这暗示我们应该用域对象模型驱动数据设计。让DAO将域对象和数据之间的映射复杂性隐藏起来;这就是它们的任务。我们正在试图避免持久化域对象的需要与多个DAO交互这种情形。如当一个域对象包含了一个指向另一个不同类型域对象的引用,且两个对象都被修改并需要进行持久化,这样,类似的情况自然就会发生。
在这种情况下,我们可以将逻辑封装在服务层,但是我们不必人为去引发这个问题的产生。那么我们想知道我们是否应该让域对象模型驱动DAO设计。OK,Yes or No?Yes,这种情况中,域对象模型的目标在于使得域对象模型中的数据可以进行存储和读取,这样感觉就应该让域对象模型进行驱动。No,这种情况下,盲目的为每个域对象模型创建一个DAO,这将导致每个逻辑数据单元的持久化都需要调用许多个不同的。我们之前的示例中,Order和OrderLine对象是以Cart和CartItem为蓝本构建。由于OrderLine对象的保存或者读取是与Order对象密切相关,那么感觉上应该将这两个域对象的持久化逻辑封装在一个独立的OrderDAO中,而不是创建OrderDao和OrderLineDao。
域对象关联的遍历
当与其他开发者交流时,我们常常发现一个问题,就是他们无法确定在DAO中何时对域对象关联进行遍历,特别是当从数据库读取数据时。观察一下SpringBlog应用中Entry和Attachment对象所处的情况。如图11-1中所示,一个Entry可以包含多个Attachment对象。因此这里的问题就是我们是否应该在读取Entry对象时同时也读取Attachment对象?一样,答案非是即非。我们曾经在一个项目组中工作,负责DAO开发的程序员盲目的将所有关联数据都读取出来,不管这些数据是否会得到使用。这导致了数百次无谓的数据库访问以及数百个对象的无谓创建。读取关联数据本身并没有错误,但是必须是我们需要的时候才这么去做。
在SpringBlog应用中, EntryDao.getAll() 方法负责返回发布项列表。我们希望通过这个方法返回一个简单的发布项列表,而没有考虑任何关于附件的因素。在这个示例中,从数据库中读取附件数据并创建对应的Attachment对象是一个不必要的开销。相反, EntryDao.getById() 方法,每次读取一个特定的Entry对象。SpringBlog用这个方法读取完整的发布项以及评论和附件数据。这个案例中,当我们确实需要读取发布项的附件时,我们进行数据库读取操作。代码清单 11-14 展示了一个基于JDBC实现的片断摘录(我们在这里列出了最主要的代码)。
代码清单 11-14. 在DAO中读取关联数据
package com.apress.prospring.data.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springframework.jdbc.object.SqlUpdate;
import com.apress.prospring.data.AttachmentDao;
import com.apress.prospring.data.CommentDao;
import com.apress.prospring.data.EntryDao;
import com.apress.prospring.domain.Entry;
private CommentDao commentDao;
private AttachmentDao attachmentDao;
private SelectById selectById;
private SelectAll selectAll;
public void setCommentDao(CommentDao commentDao) {
this.commentDao = commentDao;
}
public void setAttachmentDao(AttachmentDao attachmentDao) {
this.attachmentDao = attachmentDao;
}
protected void initDao() throws Exception {
super.initDao();
selectById = new SelectById(getDataSource());
}
public Entry getById(int entryId) {
Entry e = (Entry) selectById.findObject(entryId);
e.setComments(commentDao.getByEntry(e.getEntryId()));
e.setAttachments(attachmentDao.getByEntry(e.getEntryId()));
return e;
}
public List getAll() {
return selectAll.execute();
}
}
正如我们所见, getAll() 方法简单的读取了Entry列表并返回。而getById()方法中,Entry对象加载的同时也被填充了相应的附件对象(通过AttachmentDao实现)。通过选择性的数据加载,而不是全盘读取,我们可以提高系统性能而不影响功能实现。
代码清单 11-14 中的代码并没有显示出另外一个有趣的问题:我们应该如何进行关联数据加载?基本上,有两个选择。第一个是将代码嵌入父域对象的DAO中;第二个,让父域对象的DAO调用子域对象的DAO。这两种方式都无可非议,不过他们是针对不同的场景。当子域对象完全从属于父域对象时,我们倾向于使用第一种方式,Order和OrderLine就是一个示例。我们开发的大多数系统中,OrderLine无需在Order对象的语义之外进行操作。
代码清单 11-15 展示了Spring附属的jPetStore示例中的一段代码。
代码清单 11-15. 在父DAO加载子数据
package org.springframework.samples.jpetstore.dao.ibatis;
import java.util.List;
import org.springframework.dao.DataAccessException;aff
import org.springframework.orm.ibatis.support.SqlMapDaoSupport;
import org.springframework.samples.jpetstore.dao.OrderDao;
import org.springframework.samples.jpetstore.domain.LineItem;
import org.springframework.samples.jpetstore.domain.Order;
public class SqlMapOrderDao extends SqlMapDaoSupport implements OrderDao {
public List getOrdersByUsername(String username) throws DataAccessException {
return getSqlMapTemplate().executeQueryForList(
"getOrdersByUsername", username);
}
public Order getOrder(int orderId) throws DataAccessException {
Object parameterObject = new Integer(orderId);
Order order = (Order) getSqlMapTemplate().executeQueryForObject(
"getOrder", parameterObject);
if (order != null) {
order.setLineItems(getSqlMapTemplate().executeQueryForList(
"getLineItemsByOrderId", new Integer(order.getOrderId())));
}
return order;
}
/* omitted for brevity */
}
这段代码中,我们可以看到getOrdersByUsername() 方法仅仅读取了Order对象。而 getOrder() 方法读取了order对象以及其关联的OrderLine对象。读取OrderLine的逻辑被包含在此DAO中,而非OrderLine对应的DAO。实际上,也不存在这个DAO。当子域对象完全从属于父域对象时,这种实现方式可以完全胜任,第二种实现方式——父域对象的DAO调用子域对象的DAO—更好。
在SpringBlog应用中,我们选择为Attachment单独建立一个DAO,然后让EntryDao 和 CommentDao 实现调用这个DAO。这个决策有以下两个原因。第一,Attachment既是Entry也是Comment的子对象。使用第一种解决方案意味着我们必须在EntryDao和CommentDao中重复实现读取Attachment的代码。对这种情况进行重构,自然会导致独立AttachmentDao的出现。第二,我们需要从所有其他对象中对Attachment进行独立操作;一个独立的DAO就是合乎逻辑的。
没有任何事物阻止你选择何时加载关联数据构成。对于构成一个清晰的子对象完全从属于父对象的父子层次中的域对象而言,我们可以选择在父域对象的DAO中完成对子对象的操作。对于缺乏这样清晰的层次关系的域对象,或者有着多重父子关系的域对象而言,我们可以为子对象创建独立的DAO,将父对象与子对象切分开来。
标准化 与数据访问
在本章前面我们曾经涉及过标准化的相关内容,并且讨论过如何在域对象模型中加以实现。显然,标准化 对象的public的构造方法将对DAO对象带来重大影响。本节中,我们将就为标准化 域对象创建DAO时可能碰到的一些问题进行讨论。
对域对象使用标准化需要考虑的第一个要素是数据识别问题。如果回到代码清单 11-9中展示的ShippingCompany 示例,我们记得客户端代码可以通过静态方法fromInt()访问ShippingCompany示例。ShippingCompany 的每个示例都被赋予了一个唯一ID,以供fromInt方法查找特定的实例。当canonicalizing 域对象被保存至数据库中时,必须保证适当的ID也被保存,以便之后可以获取正确的实例。对于ShippingCompany对象,我们的典型实现方式是在数据库中建立一个表,ShippingCompanies, 以主键映射到正确的ShippingCompany 对象ID。当使用标准化对象时,我们并不需要真的在数据库中建立一个库表,但是我们推荐这种方式,因为我们可以使用外键约束强制数据库的引用合法性。考虑到Order对象包含一个ShippingCompany属性: shippedBy。如果ShippingCompanies表没有一个与Orders表之间的外键约束存在。那么就可能在ShippedBy字段中存储任意数据。
如果我们选择使用JDBC来构建DAO对象,使用标准化不会真的带来太大影响。因为我们担负了在DAO中创建域对象的职责,我们可以简单从使用域对象构造方法切换到使用静态fromInt()方法,以获得适当的标准化 对象。当使用ORM工具如Hibernate或者ibatis时,事情变得有点麻烦。
Hibernate 的确通过PersistentEnum接口提供了对标准化 的支持,这使得我们的域对象实现了一个与ShippingCompany对象非常类似的结构。然而,我们可能对将域对象与某个特定的持久化实现过于耦合持保守态度。如果我们计划长期使用Hibernate,这可能并不是一个太大的问题,但是请谨慎考虑这个决策。如果我们使用ibatis,那么我们就完全没那么幸运了;它完全不支持标准化。谢天谢地,如果我们的ORM导致过耦合或者完全没有这方面的支持,还有个解决方案——使用JDBC。
有Spring的支持,使用JDBC变得如此简单,我们不会再因为它的复杂性而将其的优先级打个折扣。可是,如果我们选择使用某个特定的ORM工具,那么我们就很难用基于JDBC的DAO实现进行替换了——目前我们就是这样。当我们同时在一个类中包含了JDBC和hibernate代码时,我们就无法使用Spring提供的支持。因为我们的DAO只能扩展HibernateDaoSupport 或 JdbcDaoSupport二者之一。幸运的是,这里有一个优雅的解决方案,它允许我们将Hibernate和JDBC代码封装在同一个类之内,而不会因此失去Spring的支持。这个方案的关键在于将JDBC代码封装在内部类中,之后将主体DAO的调用委托给这个内部类去完成。代码清单 11-6提供了一个示例:
代码清单 11-16. 在一个单独的DAO中混合使用Hibernate 和 JDBC
package com.apress.prospring.ch11.canonicalization;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;
public class MyDao extends HibernateDaoSupport {
private MyJdbcDao innerDao;
public MyDao() {
innerDao = new MyJdbcDao();
}
public void update(MyDomainObject obj) {
// use Hibernate to persist the data
}
public MyDomainObject getById(int someId) {
return innerDao.getBy(someId);
}
private static class MyJdbcDao extends JdbcDaoSupport {
public MyDomainObject getBy(int someId) {
// do some real processing
return null;
}
}
}
尽管这只是一个简单的实现,我们也可以从中得到启发。所有的JDBC相关代码都被转移到一个内部类中,而此内部类扩展了JdbcDaoSupport,所有的Hibernate相关功能都留在外部,外部类仍然是 HibernateDaoSupport.的子类。通过这个类,我们可以使用Hibernate完成域对象的持久化,但是我们可以将查询功能交给嵌入的JDBC DAO去完成。
在我们的应用中有效利用标准化 ,可以通过避免大量无谓对象的创建,从而极大提升内存使用效率。如果我们必须更新标准化对象相关的数据,同时我们在DAO中使用了ORM框架,那么我们就可以以ORM无关的形式引入一些JDBC代码来加入标准化 支持。
数据访问层综述
为应用创建一个数据访问层向应用的其他组件提供了一个数据存储的标准机制。没有数据访问层的帮助,我们会发现数据访问代码遍布于整个应用代码之间,这往往导致代码重复且难于维护。从长期来看,这将不可避免的导致错误以及程序员的头疼病发作。
DAO模式为数据访问层的实现提供了一个良好的基础。
我们应该始终以接口定义DAO,然后用选定的数据访问技术加以实现。当使用Spring时,使用接口变得非常简单,同时我们也可以为应用中的其他组件提供一个集中的DAO接口实现。
本章中,我们就持久层设计中的一些主要的设计问题进行了探讨。实际上,数据访问层中的大部分问题都来源于实现,而非设计。您可以在第8,9章中获得更加细致的信息,在这些部分中,我们分别就JDBC、Hibernate和iBATIS进行了探讨。





