Dorado 9 : 07. 自定义数据对象

在Dorado目前提供的绝大部分示例中,DataProvider和DataResolver处理的都是标准的Java数据对象。即由包含简单Getter/Setter的JavaBean或Map来表述数据的对象。虽然Dorado会通过动态代理技术为这些数据对象添加额外的功能(例如我们在DataResolver中处理提交数据时,可以通过EntityUtils来读取数据的修改状态灯额外的信息),但是它们最终仍然会被伪装成标准的Java数据对象。

在一些特殊的用户场景中,开发者会因为某些特殊的需要设计出一些非标准的自定义数据对象,并且相关的业务逻辑代码也是围绕这些自定义数据对象而展开的。这时,我们要如何在DataProvider和DataResolver中方便的使用这些自定义的数据对象呢?最直接的方法就是编写一个自己的工具类,实现这些自定义数据对象和标准的Java数据对象的互相转换。然而,在我看来这种方法存在一些缺点。。。

  1. 代码变得拖沓——每次返回和处理数据之间都要先调用工具类方法转换数据,让逻辑代码变得繁琐而丑陋。
  2. 运行效率不高——在进行自定义数据对象和标准的Java数据对象的互相转换时需要做一次属性值的迭代复制,这回影响代码的执行效率。此问题在DataProvider中可能会显得更加突出,因为DataProvider往往需要处理较多的数据。

因此,本文接下来介绍的新方法就是要解决上面的两个问题。让开发者不再需要显式的调用转换方法,并且以更加高效的方式实现数据转换。

自定义数据对象

自定义数据对象的设计可能是千变万化的,但是无论怎样设计它都应该可以被理解成是一个包含了1到n个属性的Java对象,尽管这些属性和属性值的载体形式是我们目前无法统一概括的。这里,我们假定了一种很常见的设计形式,它的代码大致如下:

public class MyDataObject {
    private Map<String, Object> map = new HashMap<String, Object>();
 
    public Object getValue(String property) {
        return map.get(property);
    }
 
    public void setValue(String property, Object value) {
        map.put(property, value);
    }
}

MyDataObject是一个可以存储1到n个属性值的自定义数据对象,它通过getValue()和setValue()方法允许外界读写其中的属性值。本文后面的示例,都会围绕MyDataObject来进行讨论。

定义DataType

Dorado中的DataType当初是被设计用来描述各种数据载体的,所以,当然,它也可以用来描述自定义的数据对象。要定义这样一种能够处理自定义数据对象的DataType,我们需要实现com.bstek.dorado.data.type.CustomEntityDataType接口。先来看一下CustomEntityDataType接口的定义。

public interface CustomEntityDataType<T> extends EntityDataType {
    /**
     * 尝试将一个Map转换成本DataType所描述的类型。
     * @param map 要转换的Map。
     * @return 转换后得到的数据。
     */
    T fromMap(Map<String, Object> map) throws Exception;
 
    /**
     * 将一个数据对象转换成Map。
     * @param customEntity 数据对象。
     * @return 转换后得到的Map。
     */
    Map<String, Object> toMap(T customEntity) throws Exception;
}

可见,CustomEntityDataType描述自定义的数据对象的解决方法,就是实现其与Map之间的互转。不过实现这两个方法并不像看起来那么容易,因为你必须在这两个方法中考虑属性值的深度复制问题。而且大部分开发者到这一步想到的实现方法恐怕仍然是通过值拷贝获得Map对象,我们在前面说过了,这是一种效率并不高的转换方式。

为了降低CustomEntityDataType接口的实现难度和应用质量,Dorado为CustomEntityDataType提供了一个不错的抽象类com.bstek.dorado.data.type.GenericCustomEntityDataType,GenericCustomEntityDataType把问题简化成了这样两个抽象方法...

/**
 * 从自定义数据对象中读取一个属性值。
 */
public abstract Object readProperty(T customEntity, String property) throws Exception;
 
/**
 * 向自定义数据对象中写入一个属性值。
 */
public abstract void writeProperty(T customEntity, String property, Object value) throws Exception;

实现这两个方法时,你不必考虑属性值的类型转换问题,只要告诉Dorado如何读写自定义的数据对象中的属性值,剩下的一切都可以交给Dorado来完成。我们为MyDataObject实现的CustomEntityDataType大致如下...

package test;
 
import java.util.Map;
import com.bstek.dorado.data.type.GenericCustomEntityDataType;
 
public class MyDataObjectDataType extends GenericCustomEntityDataType<MyDataObject> {
    @Override
    public Object readProperty(MyDataObject myDataObject, String property)
            throws Exception {
        return myDataObject.getValue(property);
    }
 
