京山游俠

          專注技術(shù),拒絕扯淡
          posts - 50, comments - 868, trackbacks - 0, articles - 0
            BlogJava :: 首頁 :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          日歷

          <2009年7月>
          2829301234
          567891011
          12131415161718
          19202122232425
          2627282930311
          2345678

          搜索

          •  

          積分與排名

          • 積分 - 658186
          • 排名 - 73

          最新評論

          距離上一篇SpringSide 3 中的Struts 2已經(jīng)有一段時間了,中間因為研究了一下Fedora 10,所以就把對SpringSide 3的學(xué)習(xí)擱置了下來。以目前的Web開發(fā)來看,主流的模式還是MVC,在SpringSide 3中,控制器使用的是Struts 2,前面我們已經(jīng)探討過了,接下來毫無疑問應(yīng)該探討Model層,也就是和數(shù)據(jù)庫訪問有關(guān)的內(nèi)容。

          在SpringSide 3 中,數(shù)據(jù)庫訪問層使用的是Hibernate,Hibernate是一個很優(yōu)秀的ORM框架,是大家耳熟能詳?shù)臇|西了。關(guān)于Hibernate的內(nèi)容,很多人是寫了又寫,我想我是很難寫出新意了。不過我的思路是這樣的,我從實際開發(fā)的過程出發(fā),寫出在SpringSide 3中使用Hibernate的步驟,在這些步驟中,探討SpringSide 3對Hibernate的封裝,探討數(shù)據(jù)持久層的單元測試,探討二級緩存和性能優(yōu)化。

          我創(chuàng)建一個虛擬的應(yīng)用場景來做示范,假設(shè)我們開發(fā)的是一個簡單的文章發(fā)布系統(tǒng),實現(xiàn)對文章簡單的增刪查改功能。同時為了演示多個表之間的關(guān)聯(lián)查詢,假設(shè)每篇文章有多篇評論。這時,我們需要在數(shù)據(jù)庫中創(chuàng)建兩個表,如下:

          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)
          );


          我的開發(fā)習(xí)慣是先寫數(shù)據(jù)庫Schema,再寫Hibernate的Entity類,再寫DAO類,最后在Action里面使用DAO類。這只是我個人的習(xí)慣,大家都知道,Hibernate有通過Entity類自動生成數(shù)據(jù)庫Schema的工具,這說明很多人習(xí)慣先寫Entity類而不關(guān)注數(shù)據(jù)庫的細節(jié)。但是我從沒有用過這樣的工具,我喜歡了解數(shù)據(jù)庫的細枝末節(jié),所以我總是自己寫數(shù)據(jù)庫Schema。

          在MySQL的客戶端直接執(zhí)行上面的SQL語句就可以創(chuàng)建這兩個表了。這里需要額外提一下的是我使用的數(shù)據(jù)庫是MySQL,而不是默認的Derby,要把SpringSide創(chuàng)建的項目的數(shù)據(jù)庫更換為MySQL并不難,只需要如下幾個步驟:
          1、更改數(shù)據(jù)庫地址、用戶名、密碼(MySQL需要在數(shù)據(jù)庫地址中指定UTF-8編碼);
          2、更改數(shù)據(jù)庫驅(qū)動、Dialect,同時,需要自己下載MySQL的JDBC驅(qū)動放到項目中;
          3、SQL文件,因為Derby的語法和MySQL的有點不一樣,比如MySQL中就應(yīng)該使用AUTO_INCREMENT,而不是GENERATED ALWAYS as IDENTITY,并且Drop數(shù)據(jù)表的時候,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基類讓我們繼承,所以,以后只要是數(shù)據(jù)庫中含有id的表,編寫Entity類的時候都可以從IdEntity繼承。
          2、Entity中使用的Annotation就不用多說了,JPA Annotation已經(jīng)不是什么新東西,在上面的Entity中,我演示了一下@OneToMany,而白衣在項目里面大量演示了@ManyToMany,我以前寫的一篇博文《打通數(shù)據(jù)持久層的任督二脈》中討論了@OneToOne和@ManyToOne,這回算是補齊了。
          3、上面的Entity中涉及到了抓取策略和緩存策略,使用注解設(shè)置起來也很簡單。

          下一步,編寫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類更提供了分頁查詢函數(shù)。這個封裝的思路和前一代的SpringSide是一樣的,但是有幾個區(qū)別:
          1、可以不創(chuàng)建自己的DAO類,什么意思呢?舉例說明,上面為Article創(chuàng)建了ArticleDao類,那么在Action中可以這樣用:
          ArticleDao articleDao = new ArticleDao();(這只是一個示范,事實上不需要顯示創(chuàng)建,因為在SpringSide 3中,靠的都是注入)
          但是也可以不要ArticleDao,而直接這樣用:
          HibernateDao<Article,Long> articleDao = new HibernateDao<Article,Long>();
          這樣做有什么好處呢?當(dāng)然是可以有效減少Dao層類的數(shù)量,如果有的Dao類使用得比較少,那么就沒有必要專門為它創(chuàng)造一個Dao類了。

          寫到這里,我又忍不住要評論一下江南白衣在項目架構(gòu)方面的一些習(xí)慣了,他的層次太多,這應(yīng)該是他在實際項目中錘煉出來的經(jīng)驗,但是和教科書上的就不大一樣了,教科書上的三層就是三層,而白衣可以把它擴展到4層甚至5層,白衣的層次可以總結(jié)成Entity->DAO->Service(Manager)->Action->View,其中Service這一層命名還不統(tǒng)一,包名是Service,類名中用的是Manager。我覺得這個大家可以探討探討,也許白衣認為DAO里面不應(yīng)該包含業(yè)務(wù)邏輯,只應(yīng)該包含CRUD和分頁操作,而Action里面也不應(yīng)該包含業(yè)務(wù)邏輯,所以就單獨抽出一層來了吧,所以這一層應(yīng)該稱為Bussiness層比較合適,而白衣也認為,有時候DAO層和Bussiness層可以合并在一起。另外,我認為白衣在項目中搞的package也太多了一點,在IDE里面不方便,所以我的實際項目中,我會對包重新進行整理。

          2、在DAO類中可以使用Hibernate的原生API。我們來總結(jié)一下在Hibernate中通常采用的查詢方式:一是使用HQL語言,它的過程基本上是先獲取Session,然后創(chuàng)建Query對象,最后通過Query對象執(zhí)行HQL語句;二是使用條件查詢,它的過程基本上是先獲取Session,然后創(chuàng)建Creteria對象,然后執(zhí)行Creteria對象的list()方法。而在Dao類中,我們可以很簡單的通過sessionFactory.getCurrentSession()來獲得Session對象,進而很方便的使用到HQL或者Creteria。

          3、在SpringSide 2中,我們可以對數(shù)據(jù)表中的數(shù)據(jù)不做物理刪除,該特性得益于白衣提供的@Undeletable注解和HibernateEntityExtendDao類,在SpringSide 3中,該特性沒有了。現(xiàn)在回想起來,我覺得該特性也沒有什么存在的必要。

          后面再繼續(xù)探討分頁查詢和性能優(yōu)化。現(xiàn)在的任務(wù)是趕緊確認一下這Entity層和Dao層能否正常工作,完成該任務(wù)的最佳途徑,當(dāng)然是單元測試了。

          在SpringSide 3中,編寫單元測試非常方便,只需要繼承白衣提供的SpringContextTestCase類或者SpringTxTestCase類即可,事實上,只有繼承SpringTxTestCase類才能正常工作,因為我們的項目的配置無法讓我們工作在非事務(wù)的環(huán)境下。繼承這個類有什么用處呢?它的用處就是可以讀取項目中的applicationContext.xml文件,自動建立數(shù)據(jù)源、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);
          ????}
          }

          ?

          因為該單元測試工作在事務(wù)環(huán)境下,所以運行單元測試不會改變數(shù)據(jù)庫中的數(shù)據(jù)。白衣提供的這兩個類事實上只是在Spring 2.5的測試框架上做了一點點改進。關(guān)于Spring 2.5測試框架的詳細介紹,大家可以到“IBM DeveloperWorks 中國”上去看這一篇文章:
          http://www.ibm.com/developerworks/cn/java/j-lo-spring25-test/

          但是白衣自己的做法卻完全不同,在白衣寫的單元測試中,他偏偏用的是EasyMock,關(guān)于EasyMock的使用方法,大家可以到“IMB DeveloperWorks 中國”上去看這一篇文章:
          http://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/

          再讓大家看一下截圖,我特地把測試類的代碼、JUnit著名的綠條和Hibernate輸出的SQL語句放到了一起,如下:

          代碼比較簡單,只是為了證明上面寫的Entity和Dao能夠正常運行。在下面的內(nèi)容里,隨著我們的探討,測試代碼的內(nèi)容會逐漸增加。

          上文的內(nèi)容演示了SpringSide 3中Hibernate的使用過程和單元測試,也提到了SpringSide 3提供的CRUD封裝,這些都很簡單。在SpringSide 3對Hibernate的封裝中,還有一個重點,那就是分頁查詢。

          分頁查詢有HibernateDao類實現(xiàn),要配合Page類來使用。Page類一般用來設(shè)置查詢條件,并返回查詢結(jié)果,舉例說明,如果對Articles表中的數(shù)據(jù)進行分頁顯示,每一頁10條記錄,那么查詢第二頁應(yīng)該怎么辦呢?代碼如下:

          @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();
          ?}


          以上代碼在單元測試中進行,這個過程很容易理解,就是先創(chuàng)建一個Page對象,然后設(shè)置該頁的大小和序號,就可以直接查找該頁的數(shù)據(jù)了。同時,Page類還有很多輔助方法,如獲取總的記錄條數(shù),獲取頁的總數(shù),獲取是否有下一頁等等。

          Page只是一個輔助類,真正的查詢操作是在HibernateDao類中完成的,具體代碼如下:

          /**
          ??*?按Criteria分頁查詢.
          ??*?
          ??*?
          @param ?page?分頁參數(shù).
          ??*?
          @param ?criterions?數(shù)量可變的Criterion.
          ??*?
          ??*?
          @return ?分頁查詢結(jié)果.附帶結(jié)果列表及所有查詢時的參數(shù).
          ??
          */
          ?@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;
          ?}


          可以看到,白衣的實現(xiàn)用的是Hibernate中的條件查詢,從上面的代碼可以看出,該過程是先創(chuàng)建Criteria對象,然后查詢記錄的總數(shù),并將記錄的總數(shù)填入到Page對象中,然后再調(diào)用setPageParameter方法將Page對象中的信息填入到Criteria對象中,最后調(diào)用Criteria對象的list()方法來獲取結(jié)果。

          下面跟蹤到setPageParameter方法中,其代碼如下:

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


          可以看到,該方法中只是簡單地調(diào)用了Criteria對象的setFirstResult和setMaxResults方法,這都是Hibernate的原生API,沒有什么需要特殊說明的。我比較關(guān)心的是分頁查詢所生成的SQL語句及其正確性。

          講到這里,我得提一下我的技術(shù)背景:在使用MySQL之前,我有很長一段時間使用的是MS SQL Server 2000。為什么要提這個問題呢?那是因為站在SQL Server 2000的角度,處理分頁問題是比較困難的。在SQL Server 2000中,如果要獲取指定條數(shù)的記錄,只能使用top關(guān)鍵字,也就是說要獲取10條數(shù)據(jù),就應(yīng)該使用select * top 10 from articles,那么怎么定位到第二頁呢?就必須知道第二頁的第一條數(shù)據(jù)的ID是多少,然后用這樣的語句select * top 10 from articles where id >= ?,那怎么知道第二頁的第一條記錄的ID是多少呢?免不了又要多一次查詢?nèi)鐂elect id top 20 from articles order by id desc。

          所以在SQL Server 2000中,要實現(xiàn)分頁查詢比較困難,不是思考起來困難,而是提高效率困難,必須得避免多次查詢。解決的辦法當(dāng)然有,要么使用存儲過程,要么在前面的select語句中加入子查詢。但是不管采取哪種辦法,SQL語句寫起來都不簡單。

          在MySQL中,該問題就簡單多了,MySQL不提供top,但提供limit,更重要的是limit接受兩個參數(shù),而不是像top只接受一個參數(shù)。limit后面的參數(shù)可以是{[offset,] row_count | row_count OFFSET offset},其中的offset就代表了第2頁的第一條數(shù)據(jù)所在的位置,大家請注意,這里說的是位置,而不是SQL Server 2000中的ID,這兩者是有區(qū)別的,因為ID可能不連續(xù),而位置肯定是連續(xù)的,所以位置是可以通過簡單的數(shù)學(xué)計算來獲得的,這樣,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;

          因為我沒有把數(shù)據(jù)庫遷移到SQL Server,所以該語句一運行就出錯了,不過從該語句中的top 20也可以看出,要么該語句的作用是為了得到第二頁的第一條記錄的ID,然后后面再跟一條SQL語句,只不過因為出現(xiàn)錯誤,所以后面的語句沒有顯示出來,要么是直接取出20條記錄,并拋棄10條,只留下第二頁的數(shù)據(jù)。總之,和我之前預(yù)想的一樣,性能得不到保證。

          通過搜索引擎我還查出,Oracle也不支持limit語句,所以說,我們不能完全相信Hibernate,必要的時候,還是得靠自己寫存儲過程。

          Fetch策略也是影響性能的一個方面,F(xiàn)etch策略主要是針對Entity中的集合數(shù)據(jù),正如白衣所說,很多人多只知道使用默認的Lazy策略,我就是這很多人中的一個,以前我還因為Lazy策略出現(xiàn)過問題,什么問題呢,那就是我先獲取一個Entity的數(shù)據(jù),然后把在Entity保存到HttpSession中,然后在使用該對象中的集合數(shù)據(jù)時,就報錯了,為什么呢,因為這個時候Hibernate的Session早就關(guān)閉了,所以出錯。

          關(guān)于Fetch策略的選擇,SpringSide的文檔和Hibernate的文檔上面都寫得很清楚,我就不羅嗦了,至于在代碼中怎么設(shè)置Fetch策略,代碼的注釋很清楚,一看就會。

          最后談一談二級緩存,Session中的緩存是一級緩存,ehcache提供二級緩存,關(guān)于二級緩存的配置,主要涉及到兩個地方,一個是xml配置文件,另一個是Entity類中的注解,xml配置文件中配置的是ehcache的屬性,而Entity中的注解設(shè)置了隔離級別,具體內(nèi)容請參閱SpringSide 3 的文檔。


          評論

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

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

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層[未登錄]  回復(fù)  更多評論   

          2009-07-10 22:50 by 過客
          使用Oracle,hibernate會通過rownum來分頁的。

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

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

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

          2009-07-12 17:03 by dd2086
          好文吶 關(guān)注你的文章

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

          2010-04-09 16:57 by 游客
          ,,太經(jīng)典了,,,太感謝了,,

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

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

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層  回復(fù)  更多評論   

          2012-01-29 13:44 by qinjiannet
          經(jīng)典,受教了。

          # re: SpringSide 3 中的數(shù)據(jù)庫訪問層[未登錄]  回復(fù)  更多評論   

          2012-05-14 16:08 by 過客
          感謝大俠
          主站蜘蛛池模板: 普格县| 和林格尔县| 武山县| 兰考县| 崇阳县| 青阳县| 湘潭市| 桦南县| 凤庆县| 三台县| 洛南县| 乐清市| 铜川市| 屯留县| 尤溪县| 云霄县| 铜陵市| 长沙市| 通江县| 红河县| 思南县| 安化县| 淳安县| 禹城市| 阳泉市| 中宁县| 电白县| 平乐县| 东台市| 木里| 永新县| 兴海县| 阳信县| 洛川县| 镇江市| 水城县| 隆林| 池州市| 泰宁县| 蕲春县| 永川市|