springside mysql_SpringSide 3 中的数据库访问层

距离上一篇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;

@Repositorypublic class ArticleDao extends HibernateDao < Article, Long > {

}

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

001.PNG

从截图中可以看出,SpringSide提供了HibernateDao和SimpleHibernateDao两个基类,在这两个基类中,封装了CRUD操作,而HibernateDao类更提供了分页查询函数。这个封装的思路和前一代的SpringSide是一样的,但是有几个区别:

1、可以不创建自己的DAO类,什么意思呢?举例说明,上面为Article创建了ArticleDao类,那么在Action中可以这样用:

ArticleDao articleDao = new ArticleDao();(这只是一个示范,事实上不需要显示创建,因为在SpringSide 3中,靠的都是注入)

但是也可以不要ArticleDao,而直接这样用:

HibernateDao articleDao = new HibernateDao();

这样做有什么好处呢?当然是可以有效减少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语句放到了一起,如下:

002.PNG

代码比较简单,只是为了证明上面写的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

9b8a8a44dd1c74ae49c20a7cd451974e.png 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 的文档。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值