Part I:
在AppFuse建立DAO和对象 - 一個建立對象(代表數據庫的表)和把這些對象存儲到數據庫的Java類的教程。
關于本教程
本教程將向你展示如何在一個數據庫里創建表,以及如何完成訪問這些表的Java代碼。
我們將建立一個對象以及處理(保存/檢索/刪除)這些類到數據庫的一些代碼。用Java術語,我們叫它Plain Old Java Object(a.k.a. a POJO
)。這個對象通常代表了數據庫中的一個表,其他的類包括:
- 一個數據訪問對象Data Access Object (a.k.a. a DAO
), 一個 Interface
和一個Hibernate實現
- 一個 JUnit
類來測試我們的DAO對象
NOTE: 如果你使用MySQL并且希望使用事務 (很有可能是這個情況),你需要使用InnoDB tables,為了做到這一點, 添加以下兩句話到 (/etc/my.cnf 或者 c:\Windows\my.ini)。 其中第二個設置 (設置使用UTF-8字符)是4.1.7+所必需的。 [mysqld]
default-table-type=innodb
default-character-set=utf8
如果你使用PostgreSQL并且在成批處理時得到許多迷惑的錯誤,試著把關閉它,方法是增加 <prop key="hibernate.jdbc.batch_size">0</prop> 到你的 src/dao/**/hibernate/applicationContext-hibernate.xml文件。 AppFuse使用Hibernate
作為持久化層, Hibernate是一套對象/關系Object/Relational (O/R)框架,他允許你把Java對象和數據庫之間聯系起來,它可以很方便的對你的對象執行CRUD (Create, Retrieve, Update, Delete)操作。
- 你也可以選擇使用iBATIS
作為持久化層,如果要在AppFuse里安裝iBATIS, 請查看extras/ibatis中的README.txt。如果你選擇iBATIS而不是Hibernate, 希望你有自己的原因并且熟悉這個框架,我也希望你能夠領會到如何將教程應用到iBATIS ;-)
字體慣例 (進行中)
- 要在命令行下執行的命令是這個樣子: ant test-all.
- 對目錄或者包中的文件的引用是這個樣子: build.xml.
- 我在?真實世界?中實際操作的方式用藍色斜體表示。
讓我們繼續在AppFuse項目的結構下創建一個新的對象、DAO和測試。
目錄
- 建立一個對象,并且作XDoclet
標記
- 使用Ant根據對象建立數據庫中的表
- 創建一個DaoTest來運行DAO對象的JUnit測試
- 創建一個新的DAO來執行關于這個對象的CRUD操作
- 在spring里配置Person和PersonDao
- 運行DaoTest
建立一個對象,并且作XDoclet標記
我們要做的第一件事情就是建立一個需要持久化的對象,我們要在src/dao/**/model目錄下建立一個簡單的Person對象,這個對象包括id、firstName和lastName屬性。
注意: 直接拷貝本教程的代碼 在FireFox下无效
,但我們可以通過CTRL+Click選定一個代碼所在的工作區(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 ????*/ }
|
這個類必須擴展BaseObject
,而這個BaseObject有三個抽象方法(equals(), hashCode()和toString())需要你在Person類里實現,前兩個是Hibernate的需要。為了完成這部分工作最簡單的方式是使用Commonclipse
,關于這個工具更多的信息可以在Lee Grey的网站
里看到,另外一個你可以使用的Eclipse的插件是Commons4E
,我還沒有使用過,這里不便對其功能作出評論。
- 如果你使用IntelliJ IDEA
,你可以自動產生equals()和hashCode(),但沒有toString(),有一個 ToStringPlugin
插件做得非常不錯
現在我們已經創建了這個POJO對象,我們需要增加XDoclet標記來產生Hibernate的映射文件,這些文件用來映射對象→ 表和屬性(變量) → 字段。
首先,我們增加@hibernate.class
來告訴Hibernate我們將要和那個表作關聯:
/** ?*?@hibernate.class?table="person" ?*/ public?class?Person?extends?BaseObject?{
|
我們也要增加主鍵的映射,否則XDoclet會在產生映射文件時出錯,注意所有的@hibernate.*標簽必須在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" 是因為我對數據庫使用"native"時,本教程使用increment。
使用Ant根據對象產生數據庫表
在這種情況下,你可以通過運行
ant setup-db來建立person表,這個任務會產生文件
Person.hbm.xml并且會建立叫做"person"的表,從Ant的控制臺窗口,你可以看到Hibernate為你建立的表結構的內容。
[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的內容(目前的內容):
<?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>
|
現在我們要為其它的字段(first_name, last_name)添加額外的@hibernate.property
標簽:
????/** ?????*?@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; ????}
|
在這個例子里,添加column屬性的唯一原因是因為這個字段名與它的屬性名不相同,如果他們相同,你沒有必要來指定column屬性,關于其它可以使用的標簽請看@hibernate.property
。
再次運行ant setup-db把新加的屬性加到數據庫表里。
[schemaexport] create table person (
[schemaexport] id bigint not null,
[schemaexport] first_name varchar(50),
[schemaexport] last_name varchar(50),
[schemaexport] primary key (id)
[schemaexport] );
如果期望修改字段的長度,修改@hibernate.property標簽的length屬性,如果希望把字段改為必添字段(NOT NULL),可以增加屬性not-null="true"。
建立新的DaoTest來對你的DAO運行JUnit測試
注意:從Appfuse版本1.6.1+開始包括了一個AppGen工具,可以用來生成本教程余下的所有的類的代碼,不過,我們最好還是先過一遍教程再使用這個工具產生代碼。現在,我們要創建一個DaoTest來測試我們的DAO的工作,?等會兒?,你說,?我們還不曾創建DAO呢!?,你說得對。無論如何,我發現测试驱动开发
大大的促進了軟件質量,在許多年里我一直認為在寫代碼之前寫測試是胡說八道,這看起來很愚蠢,但當我嘗試之后我認為這樣非常好,現在我按照測試驅動的方式工作完全因為我發現這樣可以大大提高我軟件開發的效率。
開始,我們在test/dao/**/dao目錄下建立類PersonDaoTest.java,這個類必須擴展BaseDaoTestCase
,而BaseDAOTestCase這個類是JUnit類TestCase
的子類,這個類用來加載Spring
的ApplicationContext(因為Spring把各個層綁定)和單元測試類同一目錄下同你的測試類文件同名的.properties文件(ResourceBundle),這個屬性文件的屬性可以通過?rb?屬性來訪問。
- 我經常拷貝(打開→另存為)一個已存在的測試(如UserDaoTest.java),然后查找/替換 [Uu]ser為[Pp]erson,或者任何其它需要替換的內容。
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測試而初始化和銷毀PersonDao的基本代碼,對象?ctx?引用了Spring的ApplicationContext,它在BaseDaoTestCase
類的靜態代碼區里被初始化。
現在我們需要實際測試DAO中的CRUD(create, retrieve, update, delete)方法,為此我們需要為每個方法建立以test(全部小寫)開頭的測試方法,只要這個方法是公共的,返回類型是void,它們就會被我們build.xml中的Ant的<junit>任務調用,如下是一些簡單的CRUD測試,需要注意的一點是所有的方法(或者叫做測試)必須是自治的,添加如下代碼到文件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方法,我們創建了一個person并且調用get方法,我通常會增加一條我所需要的記錄到數據庫,因為在測試運行之前DBUnit
會為數據庫準備測試數據,我們可以簡單的在metadata/sql/sample-data.xml里添加測試所必須的記錄
<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>
- 通過這種方式你可以在testGetPerson方法里消除創建新紀錄的動作,如果你愿意直接插入記錄到數據庫(使用SQL或者GUI),你可以用ant db-export和cp db-export.xml metadata/sql/sample-data.xml重新構建你的sample-data.xml文件。
在上面的例子里,你可以看到我們調用person.set*(value)來準備我們需要保存的對象,在這個例子里很簡單,但是當你要插入10條必添字段(not-null="true")時就比較麻煩了,這就是我為什么要在BaseDaoTestCase使用ResourceBundle文件,只要在PersonDaoTest.java同一個目錄創建一個PersonDaoTest.properties并且在里面定義你的屬性值:
- 我通常只是在Java里硬編碼,但是這個.properties對于大對象很有用。
firstName=Matt
lastName=Raible
此時,你要通過調用BaseDaoTestCase.populate(java.lang.Object)方法來準備對象,而不是使用person.set*。
person?=?new?Person(); person?=?(Person)?populate(person);
|
在目前情況下,還不可以編譯PersonDaoTest,因為在類路徑里還沒有PersonDao.class,我們需要創建它。PersonDao.java是一個接口,PersonDaoHibernate.java是它的Hibernate實現,讓我們繼續,開始創建。
創建一個對對象執行CRUD操作的新DAO
馬上,在
src/dao/**/dao目錄里建立PersonDao.java接口,并且指定所有實現類要實現的基本CRUD操作,為了顯示方便,我已經去掉了所有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); }
|
注意,在以上的方法聲明上并沒有exceptions說明,這是因為Spring
使用RuntimeExceptions來包裹Exceptions的方式,此時,你已經可以使用ant compile-dao來編譯src/dao和test/dao下的所有源文件,然而當你運行ant test-dao -Dtestcase=PersonDao進行測試時,你會得到一個錯誤:No bean named 'personDao' is defined,這是一個Spring的錯誤,說明你必須在applicationContext-hibernate.xml指定一個名字為personDAO的bean,在此之前我們需要創建PersonDao的實現類。
- 運行dao測試的ant任務叫做test-dao,如果你傳遞testcase參數(用-Dtestcase=name),它會查看**/*${testcase}*允許我們傳遞Person、PersonDao、或者PersonDaoTest以及所有會執行PersonDaoTest的類。
讓我們創建一個實現PersonDao的類PersonDaoHibernate并使用Hibernate來get/save/delete這個Person對象,為此,我們在src/dao/**/dao/hibernate創建一個新類PersonDaoHibernate.java,它應該擴展BaseDaoHibernate
,并且實現PersonDao。為了簡潔,省略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)); ????} }
|
現在,如果你運行ant test-dao -Dtestcase=PersonDao,你會得到同樣的錯誤,我們必須配置Spring來讓它知道PersonDaoHibernate是PersonDao的實現,同樣的,我們也要告訴它還有個Person對象。
配置Spring中的Person和PersonDao
首先我們要告訴Spring所有Hibernate文件的位置,為此,打開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>?
|
現在我們需要添加一些XML數據來綁定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"屬性來消除"sessionFactory"屬性。從個人來講,我喜歡在XML文件里保留對象的依賴。
運行DaoTest
保存所有修改的文件,運行
ant test-dao -Dtestcase=PersonDao。
Yeah Baby, Yeah:BUILD SUCCESSFUL
Total time: 9 seconds
下一部分:Part II:创建管理器Manager - 是一個建立類似于Session Facades
的,但不使用EJBs的業務Facade說明,這個facades用來建立從前端到DAO層的聯系。