Torque中的一些问题
说明:这里描述的问题来自: Torque 3.1.1, Village2.0 dev,文中用到的代码来自开源Apache项目. 文中虽例子,但主要是辅助说明错误原因和解决办法.以给使用Turbine+Torque+Velocity的人一点帮助.
Torque是apache数据库项目中一个子项目,它实现数据库的持久层,是JDO的Apache实现.自从和Turbine项目分出来后,它形成了两部分:
生成器和运行时库.生成器用来生成数据库资源和访问数据库的类,使用这些类和在运行时库的支撑下,应用程序对数据库的各种操作得以用java对象的普通用法和操作习惯实现.
Torque除了用到Apache其他项目如Commons的成果外,还用到了一个开源项目:Village(http://share.whichever.com/index.php?SCREEN=village),它位于JDBC API上,提供不用写sql语句就可以实现数据库的crud操作.使用它和数据库里的表以及里面的记录打交道,就象是在一个小镇,一些小屋和小屋里的人们交谈,非常祥和的感觉.它的设计也是非常简洁,占用的空间也不大,真的就象一个游者,可以很轻松的驴行天下.
Torque的生成器使用了Velocity项目,该项目是java的模板语言,在生成数据库资源时,它出了全力,我们可以通过修改{Torque.dir}/templates中各子文件夹里的vm模板来适合数据库及你项目中的需要.例如,对于PostgreSQL8,Torque生成的库资源中创建数据库的sql代码中没有指定代码页,这样PostgreSQL将使用默认的SQL_ASCII设置,如果想让生成的资源指定代码页可以修改{Torque.dir}/templates/sql/db-init/postgresql中的createdb.vm,本例指定代码页为UNICODE,如下:
#foreach ($databaseName in $databaseNames)
drop database $databaseName;
create database $databaseName
ENCODING = 'UNICODE';#增加该行即可
#end
用Torque做为项目的持久层,其开发过程是很简单的,需要提供的总文件数是4个:
在设计阶段使用的build.properties,project-schema.xml,id-table-schema.xml以及在开发阶段的运行时用到的Torque.properties.在构建阶段为每个表产生五个类文件:
<table-name>.java,<table-name>Peer.java,
Base<table-name>.java,Base<table-name>Peer.java,这俩文件不要修改,因为用ant重新构建时,这些文件会重新生成.把文件名称以Peer结尾的称为Peer类,实现对象关系影射(ORM),不以Peer结尾的称为数据对象,应用和数据对象交互.不以base开头的类扩展了以base开头的类.业务逻辑应该加在子类上.<table-name>MapBuilder.java,这是用于特定表<table-name>的影射文件.Peer类的crud操作就是建立在Village之上的.
现在开始一个例子,项目名称是cms,以这个例子说明一些问题.
这是一个cms项目的模式文件cms-schema.xml:
<?xml version="1.0" encoding="gb2312" standalone="no" ?>
<!DOCTYPE database SYSTEM
"http://jakarta.apache.org/turbine/dtd/database.dtd">
<database name="news" package="org.news.om">
<table name="atts" idMethod="none" description="保存了新闻的附件信息,它和新闻存在引用关系.">
<column name="pagedate" javaName="pagedate" primaryKey="true" required="true" type="VARCHAR"
size="8" javaNamingMethod="underscore" description="页创建日期,缩略形式"></column>
<column name="pagecode" javaName="pagecode" primaryKey="true" required="true" type="VARCHAR"
size="5" javaNamingMethod="underscore" description="页代码"></column>
<column name="filename" javaName="filename" primaryKey="false" required="true" type="VARCHAR"
size="30" javaNamingMethod="underscore" description="文件名称"></column>
<column name="oldfilename" javaName="oldfilename" primaryKey="false" required="false" type="VARCHAR"
size="30" javaNamingMethod="underscore" description="旧文件名称"></column>
<column name="attname" javaName="attname" primaryKey="false" required="true" type="VARCHAR"
size="50" javaNamingMethod="underscore" description="附件名称,由用户指定的名称"></column>
<column name="img" javaName="img" primaryKey="false" required="false" type="BLOB"
javaNamingMethod="underscore" description="附件文件,保存大文件"></column>
<column name="attdes" javaName="attdes" primaryKey="false" required="false" type="VARCHAR"
size="50" javaNamingMethod="underscore" description="对附件的描述"></column>
</table>
</database>
我们只关注生成java文件,使用该文件Torque生成了5个类,下面是影射类AttsMapBuilder:
其代码片段为:
public void doBuild() throws TorqueException
{
dbMap = Torque.getDatabaseMap("news");
dbMap.addTable("atts");
TableMap tMap = dbMap.getTable("atts");
tMap.setPrimaryKeyMethod("none");
tMap.addPrimaryKey("atts.PAGEDATE", new String());
tMap.addPrimaryKey("atts.PAGECODE", new String());
tMap.addColumn("atts.FILENAME", new String());
tMap.addColumn("atts.OLDFILENAME", new String());
tMap.addColumn("atts.ATTNAME", new String());
tMap.addColumn("atts.IMG", new Object());
tMap.addColumn("atts.ATTDES", new String());
}
问题一:删除数据时报告错误:You must specify KeyDef attributes for this TableDataSet in order to create a Record for update.
原因:因为Torque的Peer使用了Village,而KeyDef是其中一个类,该类定义为:
public class KeyDef
{
private Vector mySelf = null;
public KeyDef()
{
mySelf = new Vector();
mySelf.addElement ("");
}
public KeyDef addAttrib(String name)
{
mySelf.addElement (name);
return this;
}
public boolean containsAttrib (String name)
{
return (mySelf.indexOf ((Object) name) == -1) ? false : true;
}
public String getAttrib (int pos)
{
if (pos == 0)
pos = 1;
try
{
return (String) mySelf.elementAt (pos);
}
catch (ArrayIndexOutOfBoundsException e)
{
return null;
}
}
public int size()
{
return mySelf.size() - 1;
}
}
该类的作用很简单,就是保存一个列名称.提供给其他类DataSet和Record使用,通过读其源码,发现Village很依赖表的primary键,如果一个表没有
primary键,那么你不能更新和删除记录.
解决办法就是为表创建primary键.
问题二:当删除多行时,遭到操作失败.
原因:Village实现的比较奇怪,就是插入更新删除记录时,若影响行数大于1时会生成一个异常,下面的代码片段位于Record类中:
private int saveWithDelete (Connection connection)
throws DataSetException, SQLException
{
PreparedStatement stmt = null;
try
{
stmt = connection.prepareStatement (getSaveString());
int ps = 1;
for (int i = 1; i <= dataset().keydef().size(); i++)
{
Value val = getValue (dataset().keydef().getAttrib(i));
val.setPreparedStatementValue (stmt, ps++);
}
int ret = stmt.executeUpdate();
setSaveType (Enums.ZOMBIE);
if (ret > 1)
throw new SQLException ("There were " + ret + " rows deleted with this records key value.");
return ret;
}
catch (SQLException e1)
{
throw e1;
}
finally { try
{
if (stmt != null)
stmt.close();
}
catch (SQLException e2)
{
throw e2;
}
}
}
而这个异常在org.apache.torque.util.BasePeer类中是这样处理的:
public static void doDelete(Criteria criteria) throws TorqueException
{
Connection con = null;
try
{
con = Transaction.beginOptional(
criteria.getDbName(),
criteria.isUseTransaction());
doDelete(criteria, con);
Transaction.commit(con);
}
catch (TorqueException e)
{
Transaction.safeRollback(con);
throw e;
}
}
事务回滚导致删除操作失败.实际上不仅是对删除操作这样,而且对于其它操作插入更新也是这样的.
解决办法就是重写Peer子类:因为Peer类是BasePeer的子类,但doDelete(Criteria criteria)是静态方法,不能覆盖,所以我们要重新命名
上面方法为doDelete1(Criteria criteria),在问题三具体给出代码.
问题三:有时候表中不存在唯一标识一条记录的字段组合(除了整行记录),想以某几个字段删除行.比如本例,
允许指定两个字段PAGEDATE,PAGECODE的值删除记录.
有两种办法:
1 为了能使字段PAGEDATE,PAGECODE的值相等的多行存在,手工删除表中的primary键,保持影射文件不变,增删改都可工作.
2 修改本例的BaseAttsPeer类,这样做要注意备份这个文件.
增加两个函数:doDelete1(Criteria criteria, Connection con),doDelete1(Criteria criteria),这俩函数替代BasePeer中的
doDelete(Criteria criteria, Connection con),doDelete(Criteria criteria)两个函数的功能,再把BaseAttsPeer类中定义的
doDelete所有重载版本的调用改到对doDelete1的调用,就可以了.
doDelete1代码片段为:
public static void doDelete1(Criteria criteria) throws TorqueException
{
Connection con = null;
try
{
con = Transaction.beginOptional(
criteria.getDbName(),true);
//criteria.isUseTransaction());
doDelete(criteria, con);
Transaction.commit(con);
}
catch (TorqueException e)
{
Transaction.commit(con);
//Transaction.safeRollback(con);
throw e;
}
}
/**
* Method to perform deletes based on values and keys in a Criteria.
*
* @param criteria The criteria to use.
* @param con A Connection.
* @throws TorqueException Any exceptions caught during processing will be
* rethrown wrapped into a TorqueException.
*/
public static void doDelete1(Criteria criteria, Connection con)
throws TorqueException
{
DB db = Torque.getDB(criteria.getDbName());
DatabaseMap dbMap = Torque.getDatabaseMap(criteria.getDbName());
// Set up a list of required tables and add extra entries to
// criteria if directed to delete all related records.
// StringStack.add() only adds element if it is unique.
HashSet tables = new HashSet();
Iterator it = criteria.keySet().iterator();
while (it.hasNext())
{
String key = (String) it.next();
Criteria.Criterion c = criteria.getCriterion(key);
List tableNames = c.getAllTables();
for (int i = 0; i < tableNames.size(); i++)
{
String name = (String) tableNames.get(i);
String tableName2 = criteria.getTableForAlias(name);
if (tableName2 != null)
{
tables.add(new StringBuffer(
name.length() + tableName2.length() + 1)
.append(tableName2)
.append(' ')
.append(name)
.toString());
}
else
{
tables.add(name);
}
}
if (criteria.isCascade())
{
// This steps thru all the columns in the database.
TableMap[] tableMaps = dbMap.getTables();
for (int i = 0; i < tableMaps.length; i++)
{
ColumnMap[] columnMaps = tableMaps[i].getColumns();
for (int j = 0; j < columnMaps.length; j++)
{
// Only delete rows where the foreign key is
// also a primary key. Other rows need
// updating, but that is not implemented.
if (columnMaps[j].isForeignKey()
&& columnMaps[j].isPrimaryKey()
&& key.equals(columnMaps[j].getRelatedName()))
{
tables.add(tableMaps[i].getName());
criteria.add(columnMaps[j].getFullyQualifiedName(),
criteria.getValue(key));
}
}
}
}
}
Iterator tabIt = tables.iterator();
while (tabIt.hasNext())
{
String tab = (String) tabIt.next();
KeyDef kd = new KeyDef();
HashSet whereClause = new HashSet();
ColumnMap[] columnMaps = dbMap.getTable(tab).getColumns();
for (int j = 0; j < columnMaps.length; j++)
{
ColumnMap colMap = columnMaps[j];
/*原来的在BasePeer类中操作KeyDef加入列字段的代码
if (colMap.isPrimaryKey())
{
kd.addAttrib(colMap.getColumnName());
}
*/
String key = new StringBuffer(colMap.getTableName())
.append('.')
.append(colMap.getColumnName())
.toString();
if (criteria.containsKey(key))
{
if (criteria.getComparison(key).equals(Criteria.CUSTOM))
{
whereClause.add(criteria.getString(key));
}
else
{
//增加下列语句,是为了提供where子句中的限定,以利于删除数据.
//这种修改就避免了表一定要有primarykey的限制.
kd.addAttrib(colMap.getColumnName());
whereClause.add(SqlExpression.build(
colMap.getColumnName(),
criteria.getValue(key),
criteria.getComparison(key),
criteria.isIgnoreCase(),
db));
}
}
}
// Execute the statement.
TableDataSet tds = null;
try
{
tds = new TableDataSet(con, tab, kd);
String sqlSnippet = StringUtils.join(whereClause.iterator(), " AND ");
if (log.isDebugEnabled())
{
log.debug("BasePeer.doDelete: whereClause=" + sqlSnippet);
}
tds.where(sqlSnippet);
tds.fetchRecords();
if (tds.size() > 1 && criteria.isSingleRecord())
{
handleMultipleRecords(tds);
}
for (int j = 0; j < tds.size(); j++)
{
Record rec = tds.getRecord(j);
rec.markToBeDeleted();
rec.save();
}
}
catch (Exception e)
{
throwTorqueException(e);
}
finally
{
if (tds != null)
{
try
{
tds.close();
}
catch (Exception ignored)
{
}
}
}
}
}
private static void throwTorqueException(Exception e) throws TorqueException {
if (e instanceof TorqueException) {
throw (TorqueException) e;
}
else {
throw new TorqueException(e);
}
}
这种修改解决了本问题,也彻底修正了上面遇到的2个问题.