    @Override
    public void writeProperty(MyDataObject myDataObject, String property,
            Object value) throws Exception {
        myDataObject.setValue(property, value);
    }
}

上面这段代码足够简单几乎不需要解释什么。


GenericCustomEntityDataType内部为这种场景提供的转换方式是将自定义数据对象适配成标准的Map对象,而不是通过值拷贝获得Map对象,这是一种对性能更加有保障的实现方式。

定义测试View

在View的Model节点下添加一个DataType,name可以随便定义,impl设定为上面我们编写的CustomEntityDataType实现类。再根据需要为它添加几个PropertyDef就可以了。如图...

上面这个例子中的View,除了DataType使用impl属性指定了自己定义的实现类之外,跟普通的View已经没有任何区别了,其他关于DataSet、UpdateAction之类的使用方法此处就不再累述了。

如何让DataType自动创建PropertyDef?

在另一些设计场景中,自定义的数据对象的拥有哪些属性可能是由数据对象类本身而不是XML配置中的PropertyDef决定的。此时,你一定会希望Dorado IDE能够帮你自动生成DataType下面的PropertyDef,要实现这一目标,你需要在MyDataObjectDataType中复写doCreatePropertyDefinitons()方法。例如:

protected void doCreatePropertyDefinitons() throws Exception {
    PropertyDef propDef;
 
    propDef = new BasePropertyDef("name");
    addPropertyDef(propDef);
 
    propDef = new BasePropertyDef("disabled");
    propDef.setDataType(DataUtils.getDataType("boolean"));
    addPropertyDef(propDef);
 
    propDef = new BasePropertyDef("age");
    propDef.setDataType(DataUtils.getDataType("int"));
    addPropertyDef(propDef);
 
    propDef = new BasePropertyDef("birthday");
    propDef.setDataType(DataUtils.getDataType("Date"));
    addPropertyDef(propDef);
}

上面的代码声明了DataType拥有四个PropertyDef。这样,当你在IDE中点击DataType的自动创建PropertyDef功能时,就可以自动生成四个PropertyDef了。

编写DataProvider和DataResolver

下面的代码是我为上面的View编写的DataProvider和DataResolver,其实它跟我们在其他例子中看到的DataProvider和DataResolver也没有任何区别。我们现在已经可以自由的使用MyDataObject来编写自己的逻辑代码了,尽管MyDataObject并不是一种标准的Java数据的载体,但是有了MyDataObjectDataType的帮助,Dorado已经能够顺利的处理它了。

@Component
public class MyDataObjectTest {
    @DataProvider
    public List<MyDataObject> getAll() {
        MyDataObject myDataObject;
        List<MyDataObject> list = new ArrayList<MyDataObject>();
 
        myDataObject = new MyDataObject();
        myDataObject.setValue("p1", "row1-p1");
        myDataObject.setValue("p2", "row1-p2");
        myDataObject.setValue("p3", "row1-p3");
        list.add(myDataObject);
 
        myDataObject = new MyDataObject();
        myDataObject.setValue("p1", "row2-p1");
        myDataObject.setValue("p2", "row2-p2");
        myDataObject.setValue("p3", "row2-p3");
        list.add(myDataObject);
 
        return list;
    }
 
    @DataResolver
    public void save(List<MyDataObject> myDataObjects) {
        for (MyDataObject myDataObject : myDataObjects) {
            System.out.println(myDataObject.getValue("p1"));
        }
    }
}

关于Dorado数据实体特别功能

在实际的使用过程中,你可能很快就会碰到一个问题——如何使用Dorado数据实体中的那些特别功能呢?在使用标准Java数据对象时,我们在DataResolver中得到的数据其实经Dorado包装过的动态代理对象,尽管他们看起来跟简单的数据对象无异,但事实上,这些对象中隐藏了更多的信息和功能。主要有下面的这三种...

  1. state——获取某数据实体在客户端时的编辑状态。另外,当你在DataResolver中改变某个数据实体的状态时,这种状态也会被再同步回Client端。
  2. oldValues——读取某数据实体被修改之前的属性值。
  3. 值回写——当我们在DataResolver中修改某个提交上来的数据对象时,Dorado会自动将这些修改的内容同步回Client端。

可是对于自定义数据对象,Dorado并不会为他们创建动态代理,因为这些对象可能并不支持被动态代理,或者贸然的为它们创建动态代理可能会导致严重的性能问题,所以你会失去上面的这三种功能。现在你有两个选择...

一. 调整设计思路,避免使用这些功能

