摘要
hibernate和struts是当前市面上几个最流行的开源的库之一。它们很有效率,是程序员在开发java企业应用,挑选几个竞争的库的首选。虽然它们经常被一起应用,但是hibernate的设计目标并不是和struts一起使用,而struts在hibernate诞生好多年之前就发布了。为了让它们在一起工作,仍然有很多挑战。这篇文章点明了struts和hibernate之间的一些鸿沟,尤其关系到面向对象建模方面。文章也描述了如何在两者间搭起桥梁,给出了一个基于扩展struts的解决方案。所有的基于struts和hibernate构建的web应用都能从这个通用的扩展中获益。
在hibernate in action(manning,2004十月)这本书里,作者christian bauer和gavin king揭示了面向对象世界的模型和关系数据模型,两个世界的范例是不一致的。hibernate非常成功地在存储层(persistence layer)将两者粘合在一起。但是领域模型(domain model)(也就是model-view-controller的model layer)和html页面(mvc的view layer)仍然存在不一致。在这篇文章中,我们将检查这种不一致,并且探索解决的方案。
范例不一致的再发现
让我们先看一个经典的parent-child关系例子(看下面的代码):product和category。category类定义了一个类型为long的标示符id和一个类型为string的属性name。product类也有一个类型为long的标示符id和一个类型为category的属性category,表示了多对一的关系(也就是说很多product可以属于一个category)
我们希望一个product可以被更改category,所以我们的html提供了一个下拉框列出所有category。
这里我们看出了两者的不一致:在product领域对象里,category属性是category类型,但是productform只有一个类型为long的categoryid。这种不匹配不但增加了不一致,而且导致了不必要的代码进行primitive type的标示符和对应的对象之间的转换。
这种不一致部分是由于html form自己引起的:它只代表了一种关系模型,不能代表面向对象的模型。面向对象和关系模型的不一致在存储层由对象关系映射(o/rm)解决。但是类似的问题在表示层(view layer)仍然存在。解决的关键是让他们一起无缝地工作。
struts的功能和局限
幸运的是,struts能够生成和解释内嵌的对象属性。category下拉框可以用struts page-construction(html) tag library:
我们假设categories是category对象的一个list。所以现在我们要修改productform,让它变得更加“面向对象”,我们要修改productform的categoryid,改成类型为category的category。这种改变会导致在product和productform之间复制属性的工作更加繁琐,因为两者有相同的属性。
当我们完成剩余的struts action, configuration, validator, jsp, hibernate层后,开始测试,我们马上在访问productform.category.id时遇到了nullpointerexception。这是预料中的!因为productform.category还没有被设置,同时,hibernate也会将多对一所联系的对象引用设为空(如果database field为空指)(译者:这里指hiberate将product.category为null,如果该product没有联系到任何category)。struts要求所有的对象在显示(生成html form)和传播(提交html form)之前被建立。
让我们看看如何用actionform.reset()来架起桥梁。
(并非如此)臭名昭著的struts actionform
在我第一个星期接触struts的时候,我最大的一个疑问就是:为什么我必须为properties, getter方法, setter方法保持几乎完全相同的两份copy, 一份在actionform bean, 一份在domainobject。这个繁琐的步骤成了struts社区最主要的抱怨之一。
以我的观点,actionform存在有原因的。首先,它们可以区别于domain object因为他们但当了不同的角色。在mvc模式下,domain object是model层的一个部分,actionform是view层的。因为webpage的field和database的field可能不一样,某些特制的转换是常见的。第二,actionform.validate()方法可以定义非常好用的验证规则。第三,可能有其他的,特定的view行为,但是又不想在domain layer实现,特别当persistence framework来管理domain object的时候。
提交form
让我们来用actionform内有的方法-reset()-来解决view和model之间的不一致。这个reset()方法是在actionform在被struts controller servlet处理request时候复制actionform属性之前调用的。这个方法最常见的使用是:checkbox必须被显式地设为false,让没有被选中的checkbox被正确识别。reset()也是一个初始化用于view rending对象的合适地方。代码看起来是这样的:
struts在使用用户提交的值填写productform之前,struts会调用reset(),这样category属性将会被初始化。请注意,你必须检查category看它是不是null,后面我们会讨论这个。
编辑form
到目前为止,我们已经解决了form提交时候的问题。但是当我们在生成form页面的时候呢?html:select tag也希望有一个非空的引用,所以我们将在form生成页面之前调用reset()。我们在action类里加入了一行:
我假设读者已经对action类和jakarta commons beanutils包非常熟悉了。createorloadproduct()建立了一个新的product实例或者从数据库里载入一个已有的实例,具体取决于这个action是建立或者修改product的。productform被赋值后(译者:也就是调用propertyutils.copyproperties后),productform.category已经从product.category复制过来了(译者:实际上只是复制了category对象引用,并没有开销),然后,productform就能用来生成页面了。我们同时也必须保证:不覆盖已经被hibernate载入的对象,所以我们必须检查(category)是不是为null。
因为reset()方法是在actionform中定义的,我们可以把上述代码放入一个superclass,比如commoneditaction,来处理这些事情:
如果你需要一个只读的form, 你有两个选择: 第一检查所联系的jsp对象是不是null, 第二复制domain对象到actionform之后调用reset()
保存domain对象
我们解决了提交form和生成form页面的问题, 所以struts可以满足了。但是hibernate呢?当用户选择了一个null id option – 在我们的例子中“no category”option- 并且提交form, productform.category指向一个新建立的hibernate对象,id为null。当category属性从productform复制到hibernate控制的product对象并且存储时,hibernate会抱怨product.category是一个临时对象,需要在product存储前先被存储。当然,我们知道它是null,并且不需要被存储。所以我们需要将product.category置为null,然后hibernate就能存储product了(译者:在这种情况下,数据库product.category被设成空值)。我们也不希望改变hibernate的工作方式,所以我们选择在复制到domain对象之前清理这些临时对象,我们在productform中加了一个方法:
我们在copyproperties之前清理掉这些临时对象,所以如果productform.category只是用来放null的,则将productform.category置为null。然后domain对象的category也会被设成null:
一对多关系
我还没有解决category到product的一对多关系。我们把它加入到category的metadata中:
注意:hibernate的cascade属性为all-delete-orphan表明:hibernate需要在存储包含的category对象时候,自动存储product对象。和parent对象一起存储child对象的情况并不常见,常见的是:分别控制child的存储和parent的存储。在我们的例子中,我们可以容易地做到这一点,如果我们允许用户在同一个html page编辑category和products。用set表示products是非常直观的:
更进一步
我们已经可以察看,编辑,提交forms,并且存储相关的objects,但是为所有的actionform类定义cleanupemptyobjects()和reset()方法是个累赘。我们将用一个抽象的actionform来完成协助完成这些工作。
作为通用的实现,我们必须遍历所有的hibernate管理的domain对象,发现他们的identifier,并且测试id值。幸运的是:org.hibernate.metadata包已经有两个utility类能取出domain对象的元数据。我们用classmetadata类检查这个object是不是hibernate管理的。如果是:我们把它们的id value取出来。我们用了jakarta commons beanutils包来协助javabean元数据的操作。
为了让代码可读,我们省略了exception的处理代码。
我们的新abstractform类从struts的actionform类继承,并且提供了通用行为:reset和cleanup多对一关联对象。当这个关系是相反的话(也就是一对多关系),那么每个例子将会有所不同,类似在abstract类里实现是比较好的办法。
总结
struts和hibernate是非常流行和强大的框架,他们可以有效地相互合作,并且弥补domain模型和mvc视图(view)之间的差别。这篇文章讨论一个解决struts/hibernate project的通用的方案,并且不需要大量修改已经有的代码。
hibernate和struts是当前市面上几个最流行的开源的库之一。它们很有效率,是程序员在开发java企业应用,挑选几个竞争的库的首选。虽然它们经常被一起应用,但是hibernate的设计目标并不是和struts一起使用,而struts在hibernate诞生好多年之前就发布了。为了让它们在一起工作,仍然有很多挑战。这篇文章点明了struts和hibernate之间的一些鸿沟,尤其关系到面向对象建模方面。文章也描述了如何在两者间搭起桥梁,给出了一个基于扩展struts的解决方案。所有的基于struts和hibernate构建的web应用都能从这个通用的扩展中获益。
在hibernate in action(manning,2004十月)这本书里,作者christian bauer和gavin king揭示了面向对象世界的模型和关系数据模型,两个世界的范例是不一致的。hibernate非常成功地在存储层(persistence layer)将两者粘合在一起。但是领域模型(domain model)(也就是model-view-controller的model layer)和html页面(mvc的view layer)仍然存在不一致。在这篇文章中,我们将检查这种不一致,并且探索解决的方案。
范例不一致的再发现
让我们先看一个经典的parent-child关系例子(看下面的代码):product和category。category类定义了一个类型为long的标示符id和一个类型为string的属性name。product类也有一个类型为long的标示符id和一个类型为category的属性category,表示了多对一的关系(也就是说很多product可以属于一个category)
/**
* @hibernate.class table="category"
*/
public class category {
private long id;
private string name;
/**
* @hibernate.id generator-class="native" column="category_id"
*/
public long getid() {
return id;
}
public void setid(long id) {
this.id = id;
}
/**
* @hibernate.property column="name"
*/
public string getname() {
return name;
}
public void setname(long name) {
this.name = name;
}
}
/**
* @hibernate.class table="product"
*/
public class product {
private long id;
private category category;
/**
* @hibernate.id generator-class="native" column="product_id"
*/
public long getid() {
return id;
}
public void setid(long id) {
this.id = id;
}
/**
* @hibernate.many-to-one
* column="category_id"
* class="category"
* cascade="none"
* not-null="false"
*/
public category getcategory() {
return category;
}
public void setcategory(category category) {
this.category = category;
}
}
我们希望一个product可以被更改category,所以我们的html提供了一个下拉框列出所有category。
<select name="categoryid">
<option value="">no category</option>
<option value="1">category 1</option>
<option value="2">category 2</option>
<option value="3">category 3</option>
</select>
这里我们看出了两者的不一致:在product领域对象里,category属性是category类型,但是productform只有一个类型为long的categoryid。这种不匹配不但增加了不一致,而且导致了不必要的代码进行primitive type的标示符和对应的对象之间的转换。
这种不一致部分是由于html form自己引起的:它只代表了一种关系模型,不能代表面向对象的模型。面向对象和关系模型的不一致在存储层由对象关系映射(o/rm)解决。但是类似的问题在表示层(view layer)仍然存在。解决的关键是让他们一起无缝地工作。
struts的功能和局限
幸运的是,struts能够生成和解释内嵌的对象属性。category下拉框可以用struts page-construction(html) tag library:
<html:select property="category.id">
<option value="">no category</option>
<html:options collection="categories" property="id" labelproperty="name"/>
</html:select>
我们假设categories是category对象的一个list。所以现在我们要修改productform,让它变得更加“面向对象”,我们要修改productform的categoryid,改成类型为category的category。这种改变会导致在product和productform之间复制属性的工作更加繁琐,因为两者有相同的属性。
public class productform extends actionform {
private long id;
private category category;
...
} 当我们完成剩余的struts action, configuration, validator, jsp, hibernate层后,开始测试,我们马上在访问productform.category.id时遇到了nullpointerexception。这是预料中的!因为productform.category还没有被设置,同时,hibernate也会将多对一所联系的对象引用设为空(如果database field为空指)(译者:这里指hiberate将product.category为null,如果该product没有联系到任何category)。struts要求所有的对象在显示(生成html form)和传播(提交html form)之前被建立。
让我们看看如何用actionform.reset()来架起桥梁。
(并非如此)臭名昭著的struts actionform
在我第一个星期接触struts的时候,我最大的一个疑问就是:为什么我必须为properties, getter方法, setter方法保持几乎完全相同的两份copy, 一份在actionform bean, 一份在domainobject。这个繁琐的步骤成了struts社区最主要的抱怨之一。
以我的观点,actionform存在有原因的。首先,它们可以区别于domain object因为他们但当了不同的角色。在mvc模式下,domain object是model层的一个部分,actionform是view层的。因为webpage的field和database的field可能不一样,某些特制的转换是常见的。第二,actionform.validate()方法可以定义非常好用的验证规则。第三,可能有其他的,特定的view行为,但是又不想在domain layer实现,特别当persistence framework来管理domain object的时候。
提交form
让我们来用actionform内有的方法-reset()-来解决view和model之间的不一致。这个reset()方法是在actionform在被struts controller servlet处理request时候复制actionform属性之前调用的。这个方法最常见的使用是:checkbox必须被显式地设为false,让没有被选中的checkbox被正确识别。reset()也是一个初始化用于view rending对象的合适地方。代码看起来是这样的:
public class productform extends actionform {
private long id;
private category category;
...
public void reset(actionmapping mapping, httpservletrequest request)
{
super.reset( mapping, request );
if ( category == null ) { category = new category(); }
}
} struts在使用用户提交的值填写productform之前,struts会调用reset(),这样category属性将会被初始化。请注意,你必须检查category看它是不是null,后面我们会讨论这个。
编辑form
到目前为止,我们已经解决了form提交时候的问题。但是当我们在生成form页面的时候呢?html:select tag也希望有一个非空的引用,所以我们将在form生成页面之前调用reset()。我们在action类里加入了一行:
public class editproductaction extends action {
public final actionforward execute( actionmapping mapping, actionform form,
httpservletrequest request, httpservletresponse response ) throws exception
{
...
product product = createorloadproduct();
productform productform = (productform)form;
propertyutils.copyproperties( productform, product );
productform.reset( mapping, request );
...
}
} 我假设读者已经对action类和jakarta commons beanutils包非常熟悉了。createorloadproduct()建立了一个新的product实例或者从数据库里载入一个已有的实例,具体取决于这个action是建立或者修改product的。productform被赋值后(译者:也就是调用propertyutils.copyproperties后),productform.category已经从product.category复制过来了(译者:实际上只是复制了category对象引用,并没有开销),然后,productform就能用来生成页面了。我们同时也必须保证:不覆盖已经被hibernate载入的对象,所以我们必须检查(category)是不是为null。
因为reset()方法是在actionform中定义的,我们可以把上述代码放入一个superclass,比如commoneditaction,来处理这些事情:
product product = createorloadproduct();
propertyutils.copyproperties( form, product );
form.reset( mapping, request );
如果你需要一个只读的form, 你有两个选择: 第一检查所联系的jsp对象是不是null, 第二复制domain对象到actionform之后调用reset()
保存domain对象
我们解决了提交form和生成form页面的问题, 所以struts可以满足了。但是hibernate呢?当用户选择了一个null id option – 在我们的例子中“no category”option- 并且提交form, productform.category指向一个新建立的hibernate对象,id为null。当category属性从productform复制到hibernate控制的product对象并且存储时,hibernate会抱怨product.category是一个临时对象,需要在product存储前先被存储。当然,我们知道它是null,并且不需要被存储。所以我们需要将product.category置为null,然后hibernate就能存储product了(译者:在这种情况下,数据库product.category被设成空值)。我们也不希望改变hibernate的工作方式,所以我们选择在复制到domain对象之前清理这些临时对象,我们在productform中加了一个方法:
public class productform extends actionform {
private long id;
private category category;
...
public void reset(actionmapping mapping, httpservletrequest request) {
super.reset( mapping, request );
if ( category == null ) { category = new category(); }
}
public void cleanupemptyobjects() {
if ( category.getid() == null ) { category = null; }
}
}我们在copyproperties之前清理掉这些临时对象,所以如果productform.category只是用来放null的,则将productform.category置为null。然后domain对象的category也会被设成null:
public class saveproductaction extends action {
public final actionforward execute( actionmapping mapping, actionform form,
httpservletrequest request, httpservletresponse response ) throws exception
{
...
product product = new product();
((productform)form).cleanupemptyobjects();
propertyutils.copyproperties( product, form );
saveproduct( product );
...
}
} 一对多关系
我还没有解决category到product的一对多关系。我们把它加入到category的metadata中:
public class category {
...
private set products;
...
/**
* @hibernate.set
* table="product"
* lazy="true"
* outer-join="auto"
* inverse="true"
* cascade="all-delete-orphan"
*
* @hibernate.collection-key
* column="category_id"
*
* @hibernate.collection-one-to-many
* class="product"
*/
public set getproducts() {
return products;
}
public void setproducts(set products) {
this.products = products;
}
} 注意:hibernate的cascade属性为all-delete-orphan表明:hibernate需要在存储包含的category对象时候,自动存储product对象。和parent对象一起存储child对象的情况并不常见,常见的是:分别控制child的存储和parent的存储。在我们的例子中,我们可以容易地做到这一点,如果我们允许用户在同一个html page编辑category和products。用set表示products是非常直观的:
public class categoryform extends actionform {
private set productforms;
...
public void reset(actionmapping mapping, httpservletrequest request) {
super.reset( mapping, request );
for ( int i = 0; i < max_product_num_on_page; i++ ) {
productform productform = new productform();
productform.reset( mapping, request );
productforms.add( productform );
}
}
public void cleanupemptyobjects() {
for ( iterator i = productforms.iterator(); i.hasnext(); ) {
productform productform = (productform) i.next();
productform.cleanupemptyobjects();
}
}
} 更进一步
我们已经可以察看,编辑,提交forms,并且存储相关的objects,但是为所有的actionform类定义cleanupemptyobjects()和reset()方法是个累赘。我们将用一个抽象的actionform来完成协助完成这些工作。
作为通用的实现,我们必须遍历所有的hibernate管理的domain对象,发现他们的identifier,并且测试id值。幸运的是:org.hibernate.metadata包已经有两个utility类能取出domain对象的元数据。我们用classmetadata类检查这个object是不是hibernate管理的。如果是:我们把它们的id value取出来。我们用了jakarta commons beanutils包来协助javabean元数据的操作。
import java.beans.propertydescriptor;
import org.apache.commons.beanutils.propertyutils;
import org.hibernate.metadata.classmetadata;
public abstract class abstractform extends actionform {
public void reset(actionmapping mapping, httpservletrequest request) {
super.reset( mapping, request );
// get propertydescriptor of all bean properties
propertydescriptor descriptors[] =
propertyutils.getpropertydescriptors( this );
for ( int i = 0; i < descriptors.length; i++ ) {
class propclass = descriptors[i].getpropertytype();
classmetadata classmetadata = hibernateutil.getsessionfactory()
.getclassmetadata( propclass );
if ( classmetadata != null ) { // this is a hibernate object
string propname = descriptors[i].getname();
object propvalue = propertyutils.getproperty( this, propname );
// evaluate property, create new instance if it is null
if ( propvalue == null ) {
propertyutils.setproperty( this, propname, propclass.newinstance() );
}
}
}
}
public void cleanupemptyobjects() {
// get propertydescriptor of all bean properties
propertydescriptor descriptors[] =
propertyutils.getpropertydescriptors( this );
for ( int i = 0; i < descriptors.length; i++ ) {
class propclass = descriptors[i].getpropertytype();
classmetadata classmetadata = hibernateutil.getsessionfactory()
.getclassmetadata( propclass );
if ( classmetadata != null ) { // this is a hibernate object
serializable id = classmetadata.getidentifier( this, entitymode.pojo );
// if the object id has not been set, release the object.
// define application specific rules of not-set id here,
// e.g. id == null, id == 0, etc.
if ( id == null ) {
string propname = descriptors[i].getname();
propertyutils.setproperty( this, propname, null );
}
}
}
}
}
为了让代码可读,我们省略了exception的处理代码。
我们的新abstractform类从struts的actionform类继承,并且提供了通用行为:reset和cleanup多对一关联对象。当这个关系是相反的话(也就是一对多关系),那么每个例子将会有所不同,类似在abstract类里实现是比较好的办法。
总结
struts和hibernate是非常流行和强大的框架,他们可以有效地相互合作,并且弥补domain模型和mvc视图(view)之间的差别。这篇文章讨论一个解决struts/hibernate project的通用的方案,并且不需要大量修改已经有的代码。
闽公网安备 35060202000074号