京山游侠

专注技术,拒绝扯淡
posts - 50, comments - 868, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

SpringSide 3 中的数据库访问层

Posted on 2009-07-09 23:21 京山游侠 阅读(5750) 评论(8)  编辑  收藏 所属分类: SpringSide开发实战

距离上一篇SpringSide 3 中的Struts 2已经有一段时间了,中间因为研究了一下Fedora 10,所以就把对SpringSide 3的学习搁置了下来。以目前的Web开发来看,主流的模式还是MVC,在SpringSide 3中,控制器使用的是Struts 2,前面我们已经探讨过了,接下来毫无疑问应该探讨Model层,也就是和数据库访问有关的内容。

在SpringSide 3 中,数据库访问层使用的是Hibernate,Hibernate是一个很优秀的ORM框架,是大家耳熟能详的东西了。关于Hibernate的内容,很多人是写了又写,我想我是很难写出新意了。不过我的思路是这样的,我从实际开发的过程出发,写出在SpringSide 3中使用Hibernate的步骤,在这些步骤中,探讨SpringSide 3对Hibernate的封装,探讨数据持久层的单元测试,探讨二级缓存和性能优化。

我创建一个虚拟的应用场景来做示范,假设我们开发的是一个简单的文章发布系统,实现对文章简单的增删查改功能。同时为了演示多个表之间的关联查询,假设每篇文章有多篇评论。这时,我们需要在数据库中创建两个表,如下:

