Expresso持久层的深入使用研究
修订历史记录
日期 | 版本 | 说明 | 作者 |
2005-08-02 | 1.0 | 初建 | 唐家平 |
|
|
|
|
|
|
|
|
1 前言
Expresso为我们提供了一个优秀的持久层实现,使得我们在操作数据库时觉得相当的便捷。本文将就怎样使用Expresso持久层做一些探讨,希望它对我们正确、全面使用Expresso起到一些指导作用。
使用持久层主要包括以下內容:
1. 连接池的配置(与数据库相关,包含驱动配置信息)
2. 直接使用连接池(以下为与应用相关);
3. 使用DBObject;
4. 使用缓冲;
5. 数据安全等。
2 连接池的配置
Expresso提供了一个多数据库连接池并发的功能机制,这极大程度地改进了连接池性能表现。当然,通常情况下,我们只需要使用一个连接池即可,多连接池的功能可以让我们平滑地将Expresso系统表以及我们定义的表转移到我们喜欢的数据库里;也可以让我们把应用程序的数据同时保存在不同的数据库里,实现数据库的分布式系统。这正是Expresso持久层对“与数据库无关”理论实现后的一大杰出成果。
通常配置如下(expresso-config.xml):
<context name="default">
<description>Hypersonic Database</description>
<hasSetupTables>true</hasSetupTables>
<jdbc driver="org.hsqldb.jdbcDriver"
url="jdbc:hsqldb:%web-app%WEB-INF/db/default/default"
connectFormat="3"
login="sa"
password=""
cache="y"
createTableIndicies="true"
limitationPosition=""
escapeHandler="com.jcorporate.expresso.core.db.DoubleQuoteEscapeHandler"/>
<type-mapping>
<java-type>LONGVARCHAR</java-type>
<db-type>LONGVARCHAR</db-type>
</type-mapping>
<images>%context%/%expresso-dir%/images</images>
<startJobHandler>y</startJobHandler>
<showStackTrace>y</showStackTrace>
<mailDebug>n</mailDebug>
</context>
<context name="mysql">
<description>MySQL Database</description>
<hasSetupTables>false</hasSetupTables>
<jdbc driver="org.gjt.mm.mysql.Driver"
url="jdbc:mysql://localhost/expresso"
connectFormat="1"
login="expresso"
password="expresso"
cache="true"
limitationPosition="LIMITATION_AFTER_ORDER_BY"
limitationSyntax="LIMIT %offset% , %maxrecords%"
createTableIndicies="true"
hasBooleanType="false"
skipText="true"/>
<type-mapping>
<java-type>LONGVARCHAR</java-type>
<expresso-type>text</expresso-type>
<db-type>text</db-type>
</type-mapping>
<type-mapping>
<java-type>BIT</java-type>
<db-type>CHAR</db-type>
</type-mapping>
<type-mapping>
<java-type>REAL</java-type>
<db-type>DECIMAL</db-type>
</type-mapping>
<type-mapping>
<java-type>BINARY</java-type>
<db-type>LONGBLOB</db-type>
</type-mapping>
<type-mapping>
<java-type>VARCHAR</java-type>
<db-type>varchar</db-type>
</type-mapping>
<images>%context%/%expresso-dir%/images</images>
<startJobHandler>n</startJobHandler>
<showStackTrace>y</showStackTrace>
<mailDebug>n</mailDebug>
<expressoDir>dierendatabase</expressoDir>
</context>
补充说明:
1. 例子是在原来的连接池配置上添加了一个Mysql连接;
2. 配置使用<hasSetupTables>true</hasSetupTables>确定Expresso系统表在哪个数据库里;
3. 在没有显式调用ControllerRequest、State等对象的setDataContext()或setDBName()方法的情况下,程序在调用一个名为default的上下文连接池;即使是显调用,指定的连接池也只在一次请求过程有效;若采用Expresso的用户登录,该连接池名字保存在cookies里。(setDataContext()为建议使用方式);
4. 通过Schema注册数据对象的add(class,String);方法,或在创建数据对象里使用setDBName()方法,设置数据库名称,可以将表映射到不同机器的数据库中。
5. 注意type-mapping部分,与数据库的无关性正是通过该设置实现的。
6. 其他数据库配置请直接参阅Expresso文档。
3 直接使用连接池
3.1 运行自定义SQL语句
定义的数据对象类(DBObject)往往不能完全满足编写灵活的数据操作程序的需要,可喜的是,Expresso定义的DBConnection类为我们提供了直接运行SQL语句的方法,为程序的开发带来极大程度的方便。典型的用法如下:
DBConnectionPool pool = DBConnectionPool.getInstance("default");
DBConnection connection =pool.getConnection();
connection.execute("select * from setup");
while (connection.next())
{
String s=connection.getString("ID");
}
pool.release(connection);或connection.release();
当然,若需要平台转移的兼容等问题,旧的程序只需要标准的数据库连接池对象,则代码可以如下编写:
javax.sql.DataSource dataSource = DBConnectionPool.getDataSource("default");
java.sql.Connection connection = dataSource.getConnection();
//... Use the connection here
connection.close();
3.2 事务处理
事务属于连接池属性,数据表对象以及直接运行SQL均需要借助连接池的事务处理功能。典型应用如下:
DBConnectionPool myPool = null;
DBConnection myConnection = null;
try {
myPool = DBConnectionPool.getInstance(getDataContext());
myConnection = myPool.getConnection();
myConnection.setAutoCommit(false);
Customer oneCustomer = new Customer(myConnection);
Invoice oneInvoice = new Invoice(myConnection);
/* populate the Invoice fields */
oneCustomer.setField("Balance", newBalance);
oneInvoice.add();
oneCustomer.update();
myConnection.commit();
} catch (DBException de) {
if (myConnection != null)
myConnection.rollback();
throw newDBConnection(de);
} finally {
if (myPool != null) {
myPool.release(myConnection);
}
}
4 使用DBObject
4.1 创建DBObject
创建数据对象可以有两个方法,一是使用DBTool工具创建,创建方法见《使用Expresso DBTool帮助开发过程.》一文;另一种方法就是根据PowerDesigner的表图手动创建。
建议方法是:先使用PowerDesigner生成数据库脚本,在具体数据库上生成数据库表,之后使用DBTool一次性创建。
4.1.1 指定表所属数据库
一般情况下,在定义DBObject时并不设置数据库,而在使用该DBObject时指定,即在某个控制器对象里调用方法如下:
custList.setDataContext(getDataContext());
4.1.2 虚拟字段
象发票的总金额、从另外的表Lookup过来的字段这些数据,它们并不需要保存在数据库表中,而仅仅起到一个显示作用,这时,我们便可以采用虚拟字段。
我们在setupFields()方法中定义(没有特别说明,以下方法均在setupFields()方法中使用):
addVirtualField("CustomerName", "varchar", 30, "Customer Name");
为了在读取该字段时自动取得数据,我们需要覆盖getField()方法,如下:
public String getField(String fieldName) throws DBException {
if (fieldName.equals("CustomerName")) {
Customer ourCustomer = newCustomer();
ourCustomer.setDataContext(getDataContext());
ourCustomer.setField("CustomerID", getField("CustomerID"));
ourCustomer.retrieve();
return ourCustomer.getField("CustomerName");
}
return super.getField(fieldName);
}
4.1.3 只读字段
象创建日期时间、序列,这些都是需要保护的字段,我们通常将之设为只读属性,设置方法如下:
setReadOnly("CreationDate");
你可以使用Expresso的"auto-inc"字段类型来产生一个唯一的序列字段,该字段将在你增加记录时自动产生而存入数据库表。
4.1.4 多值字段
有些字段的值是规定而有限的(多但可数),如表示只有Yes或No的布尔类型字段,这种情况下,我们简单地使用如:
setMultiValued("AccountOpen");
同时覆盖getValidValues()方法即可,如下:
public Vector getValidValues(String fieldName) throws DBException {
if (fieldName.equals("AccountOpen")) {
Vector myValues = newVector();
myValues.addElement(new ValidValue("Y", "Yes"));
myValues.addElement(new ValidValue("N", "No"));
return myValues;
}
return super.getValidValues(fieldName);
}
另外一种情况,这种校验数据来自另外的DBObject,首先,我们需要在本DBObject里设置形如:
setMultiValued("CustomerType");
setLookupObject("CustomerType", "com.yourcompany.dbobj.CustomerType");
然后,在另外一个DbObject里覆盖getValues()方法,如下:
public Vector getValues() throws DBException {
Vector myValue = this.getValuesDefault( "Dept_Id", "Dept_Name" );
return myValues;
}
这个结果列表自动被Expresso存入缓存,以加快访问速度。
这种校验方法在DBMaint里得到特殊的处理,处理细节可参考DBMaint的源码。
4.1.5 保密字段
象密码这样的字段,需要得到适当的保护,不可提供查阅功能,使得程序使用任何"search"方法时都隐藏起来。在这种情况下,我们可以使用setSecret()方法处理。
4.1.6 字段掩码
当然我们也可以使用setMask(fieldname, Mask)方法以掩码方式保护字段。
4.2 操作DBObject
4.2.1 增加记录
调用setField()函数设置数据表字段内容,特别是键值字段,最后调用add()方法将数据保存到数据库,即完成增加记录操作。
如果一个记录的关键字已经存在,会产生异常。对设置有默认值的字段,可以忽略设置。
addOrUpdate()方法则是通过检查键值字段在数据库表里是否存在而决定那种操作数据提交方式,从而避免了上述异常,是一个很有用的方法。
4.2.2 查询记录
4.2.2.1 查询单个记录
使用关键字检索特定的记录,这里需要该关键字是数据库表的键值字段,取得特定关键字值的记录相对较简单,如:
oneCustomer.setField("CustomerID", "1");
oneCustomer.retrieve();
oneCustomer.getField(fieldName);
在不知道用户ID(键值)的时候,我们能否通过其它的标准来设置它们?retrieve()方法是不能完成此功能的,我们需要使用find()方法,比如:
/* Erase any current values in fields */
oneCustomer.clear();
oneCustomer setField("CustomerName", "Jones");
if (oneCustomer.find()) {
System.out.println("Jones found!");
}
Find()方法在查找成功的时候会返回一个true值并且对于第一次查找的机器会在DBObject设置一个值,如果查找失败将返回一个false值,并且记录不返回。
4.2.2.2 查询多个纪录(记录集)
DBObjects不仅可以检索一条记录,而且可以查询一个记录集。比如,我们要在所有的"AB"型的用户上完成一些处理,我们可以这样写代码:
Customer custList = new Customer();
Customer oneCustomer = null;
custList.setField("CustomerType", "AB");
for (Iterator e = custList.searchAndRetrieveList()
.iterator(); e.hasNext(); ) {
oneCustomer = (Customer) e.next();
/* do whatever we need to do to oneCustomer */
}
通过这种方式,我们可以一次处理所需的尽可能大的记录集,达到最小访问数据库次数而取得程序最大运行效率。
4.2.2.2.1 排序查询结果数据
如果我们需要按照一个具体的顺序处理记录集,我们可以使用另外一个重载的searchAndRetrieve()方法,可以加入一个参数调用来指定各种排序的字段。比如:
custList.searchAndRetrieveList("CustomerName");
表示通过客户名升序查询记录;如果想按照降序查询,则需要在最后的字符串后标明"Desc",比如:
custList.searchAndRetrieveList("CustomerName Desc");
你也可以通过使用"|",来标注不同的字段。
custList.searchAndRetrieveList("CustomerName|CustomerID");
4.2.2.2.2 使用查询范围
没有明确查询条件的查询是不可取的,也就是说将一个上千万条记录的表数据一次取出来是不现实的,大量的数据流将影响系统的稳定性,可以说这是极其不安全的。因此,在使用查询记录集时,请记住设置合适的查询条件。
多次调用setField()方法,还会产生关联(and)效果,比如:
custList.setField("CustomerName", "Jones");
custList.setField("CustomerType", "AB");
以上代码的意思是你要设置一个叫Jones的名字,而且他的类型是"AB"。
对于当前DBObject,如果你需要特别的查询条件,如“or”条件,你可以采用如下方法:
custList.setCustomWhereClause("CustomerType = /"AA"/ OR CustomerType = /"BA"/");
为安全起见,该查询条件仅在下一次的查询运行时候有效,每次执行它都被置空。
当然你可以使用更多的条件查询方式,只是具体的语法细节可能依赖具体的数据库系统,你可以参阅相关数据库系统文档。比如:
custList.setField("CustomerName", "A%");
表示查询以"A"开头的所有记录.
custList.setField("CustomerName", "[A-M]%");
则表示查询所有从"A"到"M"开头的客户名字的记录,并且,
custList.setField("CustomerID", "BETWEEN 1 AND 20");
表示查询所有ID从1到20客户的记录。
你可以通过DBObject.setMaxRecords(int)和DBObject.setOffset(int)这两个方法限制结果集合的数量,减少内存使用数量,从而提高程序运行效率。当然count()方法返回的是本次满足查询条件的记录条数,它们之间互相配合,就可以实现记录的分页显示程序了。
另外一种情况,如果你需要处理一个非常大的记录集,则可以采用标记技术,比如如下,这里我们假设所有的未处理的记录的Processed字段都被初始化成N的值:
Customer cust.List = new Customer();
Customer oneCust = null;
custList.setField("Processed", "N");
custList.setMaxRecords(100);
. boolean moreRecords = true;
while (moreRecords) {
for (Iterator i = custList.searchAndRetrieveList().iterator();e.hasNext();) {
oneCust = (Customer) e.next();
/* Process Customer */
oneCust.setField("Processed", "Y");
oneCust.update();
}
if (custList.count() == 0) {
moreRecords = false;
} /* if */
} /* while */
以上代码一次处理100个客户的信息。
4.2.2.2.3 指定查询结果字段
在实际查询语句中,象"SELECT a, b, c FROM..." 要比 "SELECT * FROM..."高效。DBObject也可以通过"setFieldsToRetrieve(String)"方法支持这个功能。
4.2.3 更新记录
在数据库中更新记录操作很简单,在要改变的地方调用update()方法即可。每次调用该方法时,仅仅只更新一条记录。因此,为安全起见,更新时经常和retrieve()方法一起连用。
(若采用事务提交,可以在参考commit和rollback章节)
4.2.4 删除记录
和添加一样,删除也至少需要关键字字段,调用delete()方法完成删除操作。
注:删除和更新、添加一样,每次只能操作一条记录。需要进行多记录操作,请使用前文提到的事务处理方法。
5 DBOjects使用缓存
为了改善性能,DBOjects和有效的值可以缓存或者保存在内存中。因为内存要比磁盘的速度快好多倍,这样可以显著改善性能。
通过添加一个实体到DBObjPageLimit表中,DBObject可以被缓存起来。可以通过为DBObject一系列的缓存操作方法进行缓存操作。
最好的设置缓存的大小依照你的应用程序和JVM可见的内存。JVM的操作命令可以改变缓存在内存中的大小。
当用getDefaultValues方法时,缓存会自动使用。
缓存里的DBObject和有效的值受到升级,删除,增添的影响,因此缓存中的值总是有效的。这也是另外一个只使用DBObject数据库访问你的应用程序的重要的原因--如果你乐意,缓存中的值始终保持最新的。另一方面,如果你使用了直接SQL提交记录,那么相对于你数据库中的数据缓存中的值是过期的。
关于缓存更多的细节,参看缓存文档
6 数据安全
从SecuredDBObject继承的数据库对象自动获得安全机制,安全的实现则是由一系列的用户表和一些组信息组成。
SecuredDBObject仅提供了安全功能,这是设计模式的的代理模式。
在具体程序编写中,可通过调用setUser(String)方法设置请求操作的许可,比如:
Customer oneCustomer = newCustomer();
oneCustomer.setUser("Fred");
oneCustomer.setField("CustomerID", "1");
oneCustomer.retrieve();
以上上代码表示只有Fred用户会正确运行,如果不是该用户调用,retrieve抛出一个安全异常。
你也可以在调用方法时检查安全许可,比如:
custList.isAllowed("S");
表示如果允许当前用户查找(Search)将返回一个true值,否则是false值。同样,A,U和D表示Add,Update和Delete的许可情况。
7 多数据对象(MultiDBObjects)
多数据对象在Expresso5.1后的版本才得到支持,数据对象是数据库里表的概念,多数据对象则是视图的概念。多数据对象不需要定义,直接使用就是,下面便实现了一个多对多表间查询的例子。
MultiDBObject myMulti = new MultiDBObject();
myMulti.setDataContext(getDataContext());
myMulti.addDBObj("com.jcorporate.expresso.services.dbobj.UserDBObj");
myMulti.addDBObj("com.jcorporate.expresso.services.dbobj.UserGroup",
"group");
myMulti.addDBObj("com.jcorporate.expresso.services.dbobj.GroupMembers",
"members");
setForiegnKey("members", "UserName", "User", "UserName");
setForiegnKey("members", "GroupName", "group", "GroupName");
MultiDBObject oneMulti = null;
myMulti.setField("User", "Username", "Fred");
System.out.println("User Fred belongs to the following groups:");
for (Enumeration e = myMulti.searchAndRetrieve().elements();
e.hasMoreElements()); {
oneMulti = (MultiDBObject)e.nextElement();
System.out.println(oneMulti.getField("group", "Descrip");
}
使用这种方法,再复杂的查询应该都能实现了。
8 JoinedDataObject
和MultiDBObject比较,JoinedDataObject相当于有定义的视图,只是这个定义放在.xml文件里,典型的使用方法如下:
import com.jcorporate.expresso.core.dataobjects.jdbc.JoinedDataObject;
import com.jcorporate.expresso.core.dataobjects.Securable;
//...
JoinedDataObject myjoin = new JoinedDataObject();
myjoin.setRequestingUid(Securable.SYSTEM_ACCOUNT);
myjoin.setDataContext("default");
myjoin.setDefinition("/org/example/myapp/dbobj/myjoin.xml");
//...
/org/example/myapp/dbobj/myjoin.xml文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dataobject-join PUBLIC "-//Jcorporate Ltd//DTD Expresso DataObject Join 5.1//EN"
"http://www.jcorporate.com/dtds/jdbc-join_5_1.dtd">
<dataobject-join description="Download Log View">
<dataobject className="com.jcorporate.expresso.ext.dbobj.DownloadLog"
alias="DownloadLog"/>
<dataobject className="com.jcorporate.expresso.ext.dbobj.DownloadFiles"
alias="DownloadFiles"/>
<relations>
<foreign-key local-alias-ref="DownloadLog" local-alias-key="FileNumber"
foreign-alias-ref="DownloadFiles" foreign-alias-key="FileNumber" />
</relations>
</dataobject-join>
更具体的使用细节请查看Expresso Developer's Guide文档。
9 媒体数据对象MediaDBObject
从Expresso5.1开始,便可以处理象图像(image)、视频(video)、大文档(text)等数据,功能的实现依靠一系列标识有BLOB字样的方法完成。
10 总结
很明显,Expresso是想用DBObject封装数据库层的操作,在不是万不得一的情况下,我们最好不要使用连接池运行SQL语句功能直接操作数据库,以维护整个持久层的完整性。在使用缓存的情况下,直接使用SQL进行数据的新增、更新与删除将是极为不安全的做法。
Expresso持久层的还一个目的是将数据库简化为一种简单的存储器,所有的触发器、存储过程和表间关系均在持久层实现。也就是说,我们不需要在数据库里编写那些与业务有关的处理,而是定义DBObject得以实现。
尽量不要将业务逻辑放入持久层,业务逻辑是控制器对象所要处理的主要问题。这样,我们将持久层纯化为仅仅是一种数据库访问工具。
Expresso持久层的功能不仅是功能强大的开发包,也是一套有效的数据库维护工具,DBMaint就是使用之的一个很好的实例。
11 参考资料
Expresso 5.5.0 Developer's Guide
Expresso 5.5.0 JavaDoc