Part I:
在AppFuse建立DAO和对象 - 一個(gè)建立對(duì)象(代表數(shù)據(jù)庫(kù)的表)和把這些對(duì)象存儲(chǔ)到數(shù)據(jù)庫(kù)的Java類的教程。
關(guān)于本教程
本教程將向你展示如何在一個(gè)數(shù)據(jù)庫(kù)里創(chuàng)建表,以及如何完成訪問(wèn)這些表的Java代碼。
我們將建立一個(gè)對(duì)象以及處理(保存/檢索/刪除)這些類到數(shù)據(jù)庫(kù)的一些代碼。用Java術(shù)語(yǔ),我們叫它Plain Old Java Object(a.k.a. a POJO
)。這個(gè)對(duì)象通常代表了數(shù)據(jù)庫(kù)中的一個(gè)表,其他的類包括:
- 一個(gè)數(shù)據(jù)訪問(wèn)對(duì)象Data Access Object (a.k.a. a DAO
), 一個(gè) Interface
和一個(gè)Hibernate實(shí)現(xiàn)
- 一個(gè) JUnit
類來(lái)測(cè)試我們的DAO對(duì)象
NOTE: 如果你使用MySQL并且希望使用事務(wù) (很有可能是這個(gè)情況),你需要使用InnoDB tables,為了做到這一點(diǎn), 添加以下兩句話到 (/etc/my.cnf 或者 c:\Windows\my.ini)。 其中第二個(gè)設(shè)置 (設(shè)置使用UTF-8字符)是4.1.7+所必需的。 [mysqld]
default-table-type=innodb
default-character-set=utf8
如果你使用PostgreSQL并且在成批處理時(shí)得到許多迷惑的錯(cuò)誤,試著把關(guān)閉它,方法是增加 <prop key="hibernate.jdbc.batch_size">0</prop> 到你的 src/dao/**/hibernate/applicationContext-hibernate.xml文件。 AppFuse使用Hibernate
作為持久化層, Hibernate是一套對(duì)象/關(guān)系Object/Relational (O/R)框架,他允許你把Java對(duì)象和數(shù)據(jù)庫(kù)之間聯(lián)系起來(lái),它可以很方便的對(duì)你的對(duì)象執(zhí)行CRUD (Create, Retrieve, Update, Delete)操作。
- 你也可以選擇使用iBATIS
作為持久化層,如果要在AppFuse里安裝iBATIS, 請(qǐng)查看extras/ibatis中的README.txt。如果你選擇iBATIS而不是Hibernate, 希望你有自己的原因并且熟悉這個(gè)框架,我也希望你能夠領(lǐng)會(huì)到如何將教程應(yīng)用到iBATIS ;-)
字體慣例 (進(jìn)行中)
- 要在命令行下執(zhí)行的命令是這個(gè)樣子: ant test-all.
- 對(duì)目錄或者包中的文件的引用是這個(gè)樣子: build.xml.
- 我在?真實(shí)世界?中實(shí)際操作的方式用藍(lán)色斜體表示。
讓我們繼續(xù)在AppFuse項(xiàng)目的結(jié)構(gòu)下創(chuàng)建一個(gè)新的對(duì)象、DAO和測(cè)試。
目錄
- 建立一個(gè)對(duì)象,并且作XDoclet
標(biāo)記
- 使用Ant根據(jù)對(duì)象建立數(shù)據(jù)庫(kù)中的表
- 創(chuàng)建一個(gè)DaoTest來(lái)運(yùn)行DAO對(duì)象的JUnit測(cè)試
- 創(chuàng)建一個(gè)新的DAO來(lái)執(zhí)行關(guān)于這個(gè)對(duì)象的CRUD操作
- 在spring里配置Person和PersonDao
- 運(yùn)行DaoTest
建立一個(gè)對(duì)象,并且作XDoclet標(biāo)記
我們要做的第一件事情就是建立一個(gè)需要持久化的對(duì)象,我們要在src/dao/**/model目錄下建立一個(gè)簡(jiǎn)單的Person對(duì)象,這個(gè)對(duì)象包括id、firstName和lastName屬性。
注意: 直接拷貝本教程的代碼 在FireFox下无效
,但我們可以通過(guò)CTRL+Click選定一個(gè)代碼所在的工作區(qū)(OS X下是Command+Click),然后再拷貝。 package?org.appfuse.model;
public?class?Person?extends?BaseObject?{ ????private?Long?id; ????private?String?firstName; ????private?String?lastName;
????/* ?????Generate?your?getters?and?setters?using?your?favorite?IDE:? ?????In?Eclipse: ?????Right-click?->?Source?->?Generate?Getters?and?Setters ????*/ }
|
這個(gè)類必須擴(kuò)展BaseObject
,而這個(gè)BaseObject有三個(gè)抽象方法(equals(), hashCode()和toString())需要你在Person類里實(shí)現(xiàn),前兩個(gè)是Hibernate的需要。為了完成這部分工作最簡(jiǎn)單的方式是使用Commonclipse
,關(guān)于這個(gè)工具更多的信息可以在Lee Grey的网站
里看到,另外一個(gè)你可以使用的Eclipse的插件是Commons4E
,我還沒(méi)有使用過(guò),這里不便對(duì)其功能作出評(píng)論。
- 如果你使用IntelliJ IDEA
,你可以自動(dòng)產(chǎn)生equals()和hashCode(),但沒(méi)有toString(),有一個(gè) ToStringPlugin
插件做得非常不錯(cuò)
現(xiàn)在我們已經(jīng)創(chuàng)建了這個(gè)POJO對(duì)象,我們需要增加X(jué)Doclet標(biāo)記來(lái)產(chǎn)生Hibernate的映射文件,這些文件用來(lái)映射對(duì)象→ 表和屬性(變量) → 字段。
首先,我們?cè)黾?a class="external" >@hibernate.class
來(lái)告訴Hibernate我們將要和那個(gè)表作關(guān)聯(lián):
/** ?*?@hibernate.class?table="person" ?*/ public?class?Person?extends?BaseObject?{
|
我們也要增加主鍵的映射,否則XDoclet會(huì)在產(chǎn)生映射文件時(shí)出錯(cuò),注意所有的@hibernate.*標(biāo)簽必須在getters'的Javadocs里面。
????/** ?????*?@return?Returns?the?id. ?????*?@hibernate.id?column="id" ?????*??generator-class="increment"?unsaved-value="null" ?????*/
????public?Long?getId()?{ ????????return?this.id; ????}
|
- 我使用generator-class="increment"而不使用generate-class="native" 是因?yàn)槲覍?duì)數(shù)據(jù)庫(kù)使用"native"時(shí),本教程使用increment。
使用Ant根據(jù)對(duì)象產(chǎn)生數(shù)據(jù)庫(kù)表
在這種情況下,你可以通過(guò)運(yùn)行
ant setup-db來(lái)建立person表,這個(gè)任務(wù)會(huì)產(chǎn)生文件
Person.hbm.xml并且會(huì)建立叫做"person"的表,從Ant的控制臺(tái)窗口,你可以看到Hibernate為你建立的表結(jié)構(gòu)的內(nèi)容。
[schemaexport] create table person (
[schemaexport] id bigint not null,
[schemaexport] primary key (id)
[schemaexport] );
如果你查看Hibernate生成的文件Person.hbm.xml,可以到build/dao/gen/**/model目錄,這里是Person.hbm.xml的內(nèi)容(目前的內(nèi)容):
<?xml?version="1.0"?>
<!DOCTYPE?hibernate-mapping?PUBLIC ????"-//Hibernate/Hibernate?Mapping?DTD?2.0//EN"? ????"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
<hibernate-mapping> ????<class ????????name="org.appfuse.model.Person" ????????table="person" ????????dynamic-update="false" ????????dynamic-insert="false" ????>
????????<id ????????????name="id" ????????????column="id" ????????????type="java.lang.Long" ????????????unsaved-value="null" ????????> ????????????<generator?class="increment"> ????????????</generator> ????????</id>
????????<!-- ????????????To?add?non?XDoclet?property?mappings,?create?a?file?named ????????????????hibernate-properties-Person.xml ????????????containing?the?additional?properties?and?place?it?in?your?merge?dir. ????????-->
????</class>
</hibernate-mapping>
|
現(xiàn)在我們要為其它的字段(first_name, last_name)添加額外的@hibernate.property
標(biāo)簽:
????/** ?????*?@hibernate.property?column="first_name"?length="50" ?????*/ ????public?String?getFirstName()?{ ????????return?this.firstName; ????}
????/** ?????*?@hibernate.property?column="last_name"?length="50" ?????*/ ????public?String?getLastName()?{ ????????return?this.lastName; ????}
|
在這個(gè)例子里,添加column屬性的唯一原因是因?yàn)檫@個(gè)字段名與它的屬性名不相同,如果他們相同,你沒(méi)有必要來(lái)指定column屬性,關(guān)于其它可以使用的標(biāo)簽請(qǐng)看@hibernate.property
。
再次運(yùn)行ant setup-db把新加的屬性加到數(shù)據(jù)庫(kù)表里。
[schemaexport] create table person (
[schemaexport] id bigint not null,
[schemaexport] first_name varchar(50),
[schemaexport] last_name varchar(50),
[schemaexport] primary key (id)
[schemaexport] );
如果期望修改字段的長(zhǎng)度,修改@hibernate.property標(biāo)簽的length屬性,如果希望把字段改為必添字段(NOT NULL),可以增加屬性not-null="true"。
建立新的DaoTest來(lái)對(duì)你的DAO運(yùn)行JUnit測(cè)試
注意:從Appfuse版本1.6.1+開(kāi)始包括了一個(gè)AppGen工具,可以用來(lái)生成本教程余下的所有的類的代碼,不過(guò),我們最好還是先過(guò)一遍教程再使用這個(gè)工具產(chǎn)生代碼。現(xiàn)在,我們要?jiǎng)?chuàng)建一個(gè)DaoTest來(lái)測(cè)試我們的DAO的工作,?等會(huì)兒?,你說(shuō),?我們還不曾創(chuàng)建DAO呢!?,你說(shuō)得對(duì)。無(wú)論如何,我發(fā)現(xiàn)测试驱动开发
大大的促進(jìn)了軟件質(zhì)量,在許多年里我一直認(rèn)為在寫(xiě)代碼之前寫(xiě)測(cè)試是胡說(shuō)八道,這看起來(lái)很愚蠢,但當(dāng)我嘗試之后我認(rèn)為這樣非常好,現(xiàn)在我按照測(cè)試驅(qū)動(dòng)的方式工作完全因?yàn)槲野l(fā)現(xiàn)這樣可以大大提高我軟件開(kāi)發(fā)的效率。
開(kāi)始,我們?cè)?tt>test/dao/**/dao目錄下建立類PersonDaoTest.java,這個(gè)類必須擴(kuò)展BaseDaoTestCase
,而B(niǎo)aseDAOTestCase這個(gè)類是JUnit類TestCase
的子類,這個(gè)類用來(lái)加載Spring
的ApplicationContext(因?yàn)镾pring把各個(gè)層綁定)和單元測(cè)試類同一目錄下同你的測(cè)試類文件同名的.properties文件(ResourceBundle),這個(gè)屬性文件的屬性可以通過(guò)?rb?屬性來(lái)訪問(wèn)。
- 我經(jīng)常拷貝(打開(kāi)→另存為)一個(gè)已存在的測(cè)試(如UserDaoTest.java),然后查找/替換 [Uu]ser為[Pp]erson,或者任何其它需要替換的內(nèi)容。
package?org.appfuse.dao;
import?org.appfuse.model.Person; import?org.springframework.dao.DataAccessException;
public?class?PersonDaoTest?extends?BaseDaoTestCase?{ ???? ????private?Person?person?=?null; ????private?PersonDao?dao?=?null;
????public?void?setPersonDao(PersonDao?dao)?{ ????????this.dao?=?dao; ????} }
|
以上是我們使用JUnit測(cè)試而初始化和銷毀PersonDao的基本代碼,對(duì)象?ctx?引用了Spring的ApplicationContext,它在BaseDaoTestCase
類的靜態(tài)代碼區(qū)里被初始化。
現(xiàn)在我們需要實(shí)際測(cè)試DAO中的CRUD(create, retrieve, update, delete)方法,為此我們需要為每個(gè)方法建立以test(全部小寫(xiě))開(kāi)頭的測(cè)試方法,只要這個(gè)方法是公共的,返回類型是void,它們就會(huì)被我們build.xml中的Ant的<junit>任務(wù)調(diào)用,如下是一些簡(jiǎn)單的CRUD測(cè)試,需要注意的一點(diǎn)是所有的方法(或者叫做測(cè)試)必須是自治的,添加如下代碼到文件PersonDaoTest.java:
????public?void?testGetPerson()?throws?Exception?{ ????????person?=?new?Person(); ????????person.setFirstName("Matt"); ????????person.setLastName("Raible");
????????dao.savePerson(person); ????????assertNotNull(person.getId());
????????person?=?dao.getPerson(person.getId()); ????????assertEquals(person.getFirstName(),?"Matt"); ????}
????public?void?testSavePerson()?throws?Exception?{ ????????person?=?dao.getPerson(new?Long(1)); ????????person.setFirstName("Matt");
????????person.setLastName("Last?Name?Updated");
????????dao.savePerson(person);
????????if?(log.isDebugEnabled())?{ ????????????log.debug("updated?Person:?"?+?person); ????????}
????????assertEquals(person.getLastName(),?"Last?Name?Updated"); ????}
????public?void?testAddAndRemovePerson()?throws?Exception?{ ????????person?=?new?Person(); ????????person.setFirstName("Bill"); ????????person.setLastName("Joy");
????????dao.savePerson(person);
????????assertEquals(person.getFirstName(),?"Bill"); ????????assertNotNull(person.getId());
????????if?(log.isDebugEnabled())?{ ????????????log.debug("removing?person..."); ????????}
????????dao.removePerson(person.getId());
????????try?{ ????????????person?=?dao.getPerson(person.getId()); ????????????fail("Person?found?in?database"); ????????}?catch?(DataAccessException?dae)?{ ????????????log.debug("Expected?exception:?"?+?dae.getMessage()); ????????????assertNotNull(dae); ????????} ????}
|
- 在testGetPerson方法,我們創(chuàng)建了一個(gè)person并且調(diào)用get方法,我通常會(huì)增加一條我所需要的記錄到數(shù)據(jù)庫(kù),因?yàn)樵跍y(cè)試運(yùn)行之前DBUnit
會(huì)為數(shù)據(jù)庫(kù)準(zhǔn)備測(cè)試數(shù)據(jù),我們可以簡(jiǎn)單的在metadata/sql/sample-data.xml里添加測(cè)試所必須的記錄
<table name='person'>
<column>id</column>
<column>first_name</column>
<column>last_name</column>
<row>
<value>1</value>
<value>Matt</value>
<value>Raible</value>
</row>
</table>
- 通過(guò)這種方式你可以在testGetPerson方法里消除創(chuàng)建新紀(jì)錄的動(dòng)作,如果你愿意直接插入記錄到數(shù)據(jù)庫(kù)(使用SQL或者GUI),你可以用ant db-export和cp db-export.xml metadata/sql/sample-data.xml重新構(gòu)建你的sample-data.xml文件。
在上面的例子里,你可以看到我們調(diào)用person.set*(value)來(lái)準(zhǔn)備我們需要保存的對(duì)象,在這個(gè)例子里很簡(jiǎn)單,但是當(dāng)你要插入10條必添字段(not-null="true")時(shí)就比較麻煩了,這就是我為什么要在BaseDaoTestCase使用ResourceBundle文件,只要在PersonDaoTest.java同一個(gè)目錄創(chuàng)建一個(gè)PersonDaoTest.properties并且在里面定義你的屬性值:
- 我通常只是在Java里硬編碼,但是這個(gè).properties對(duì)于大對(duì)象很有用。
firstName=Matt
lastName=Raible
此時(shí),你要通過(guò)調(diào)用BaseDaoTestCase.populate(java.lang.Object)方法來(lái)準(zhǔn)備對(duì)象,而不是使用person.set*。
person?=?new?Person(); person?=?(Person)?populate(person);
|
在目前情況下,還不可以編譯PersonDaoTest,因?yàn)樵陬惵窂嚼镞€沒(méi)有PersonDao.class,我們需要?jiǎng)?chuàng)建它。PersonDao.java是一個(gè)接口,PersonDaoHibernate.java是它的Hibernate實(shí)現(xiàn),讓我們繼續(xù),開(kāi)始創(chuàng)建。
創(chuàng)建一個(gè)對(duì)對(duì)象執(zhí)行CRUD操作的新DAO
馬上,在
src/dao/**/dao目錄里建立PersonDao.java接口,并且指定所有實(shí)現(xiàn)類要實(shí)現(xiàn)的基本CRUD操作,為了顯示方便,我已經(jīng)去掉了所有JavaDocs。
package?org.appfuse.dao;
import?org.appfuse.model.Person;
public?interface?PersonDao?extends?Dao?{ ????public?Person?getPerson(Long?personId); ????public?void?savePerson(Person?person); ????public?void?removePerson(Long?personId); }
|
注意,在以上的方法聲明上并沒(méi)有exceptions說(shuō)明,這是因?yàn)?a class="external" >Spring
使用RuntimeExceptions來(lái)包裹Exceptions的方式,此時(shí),你已經(jīng)可以使用ant compile-dao來(lái)編譯src/dao和test/dao下的所有源文件,然而當(dāng)你運(yùn)行ant test-dao -Dtestcase=PersonDao進(jìn)行測(cè)試時(shí),你會(huì)得到一個(gè)錯(cuò)誤:No bean named 'personDao' is defined,這是一個(gè)Spring的錯(cuò)誤,說(shuō)明你必須在applicationContext-hibernate.xml指定一個(gè)名字為personDAO的bean,在此之前我們需要?jiǎng)?chuàng)建PersonDao的實(shí)現(xiàn)類。
- 運(yùn)行dao測(cè)試的ant任務(wù)叫做test-dao,如果你傳遞testcase參數(shù)(用-Dtestcase=name),它會(huì)查看**/*${testcase}*允許我們傳遞Person、PersonDao、或者PersonDaoTest以及所有會(huì)執(zhí)行PersonDaoTest的類。
讓我們創(chuàng)建一個(gè)實(shí)現(xiàn)PersonDao的類PersonDaoHibernate并使用Hibernate來(lái)get/save/delete這個(gè)Person對(duì)象,為此,我們?cè)?tt>src/dao/**/dao/hibernate創(chuàng)建一個(gè)新類PersonDaoHibernate.java,它應(yīng)該擴(kuò)展BaseDaoHibernate
,并且實(shí)現(xiàn)PersonDao。為了簡(jiǎn)潔,省略Javadocs。
package?org.appfuse.dao.hibernate;
import?org.appfuse.model.Person; import?org.appfuse.dao.PersonDao; import?org.springframework.orm.ObjectRetrievalFailureException;
public?class?PersonDaoHibernate?extends?BaseDaoHibernate?implements?PersonDao?{
????public?Person?getPerson(Long?id)?{ ????????Person?person?=?(Person)?getHibernateTemplate().get(Person.class,?id);
????????if?(person?==?null)?{ ????????????throw?new?ObjectRetrievalFailureException(Person.class,?id);??? ????????}
????????return?person; ????}
????public?void?savePerson(Person?person)?{ ????????getHibernateTemplate().saveOrUpdate(person); ????}
????public?void?removePerson(Long?id)?{ ????????//?object?must?be?loaded?before?it?can?be?deleted ????????getHibernateTemplate().delete(getPerson(id)); ????} }
|
現(xiàn)在,如果你運(yùn)行ant test-dao -Dtestcase=PersonDao,你會(huì)得到同樣的錯(cuò)誤,我們必須配置Spring來(lái)讓它知道PersonDaoHibernate是PersonDao的實(shí)現(xiàn),同樣的,我們也要告訴它還有個(gè)Person對(duì)象。
配置Spring中的Person和PersonDao
首先我們要告訴Spring所有Hibernate文件的位置,為此,打開(kāi)src/dao/**/dao/hibernate/applicationContext-hibernate.xml,在以下代碼塊添加"Person.hbm.xml"。
<property?name="mappingResources">? ????<list>? ????????<value>org/appfuse/model/Person.hbm.xml</value>? ????????<value>org/appfuse/model/Role.hbm.xml</value>? ????????<value>org/appfuse/model/User.hbm.xml</value> ????</list>? </property>?
|
現(xiàn)在我們需要添加一些XML數(shù)據(jù)來(lái)綁定PersonDaoHibernate到PersonDao,為此,添加如下代碼到文件底部:
<!--?PersonDao:?Hibernate?implementation?-->? <bean?id="personDao"?class="org.appfuse.dao.hibernate.PersonDaoHibernate">? ????<property?name="sessionFactory"><ref?local="sessionFactory"/></property>? </bean>?
|
- 你也可以為<bean>使用autowire="byName"屬性來(lái)消除"sessionFactory"屬性。從個(gè)人來(lái)講,我喜歡在XML文件里保留對(duì)象的依賴。
運(yùn)行DaoTest
保存所有修改的文件,運(yùn)行
ant test-dao -Dtestcase=PersonDao。
Yeah Baby, Yeah:BUILD SUCCESSFUL
Total time: 9 seconds
下一部分:Part II:创建管理器Manager - 是一個(gè)建立類似于Session Facades
的,但不使用EJBs的業(yè)務(wù)Facade說(shuō)明,這個(gè)facades用來(lái)建立從前端到DAO層的聯(lián)系。