这些功能虽然听起来很棒,但他们并不是不可或缺的。比如只要我们避免某些界面设计方式,就可以避免Server端的逻辑依赖数据对象的state特性。再比如oldValues,这是一个我原本就不大赞成使用的特性,因为如果你的代码需要依靠oldValues来得出最终的计算结果,那么只要稍稍处理不当,就有可能带来同步修改的逻辑隐患,这种逻辑错误是很隐蔽,很难被发现的。

 

在使用自定义数据对象时,我们通常不应该在UpdateAction中选用!DIRTY_TREE、!CASCADE_DIRTY、[#dirty]这几种提交方式,因为当你一次性的提交了一批包含各种编辑状态的数据之后,你会发现自己在DataResolver中根本没有办法区分它们。

二. 找回原始的数据实体

Dorado在调用DataProvider方法之前会利用MyDataObjectDataType将原始的数据实体对象替换成自定义数据对象。不过那些原始数据实体并没有就此被丢弃掉,只要我们有办法取得某个自定义数据对象对应的原始数据实体,就可以利用它得回上面的三个功能。不过Dorado并不会帮你找回原始数据实体,你需要自己想点办法。

下面的例子利用DoradoContext在MyDataObjectDataType创建每一个自定义数据对象时,保存下自定义数据对象和原始数据实体之间的映射关系,这样我们就可以在后面需要的时候把它们找回来了。

先来编写一个用于处理这种映射关系的工具类(这里的实现方式仅供参考)...

public abstract class MyDataObjectUtils {
    private static final String CONTEXT_KEY = MyDataObjectUtils.class.getName();
 
    @SuppressWarnings("unchecked")
    static void bindEntity(MyDataObject myDataObject, Map<String, Object> entity) {
        Context current = Context.getCurrent();
        Map<MyDataObject, Object> map = ((Map<MyDataObject, Object>) current
                .getAttribute(CONTEXT_KEY));
        if (map == null) {
            map = new HashMap<MyDataObject, Object>();
            current.setAttribute(CONTEXT_KEY, map);
        }
        map.put(myDataObject, entity);
    }
 
    @SuppressWarnings("unchecked")
    public static Map<String, Object> getEntity(MyDataObject myDataObject) {
        Context current = Context.getCurrent();
        Map<MyDataObject, Object> map = ((Map<MyDataObject, Object>) current
                .getAttribute(CONTEXT_KEY));
        return (map == null) ? null : (Map<String, Object>) map
                .get(myDataObject);
    }
}

在MyDataObjectDataType中复写createDataObject()方法,调用刚刚编写的MyDataObjectUtils建立映射关系...

@Override
protected MyDataObject createDataObject(Map<String, Object> map)
        throws Exception {
    MyDataObject myDataObject = super.createDataObject(map);
    MyDataObjectUtils.bindEntity(myDataObject, map);
    return myDataObject;
}

这样,我们就可以在DataResolver中通过MyDataObjectUtils获得原始的数据实体对象了...

@DataResolver
public void save(List<MyDataObject> myDataObjects) {
    for (MyDataObject myDataObject : myDataObjects) {
        Map<String, Object> entity = MyDataObjectUtils
                .getEntity(myDataObject);
        System.out.println(EntityUtils.getState(entity));
    }
}

看到这里,大概一定会有人对这种做法产生质疑——Dorado侵入了我的业务逻辑代码!

可是,等等,你有注意到这个方法体上标注的@DataResolver的Annotation吗?这分明就是一个展现层的方法呀!它的作用是接受客户端提交上来的数据,然后再根据需要调整后端的业务逻辑。Dorado拥有的诸如智能方法适配、数据实体动态代理等特性,让这个方法看起来似乎跟Dorado的关系不是那么密切。于是在这个前提下,对于一些架构不是很复杂且开发者又没有洁癖的项目,业务逻辑代码就被合并到这里来了。其实是我们为了偷懒而省略掉了真正的业务逻辑层。是的,Dorado的示例就是这么干的,因为对大部分项目而言,这样已经足够了。但是,你不能因此而认为这里应该是业务逻辑代码的地盘。无论何时请记得这个方法上有@DataResolver的Annotation,DataProvider方法同样如此。

另外,如果你真的确定自己需要使用上面提到的Dorado数据实体的那三个特别的功能的话,请记住这些功能确确切切是为展现层的逻辑而生的。你在使用这些功能的同时又想着不要让展现层相关的API掺进相应的代码,这种想法本身就是充满矛盾的。

下载本文中提及的完整示例代码 MyDataObjectTest.zip

Attachments:

MyDataObjectTest.zip (application/zip)
custom_datatype_view.png (image/png)