create   table  articles(
id 
int   primary   key  auto_increment,
subject 
varchar ( 20 not   null ,
content 
text );

create   table  comments(
id 
int   primary   key  auto_increment,
content 
varchar ( 255 ),
article_id 
int   not   null ,
foreign   key  (article_id)  references  articles(id)
);


我的开发习惯是先写数据库Schema,再写Hibernate的Entity类,再写DAO类,最后在Action里面使用DAO类。这只是我个人的习惯,大家都知道,Hibernate有通过Entity类自动生成数据库Schema的工具,这说明很多人习惯先写Entity类而不关注数据库的细节。但是我从没有用过这样的工具,我喜欢了解数据库的细枝末节,所以我总是自己写数据库Schema。

在MySQL的客户端直接执行上面的SQL语句就可以创建这两个表了。这里需要额外提一下的是我使用的数据库是MySQL,而不是默认的Derby,要把SpringSide创建的项目的数据库更换为MySQL并不难,只需要如下几个步骤:
1、更改数据库地址、用户名、密码(MySQL需要在数据库地址中指定UTF-8编码);
2、更改数据库驱动、Dialect,同时,需要自己下载MySQL的JDBC驱动放到项目中;
3、SQL文件,因为Derby的语法和MySQL的有点不一样,比如MySQL中就应该使用AUTO_INCREMENT,而不是GENERATED ALWAYS as IDENTITY,并且Drop数据表的时候,MySQL可以加上IF EXISTS选项。

下一步,编写Entity类:

package  cn.puretext.entity.web;

import  java.util.LinkedHashSet;
import  java.util.Set;

import  javax.persistence.CascadeType;
import  javax.persistence.Entity;
import  javax.persistence.JoinColumn;
import  javax.persistence.OneToMany;
import  javax.persistence.OrderBy;
import  javax.persistence.Table;

import  org.hibernate.annotations.Cache;
import  org.hibernate.annotations.CacheConcurrencyStrategy;
import  org.hibernate.annotations.Fetch;
import  org.hibernate.annotations.FetchMode;

import  cn.puretext.entity.IdEntity;

@Entity
//  表名与类名不相同时重新定义表名.
@Table(name  =   " articles " )
//  默认的缓存策略.
@Cache(usage  =  CacheConcurrencyStrategy.READ_WRITE)
public   class  Article  extends  IdEntity {
    
private  String subject;
    
private  String content;
    
private  Set < Comment >  comments  =   new  LinkedHashSet < Comment > ();
    
    
public  String getSubject() {
        
return  subject;
    }

    
public   void  setSubject(String subject) {
        
this .subject  =  subject;
    }

    
public  String getContent() {
        
return  content;
    }

    
public   void  setContent(String content) {
        
this .content  =  content;
    }

    @OneToMany(cascade 
=  { CascadeType.ALL })
    @JoinColumn(name 
=   " article_id " )
    
//  Fecth策略定义
    @Fetch(FetchMode.SUBSELECT)
    
//  集合按id排序.
    @OrderBy( " id " )
    
//  集合中对象id的缓存.
    @Cache(usage  =  CacheConcurrencyStrategy.READ_WRITE)
    
public  Set < Comment >  getComments() {
        
return  comments;
    }

    
public   void  setComments(Set < Comment >  comments) {
        
this .comments  =  comments;
    }
}

package  cn.puretext.entity.web;

import  javax.persistence.Entity;
import  javax.persistence.Table;

import  org.hibernate.annotations.Cache;
import  org.hibernate.annotations.CacheConcurrencyStrategy;

import  cn.puretext.entity.IdEntity;

@Entity
//  表名与类名不相同时重新定义表名.
@Table(name  =   " comments " )
//  默认的缓存策略.
@Cache(usage  =  CacheConcurrencyStrategy.READ_WRITE)
public   class  Comment  extends  IdEntity {
    
private  String content;
    
    
public  String getContent() {
        
return  content;
    }

    
public   void  setContent(String content) {
        
this .content  =  content;
    }
}


通过上面的代码,大家可以注意到如下的信息:
1、上面的Entity类都没有了id,为什么呢?因为白衣把它抽出来了,做了一个IdEntity基类让我们继承,所以,以后只要是数据库中含有id的表,编写Entity类的时候都可以从IdEntity继承。
2、Entity中使用的Annotation就不用多说了,JPA Annotation已经不是什么新东西,在上面的Entity中,我演示了一下@OneToMany,而白衣在项目里面大量演示了@ManyToMany,我以前写的一篇博文《打通数据持久层的任督二脉》中讨论了@OneToOne和@ManyToOne,这回算是补齐了。
3、上面的Entity中涉及到了抓取策略和缓存策略,使用注解设置起来也很简单。

下一步,编写DAO类:

package  cn.puretext.dao;

import  org.springframework.stereotype.Repository;
import  org.springside.modules.orm.hibernate.HibernateDao;

import  cn.puretext.entity.web.Article;

@Repository
public   class  ArticleDao  extends  HibernateDao < Article, Long >  {

}


可以看到该类非常之简单,原因嘛,自然是因为SpringSide的基类做了大量的工作。这这里,该DAO类的继承层次是这样的:


从截图中可以看出,SpringSide提供了HibernateDao和SimpleHibernateDao两个基类,在这两个基类中,封装了CRUD操作,而HibernateDao类更提供了分页查询函数。这个封装的思路和前一代的SpringSide是一样的,但是有几个区别:
1、可以不创建自己的DAO类,什么意思呢?举例说明,上面为Article创建了ArticleDao类,那么在Action中可以这样用:
ArticleDao articleDao = new ArticleDao();(这只是一个示范,事实上不需要显示创建,因为在SpringSide 3中,靠的都是注入)
但是也可以不要ArticleDao,而直接这样用:
HibernateDao<Article,Long> articleDao = new HibernateDao<Article,Long>();
这样做有什么好处呢?当然是可以有效减少Dao层类的数量,如果有的Dao类使用得比较少,那么就没有必要专门为它创造一个Dao类了。

写到这里,我又忍不住要评论一下江南白衣在项目架构方面的一些习惯了,他的层次太多,这应该是他在实际项目中锤炼出来的经验,但是和教科书上的就不大一样了,教科书上的三层就是三层,而白衣可以把它扩展到4层甚至5层,白衣的层次可以总结成Entity->DAO->Service(Manager)->Action->View,其中Service这一层命名还不统一,包名是Service,类名中用的是Manager。我觉得这个大家可以探讨探讨,也许白衣认为DAO里面不应该包含业务逻辑,只应该包含CRUD和分页操作,而Action里面也不应该包含业务逻辑,所以就单独抽出一层来了吧,所以这一层应该称为Bussiness层比较合适,而白衣也认为,有时候DAO层和Bussiness层可以合并在一起。另外,我认为白衣在项目中搞的package也太多了一点,在IDE里面不方便,所以我的实际项目中,我会对包重新进行整理。

2、在DAO类中可以使用Hibernate的原生API。我们来总结一下在Hibernate中通常采用的查询方式:一是使用HQL语言,它的过程基本上是先获取Session,然后创建Query对象,最后通过Query对象执行HQL语句;二是使用条件查询,它的过程基本上是先获取Session,然后创建Creteria对象,然后执行Creteria对象的list()方法。而在Dao类中,我们可以很简单的通过sessionFactory.getCurrentSession()来获得Session对象,进而很方便的使用到HQL或者Creteria。

3、在SpringSide 2中,我们可以对数据表中的数据不做物理删除,该特性得益于白衣提供的@Undeletable注解和HibernateEntityExtendDao类,在SpringSide 3中,该特性没有了。现在回想起来,我觉得该特性也没有什么存在的必要。

后面再继续探讨分页查询和性能优化。现在的任务是赶紧确认一下这Entity层和Dao层能否正常工作,完成该任务的最佳途径,当然是单元测试了。

在SpringSide 3中,编写单元测试非常方便,只需要继承白衣提供的SpringContextTestCase类或者SpringTxTestCase类即可,事实上,只有继承SpringTxTestCase类才能正常工作,因为我们的项目的配置无法让我们工作在非事务的环境下。继承这个类有什么用处呢?它的用处就是可以读取项目中的applicationContext.xml文件,自动建立数据源、Dao对象,并把Dao对象注入到测试用例中,所以,测试类的代码非常简洁,如下:

package  cn.puretext.unit.service;

import  java.util.List;

import  org.junit.Test;
import  org.springframework.beans.factory.annotation.Autowired;
import  org.springside.modules.orm.Page;
import  org.springside.modules.test.junit4.SpringTxTestCase;

import  cn.puretext.dao.ArticleDao;
import  cn.puretext.entity.web.Article;

public   class  DaoTest  extends  SpringTxTestCase {
    @Autowired
    
private  ArticleDao articleDao;

    
public  ArticleDao getArticleDao() {
        
return  articleDao;
    }

    
public   void  setArticleDao(ArticleDao articleDao) {
        
this .articleDao  =  articleDao;
    }

    @Test
    
public   void  addArticle() {
        Article article 
=   new  Article();
        article.setSubject(
" article test " );
        article.setContent(
" article test " );
        articleDao.save(article);
    }
}

 

因为该单元测试工作在事务环境下,所以运行单元测试不会改变数据库中的数据。白衣提供的这两个类事实上只是在Spring 2.5的测试框架上做了一点点改进。关于Spring 2.5测试框架的详细介绍,大家可以到“IBM DeveloperWorks 中国”上去看这一篇文章:
http://www.ibm.com/developerworks/cn/java/j-lo-spring25-test/

但是白衣自己的做法却完全不同,在白衣写的单元测试中,他偏偏用的是EasyMock,关于EasyMock的使用方法,大家可以到“IMB DeveloperWorks 中国”上去看这一篇文章:
http://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/

再让大家看一下截图,我特地把测试类的代码、JUnit著名的绿条和Hibernate输出的SQL语句放到了一起,如下:

代码比较简单,只是为了证明上面写的Entity和Dao能够正常运行。在下面的内容里,随着我们的探讨,测试代码的内容会逐渐增加。

上文的内容演示了SpringSide 3中Hibernate的使用过程和单元测试,也提到了SpringSide 3提供的CRUD封装,这些都很简单。在SpringSide 3对Hibernate的封装中,还有一个重点,那就是分页查询。

分页查询有HibernateDao类实现,要配合Page类来使用。Page类一般用来设置查询条件,并返回查询结果,举例说明,如果对Articles表中的数据进行分页显示,每一页10条记录,那么查询第二页应该怎么办呢?代码如下:

@Test
 
public   void  pageQuery() {
  Page
< Article >  page  =   new  Page < Article > ();
  page.setPageSize(
10 );
  page.setPageNo(
2 );
  page 
=  articleDao.getAll(page);
  List
< Article >  articles  =  page.getResult();
 }


以上代码在单元测试中进行,这个过程很容易理解,就是先创建一个Page对象,然后设置该页的大小和序号,就可以直接查找该页的数据了。同时,Page类还有很多辅助方法,如获取总的记录条数,获取页的总数,获取是否有下一页等等。

Page只是一个辅助类,真正的查询操作是在HibernateDao类中完成的,具体代码如下:

/**
  * 按Criteria分页查询.
  * 
  * 
@param  page 分页参数.
  * 
@param  criterions 数量可变的Criterion.
  * 
  * 
@return  分页查询结果.附带结果列表及所有查询时的参数.
  
*/
 @SuppressWarnings(
" unchecked " )
 
public  Page < T >  find( final  Page < T >  page,  final  Criterion criterions) {
  Assert.notNull(page, 
" page不能为空 " );

  Criteria c 
=  createCriteria(criterions);

  
if  (page.isAutoCount()) {
   
int  totalCount  =  countCriteriaResult(c);
   page.setTotalCount(totalCount);
  }

  setPageParameter(c, page);
  List result 
=  c.list();
  page.setResult(result);
  
return  page;
 }


可以看到,白衣的实现用的是Hibernate中的条件查询,从上面的代码可以看出,该过程是先创建Criteria对象,然后查询记录的总数,并将记录的总数填入到Page对象中,然后再调用setPageParameter方法将Page对象中的信息填入到Criteria对象中,最后调用Criteria对象的list()方法来获取结果。

下面跟踪到setPageParameter方法中,其代码如下:

  protected  Criteria setPageParameter( final  Criteria c,  final  Page < T >  page) {
  
// hibernate的firstResult的序号从0开始
  c.setFirstResult(page.getFirst()  -   1 );
  c.setMaxResults(page.getPageSize());
  
/* 以下代码省略 */
}


可以看到,该方法中只是简单地调用了Criteria对象的setFirstResult和setMaxResults方法,这都是Hibernate的原生API,没有什么需要特殊说明的。我比较关心的是分页查询所生成的SQL语句及其正确性。

讲到这里,我得提一下我的技术背景:在使用MySQL之前,我有很长一段时间使用的是MS SQL Server 2000。为什么要提这个问题呢?那是因为站在SQL Server 2000的角度,处理分页问题是比较困难的。在SQL Server 2000中,如果要获取指定条数的记录,只能使用top关键字,也就是说要获取10条数据,就应该使用select * top 10 from articles,那么怎么定位到第二页呢?就必须知道第二页的第一条数据的ID是多少,然后用这样的语句select * top 10 from articles where id >= ?,那怎么知道第二页的第一条记录的ID是多少呢?免不了又要多一次查询如select id top 20 from articles order by id desc。

所以在SQL Server 2000中,要实现分页查询比较困难,不是思考起来困难,而是提高效率困难,必须得避免多次查询。解决的办法当然有,要么使用存储过程,要么在前面的select语句中加入子查询。但是不管采取哪种办法,SQL语句写起来都不简单。

在MySQL中,该问题就简单多了,MySQL不提供top,但提供limit,更重要的是limit接受两个参数,而不是像top只接受一个参数。limit后面的参数可以是{[offset,] row_count | row_count OFFSET offset},其中的offset就代表了第2页的第一条数据所在的位置,大家请注意,这里说的是位置,而不是SQL Server 2000中的ID,这两者是有区别的,因为ID可能不连续,而位置肯定是连续的,所以位置是可以通过简单的数学计算来获得的,这样,MySQL就只需要生成一个简单的SQL语句select * from articles limit 10,10。

下面是Hibernate自己生成的SQL语句:
    select
        this_.id as id4_0_,
        this_.content as content4_0_,
        this_.subject as subject4_0_
    from
        articles this_ limit ?,
        ?

为了和SQL Server 2000对比,我把配置文件中的Dialect改为org.hibernate.dialect.SQLServerDialect,得到的SQL语句如下:
    select
        top 20 this_.id as id4_0_,
        this_.content as content4_0_,
        this_.subject as subject4_0_
    from
        articles this_
2009-07-09 22:22:53,950 [main] WARN  [org.hibernate.util.JDBCExceptionReporter] - SQL Error: 1064, SQLState: 42000
2009-07-09 22:22:53,969 [main] ERROR [org.hibernate.util.JDBCExceptionReporter] - You have an error in your SQL syntax;

因为我没有把数据库迁移到SQL Server,所以该语句一运行就出错了,不过从该语句中的top 20也可以看出,要么该语句的作用是为了得到第二页的第一条记录的ID,然后后面再跟一条SQL语句,只不过因为出现错误,所以后面的语句没有显示出来,要么是直接取出20条记录,并抛弃10条,只留下第二页的数据。总之,和我之前预想的一样,性能得不到保证。

通过搜索引擎我还查出,Oracle也不支持limit语句,所以说,我们不能完全相信Hibernate,必要的时候,还是得靠自己写存储过程。

Fetch策略也是影响性能的一个方面,Fetch策略主要是针对Entity中的集合数据,正如白衣所说,很多人多只知道使用默认的Lazy策略,我就是这很多人中的一个,以前我还因为Lazy策略出现过问题,什么问题呢,那就是我先获取一个Entity的数据,然后把在Entity保存到HttpSession中,然后在使用该对象中的集合数据时,就报错了,为什么呢,因为这个时候Hibernate的Session早就关闭了,所以出错。

关于Fetch策略的选择,SpringSide的文档和Hibernate的文档上面都写得很清楚,我就不罗嗦了,至于在代码中怎么设置Fetch策略,代码的注释很清楚,一看就会。

最后谈一谈二级缓存,Session中的缓存是一级缓存,ehcache提供二级缓存,关于二级缓存的配置,主要涉及到两个地方,一个是xml配置文件,另一个是Entity类中的注解,xml配置文件中配置的是ehcache的属性,而Entity中的注解设置了隔离级别,具体内容请参阅SpringSide 3 的文档。


评论

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2009-07-10 13:36 by h521999
通俗易懂,图文并茂,非常不错

# re: SpringSide 3 中的数据库访问层[未登录]  回复  更多评论   

2009-07-10 22:50 by 过客
使用Oracle,hibernate会通过rownum来分页的。

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2009-07-11 12:13 by 虎啸龙吟
很不错。受教了。
希望在springSide上有更多的文章。

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2009-07-12 17:03 by dd2086
好文呐 关注你的文章

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2010-04-09 16:57 by 游客
,,太经典了,,,太感谢了,,

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2010-06-18 11:25 by lacewigs
Great site

# re: SpringSide 3 中的数据库访问层  回复  更多评论   

2012-01-29 13:44 by qinjiannet
经典,受教了。

# re: SpringSide 3 中的数据库访问层[未登录]  回复  更多评论   

2012-05-14 16:08 by 过客
感谢大侠

只有注册用户登录后才能发表评论。


网站导航: