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"时发现了一些问题,如果你只是希望使用MySQL,推荐使用"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层的联系。