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


上一页 返回书目 比价购买 下一页
阅读更多
上一篇Struts2 Map 映射
下一篇如何使用spring的作用域:
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