原文出处: http://www.ibm.com/developerworks/cn/java/j-lo-redundant/index.html
在长流程的工作流事务中,实际的业务表单之间存在较多的相同字段,我们在软件设计的时,为了提高查询的效率,相应的会允许表单对应的数据库表存在一定的冗余,这就对表单之间的这些冗余字段的同步带来了挑战。本文介绍了一种配置化管理字段映射并利用 Apache BeanUtils 工具包的实现方案,可以灵活有效的对表单之间的冗余字段进行同步和管理。
------------------------------------------------------------------------------------------------------------
摘要
在长流程的工作流事务中,实际的业务表单之间存在较多的相同字段,我们在软件设计的时,为了提高查询的效率,相应的会允许表单对应的数据库表存在一定的冗余,这就对表单之间的这些冗余字段的同步带来了挑战。本文介绍了一种配置化管理字段映射并利用 Apache BeanUtils 工具包的实现方案,可以灵活有效的对表单之间的冗余字段进行同步和管理。
---------------------------------------------------------------------------------------------------------------------------------------------------------
背景
在基于工作流的应用中,常见的场景就是流转的过程中不断输入信息并生成各种表单。对于同一个流转事项来讲,各种表单间的业务数据的冗余性较高,比如名称、地址、联系电话等可能会在各个表单中都存在。从查询效率和信息追溯的角度考虑,一般在数据库设计时会保留一定的字段冗余度。从在工作流的上下文环境来看,对于一个表单,在新创建时需要自动根据已有信息对其进行初始化,以避免用户重复输入;在保存时需要自动更新其他表单中的相同数据,以保持数据一致。因此,对冗余数据的处理对提升用户体验和保障数据一致性有重要意义。考虑到用户实际业务具有一定的可变性,如何应对变化,有效管理和维护表单间的冗余数据,并兼顾可维护性和可扩展性,这对软件设计和开发人员来讲是一种挑战。
-----------------------------------------------------------------------------------------------------------------------------------------------------
多表单间冗余数据案例分析
这里以一个简单案例来引出本方案。案例描述如下,表单 D 在创建时需要根据表单 A 和表单 B 中的字段进行初始化;当表单 D 在保存的时候,需要同步更新表单 B、表单 C 和表单 E 中对应的冗余字段。其关系图如下图:
表 1. 表单 D 冗余字段关系表
源表单 * 源字段 | 同步类型(操作时机) | 目标表单 * 目标字段 |
---|---|---|
表单 A 字段 A1 | 初始化(创建表单 D 时) | 表单 D 字段 D1 |
表单 B 字段 B2 | 初始化(创建表单 D 时) | 表单 D 字段 D2 |
表单 D 字段 D2 | 同步更新(保存表单 D 时) | 表单 B 字段 B2 |
表单 D 字段 D3 | 同步更新(保存表单 D 时) | 表单 C 字段 C3 |
表单 D 字段 D4 | 同步更新(保存表单 D 时) | 表单 E 字段 E4 |
一般解决方案及存在问题
对于上述实际业务场景,一种直观而简单的方法是采用程序硬编码的方式,如采用应用编程或者数据库触发器来实现,对于表单初始化需要用到的数据,直接从各个源表单的数据字段中读取;在保存当前表单时,直接同步更新各个目标表单相应的数据字段。
图 1. 一种直接实现方案示意图
![图 1. 一种直接实现方案示意图](https://i-blog.csdnimg.cn/blog_migrate/292876601f13519a49cbc9d92274a42e.png)
采用这种方式,其优点是能直接快速的解决问题。不过其缺点也十分明显:
- 强耦合性:每个表单和其他表单之间直接建立关联。
- 代码分散:需要为单独每个表单编写代码,以实现冗余数据的同步。
- 难以维护:随着表单的增加,表单间的关联度呈几何级数增加。另外每次关联关系的变化,都需要修改代码并重新部署。
- 难以扩展:由于代码的分散,如需要对功能进行扩展,需要修改多处代码。
高效自动化解决方案
为了避免上述实现方式带来的缺点,我们进一步分析,由于这类冗余数据的初始化和同步更新操作具备很强的规律性,那么为什么不考虑通过将冗余字段之间的映射关系设计为可配置的,实际操作时自动读取冗余字段映射关系进行数据更新呢?这样,我们可以通过增加冗余数据管理层,便能有效解决该类需求。冗余数据管理层包括冗余数据关联关系管理模块和冗余数据操作模块两部分。
1. 冗余数据关联关系配置管理模块:
将表单间的冗余数据关系采用可配置的方式来动态管理。
2. 冗余数据操作模块:
负责完成对表单的冗余数据的初始化和同步更新操作。冗余数据操作模块会先读取冗余数据关联关系配置管理模块中存放的信息,根据查询条件自动查询到源数据和目标记录,然后对冗余数据自动进行初始化和同步更新操作。
针对上面提出的案例,其应用示意图如下。
图 2. 本方案的实现示意图
![图 2. 本方案的实现示意图](https://i-blog.csdnimg.cn/blog_migrate/36c0ad92fb3c54005951866b13d23fa1.png)
采用此方案,相比一般解决方案有如下优点:
1. 结构清晰:
在层次结构上单独创建一层来管理冗余字段间的关联关系:原有表单间冗余数据的网状关联关系就变成了以冗余数据管理层的为中心的平台式关联关系,表单之间以一种松耦合的方式关联并统一管理。
2. 减少冗余代码:
对冗余数据的初始化和更新操作都通过统一的代码实现。
3. 动态配置管理:
在字段关联关系发生变更,或者导入新表单时,无需修改代码,只需要将其对应的冗余字段关系进行配置并应用。
4. 扩展灵活:
由于所有代码都通过统一的代码实现,对于功能的扩展,只需要修改一处代码。
5. 降低出错的可能性:
由于字段间的关联关系可基于业务进行配置,并可提供 UI 展示和配置界面,减少了应用出错的可能性。
整体设计
本方案采用 Java 实现,对象字段之间的数据同步通过 Java 反射机制来实现,具体主要是通过 Apache 的 BeanUtils 工具包来实现,
说到 BeanUtils 包,很多读者都会比较熟悉,所以这里只是做简单介绍。大多数开发者都习惯用 Java 对象中某一属性的 getter 和 setter 方法来对该属性进行操作,但是在本案例中,属性都是动态配置的故无法事先知道,直接调用 getter 和 setter 方法明显不可行。如何解决根据属性名称从一个 Java 对象中动态获取属性的值,然后将获取到的值设置到另外一个 Java 对象的相应属性中去呢?一个直接的想法是直接利用 Java 的原生的反射 API 来实现,但是这些原生的 API 太复杂而难于理解和直接使用。正好 BeanUtils 工具包提供了对 Java 中的 Reflection 和 Introspection API 的包装,提供了一套简单、易用的 API 来操作 Bean 的属性,如获取对属性列表、获取属性值、设置属性值、在两个对象中拷贝属性值等。所以选择 BeanUtils 工具包也是顺其自然的事情。
在本实现方案中,主要使用到 BeanUtils.getProperty(Object bean,String name) 来从对象 bean 中获取属性的值和 BeanUtils.setProperty (Object bean, String name, Object value) 来对对象中的属性进行赋值。这里要提一下 BeanUtils 的 getProperty 方法和 setProperty 方法其实都会调用 convert 进行转换,内置的 convert 只支持简单的转换,甚至连 Date 类型都不支持。所以我们需要利用 ConvertUtils.register(Convert converter,Class clazz) 方法来注册定制的转换类,以支持不同数据类型的识别和转换 。
设计类图如下。
图 3. 设计类图
![图 3. 设计类图](https://i-blog.csdnimg.cn/blog_migrate/eaa62adf8940413ee02567c65effb6e7.png)
为了更加清晰的说明本方案的实现过程及执行步骤,这里给出了应用执行步骤图,并和相应的 Java 方法进行关联说明。
图 4. 应用执行步骤
![图 4. 应用执行步骤](https://i-blog.csdnimg.cn/blog_migrate/792a2348613c538ba43bdd740a83b22c.png)
实现代码
根据设置方案,详细代码实现分为冗余字段配置管理和冗余数据操作两部分组成,下面将分别进行介绍:
冗余数据关系配置管理模块
对于表单间的冗余数据,从业务上来讲,主要是创建表单时的初始化和保存表单时的同步更新。结合工作流的上下文中进一步分析:
- 对于创建表单时的初始化操作,我们需要明确以下信息:
- 操作时机:在哪个流程环节,创建哪个表单时。
- 冗余字段对应关系:需要对当前表单中的哪些字段进行初始化,而这些字段来源于哪些源表单中的哪些字段。
- 如何获取到源表单数据:用于根据当前表单信息来获取源表单的数据,以便进一步对当前表单初始化。
- 对于保存表单时的同步更新操作,我们需要明确以下信息:
- 操作时机:在哪个流程环节,保存哪个表单时。
- 冗余字段对应关系:保存当前表单时,需要对哪些目标表单中的哪些冗余字段进行同步更新,而这些字段来自于当前表单的哪些字段。
- 如何获取到目标表单数据:用于根据当前表单自动获取目标表单的数据 , 以便进一步对目标表单进行更新。
根据以上的分析,我们首先需要将所有的表单类型进行管理,并且对每个表单中存在的冗余字段进行映射配置,映射配置时包括冗余字段和查询关联字段。
本方案中表单定义 / 注册信息类通过 FormInfo.java 类来展现,并为下一步配置表单字段映射提供基础信息。
清单 1. 表单信息 FormInfo.java
/** 表单 Id */ privateString formId; /** 表单名称 */ private String formName; /** * 表单 java 类,格式 :sample.redundant.form.FormObjectA。 * 用于利用 Java 反射反向创建 FormObject */ private String formClass;
在定义了表单信息之后,接下来就是配置表单间的字段映射。
另外,创建 queryFieldsMap 成员变量来存放用于自动查询源 / 目标表单数据的查询数据,其格式为字段名称 - 源字段名称。这样在获取源/目标表单数据的时候,自动读取源字段的数据作为查询值进行查询。
清单 2. 表单字段映射记录 FormMapping.java
/** 冗余字段更新类型:字段初始化操作 */ public static final String MAPPING_TYPE_INIT= "0"; /** 冗余字段更新类型:字段同步更新操作 */ public static finalString MAPPING_TYPE_UPDATE= "1"; /** 映射标识 */ private String mappingId; /** 源表单对象 */ private FormInfo srcForm; /** 目标表单对象 */ private FormInfo destForm; /** 操作环节 */ private String actionId; /** 操作类型 : 初始化 / 同步更新 */ private String mappingType; /** * 保存表单查询条件 :queryField - valueField 值对。 * 用于根据当前表单的信息查询到映射表单的记录,获取源数据或者需更新的记录 */ private Map<String,String> queryFieldsMap = newHashMap<String,String>(); /** * 字段映射关系 srcField <----> destField */ private Map<String,String> mappingFieldsMap = newHashMap<String,String>(); /** * 添加字段映射 * @paramsrcFieldName 源字段 * @paramdestFieldName 目标字段 */ public void addMappingField(String srcFieldName,String destFieldName) { mappingFieldsMap.put(srcFieldName,destFieldName); } /** * 添加表单查询字段 * @paramqueryFieldName 目标表单的查询字段 * @paramvalueFieldName 源表单的查询值来源字段 */ public void addQueryField(String queryFieldName,String valueFieldName) { queryFieldsMap.put(queryFieldName,valueFieldName); }
在配置好表单映射及字段映射之后,接下来便是加载表单字段映射。这里通过表单字段映射注册管理类 FormMappingMgr.java 来实现。
清单 3. 表单字段映射注册管理类 FormMappingMgr.java
public class FormMappingMgr{ private static List<FormMapping> formMappingList= newArrayList<FormMapping>(); private static void addFormMapping(FormMapping formMapping) { formMappingList.add(formMapping); } /** * 获取当前环节,当前表单,操作类型下的表单字段映射清单 * @paramformId 表单标识 * @paramactionId 环节标识 * @paramtype 操作类型:初始化 / 更新 * @return */ public static List<FormMapping> getMapping(String formId,String actionId,String type) { List<FormMapping> resultMappingList = newArrayList<FormMapping>(); // 遍历查询出符合要求的映射信息 for(FormMapping formMapping : formMappingList) { if(actionId.equals(formMapping.getActionId()) && type.equals(formMapping.getMappingType())) { if(FormMapping.MAPPING_TYPE_INIT.equals(type) &&formMapping.getDestForm().getFormId().equals(formId)) { resultMappingList.add(formMapping); } else if(FormMapping.MAPPING_TYPE_UPDATE.equals(type) && formMapping.getSrcForm().getFormId().equals(formId)) { resultMappingList.add(formMapping); } } } return resultMappingList; } /** * 注册冗余数据映射关系 * 本例中的代码只是作为示意参考 , 此部分的数据可保存在数据库或者配置文件中。 * * 实际应用中,字段的名称可以进一步通过 Java 反射机制动态获取, * 并提供相应的 UI 操作界面,供管理员配置。 */ public static void registerFormMapping() { FormInfo formA = newFormInfo("A","FormObjectA","sample.redundant.form.FormObjectA"); FormInfo formB = newFormInfo("B","FormObjectB","sample.redundant.form.FormObjectB"); FormInfo formC = newFormInfo("C","FormObjectC","sample.redundant.form.FormObjectC"); // 在环节 3,创建 formC 的时候 // 数据来源表单 formB 的查询条件为 workflowInstanceId //formC 的 phone 字段根据 formA 的 phone 字段初始化。(这里只有一个映射字段) FormMapping initFormCFromFormAMapping = newFormMapping ("3",formA,formC,ActionConst.ACTION_THREE,FormMapping.MAPPING_TYPE_INIT); initFormCFromFormAMapping.addQueryField("workflowInstanceId", "workflowInstanceId"); initFormCFromFormAMapping.addMappingField("phone", "phone"); FormMappingMgr.addFormMapping(initFormCFromFormAMapping); // 在环节 3,创建 formC 的时候 // 数据来源表单 formB 的查询条件为 workflowInstanceId //formC 的 address 字段根据 formB 的 address 字段初始化 FormMapping initFormCFromFormBMapping = newFormMapping ("4",formB,formC,ActionConst.ACTION_THREE,FormMapping.MAPPING_TYPE_INIT); initFormCFromFormBMapping.addQueryField("workflowInstanceId", "workflowInstanceId"); initFormCFromFormBMapping.addMappingField("address", "address"); FormMappingMgr.addFormMapping(initFormCFromFormBMapping); // 在环节 3,保存 formC 的时候 // 目标表单 formA 的查询条件为 workflowInstanceId // 根据 formC 的 address 字段同步更新 formA 的 address 字段 FormMapping updateFormCToFormAMapping = newFormMapping ("5",formC,formA,ActionConst.ACTION_THREE,FormMapping.MAPPING_TYPE_UPDATE); updateFormCToFormAMapping.addQueryField("workflowInstanceId", "workflowInstanceId"); updateFormCToFormAMapping.addMappingField("address", "address"); FormMappingMgr.addFormMapping(updateFormCToFormAMapping); // 在环节 3,保存 formC 的时候 // 目标表单 formB 的查询条件为 workflowInstanceId // 根据 formC 的 phone 字段同步更新 formB 的 phone 字段 FormMapping updateFormCToFormBMapping = newFormMapping ("6",formC,formB,ActionConst.ACTION_THREE,FormMapping.MAPPING_TYPE_UPDATE); updateFormCToFormBMapping.addQueryField("workflowInstanceId", "workflowInstanceId"); updateFormCToFormBMapping.addMappingField("phone", "phone"); FormMappingMgr.addFormMapping(updateFormCToFormBMapping); } }
冗余数据操作模块实现
对冗余数据的更新,通过冗余数据同步管理类 RedundantDataUpdateMgr.java 来完成,技术上主由 Java 反射机制实现。具体实现为:根据所在环节信息,读取当前表单配置的字段映射信息,根据操作类型操作冗余字段。
- 新建表单的初始化:需要根据当前信息自动找到源数据对新建表单进行初始化,见 initRedundantData() 方法。
- 已有表单的同步更新:需要根据当前信息自动找到目标记录对目标字段进行同步更新,见 updateRedundantData() 方法。
- 根据配置查询条件信息,自动查询源 / 目标表单:参见 queryObjects() 方法。
- porpulateFields() 方法:根据字段的映射关系,调用 copyProperty() 方法遍历更新所有字段。
- copyProperty() 方法:通过 BeanUtils 对单个字段更新。
清单 4. 冗余数据同步管理类 RedundantDataUpdateMgr.java
/** * 初始化表单冗余数据 */ public Object initRedundantData(String formId,String actionId,Object destObject) { List<FormMapping> formMappingList = FormMappingMgr.getMapping(formId,actionId,FormMapping.MAPPING_TYPE_INIT); for(FormMapping formMapping : formMappingList) { List objectList = queryObjects(destObject,formMapping); for(Object srcObject : objectList) { porpulateFields(formMapping,srcObject,destObject); } } return destObject; } /** * 同步更新表单冗余数据 */ public void updateRedundantData(String formId, String actionId,Object srcObject) { List<FormMapping> formMappingList = FormMappingMgr.getMapping(formId, actionId, FormMapping.MAPPING_TYPE_UPDATE); for(FormMapping formMapping : formMappingList) { List objectList = queryObjects(srcObject,formMapping); for(Object destObject : objectList) { porpulateFields(formMapping,srcObject,destObject); MockDAOService.update(destObject); } } } /** * 根据和当前的 formObject 和关联关系,查找对应的数据对象 * 1.init: 查找数据来源对象 * 2.update: 查找更新目标对象 */ private List queryObjects(Object object,FormMapping formMapping) { Object queryExampleObject = null; if(FormMapping.MAPPING_TYPE_INIT.equals(formMapping.getMappingType())) { queryExampleObject = formMapping.getSrcForm().createFormObject(); } else if(FormMapping.MAPPING_TYPE_UPDATE.equals(formMapping.getMappingType())) { queryExampleObject = formMapping.getDestForm().createFormObject(); } else{ throw new RuntimeException("映射类型错误!"); } Set<String> queryFields = formMapping.getQueryFields(); for(String queryField : queryFields) { String dataField = formMapping.getQueryDataField(queryField); copyProperty(object,dataField,queryExampleObject,queryField); } return MockDAOService.findByExmaple(queryExampleObject); } /** * 对映射字段赋值 * 通过 Java 反射机制来实现对字段的自动赋值 */ private void porpulateFields (FormMapping formMapping,Object srcObject,Object destObject) { Set<String> srcFields = formMapping.getSrcFields(); for(String srcField : srcFields ) { String destField = formMapping.getDestField(srcField); copyProperty(srcObject,srcField,destObject,destField); } } /** * 对单个字段赋值 */ public void copyProperty (Object srcObject,String srcField,Object destObject,String destField)throws Exception { BeanUtilsBean.setInstance(newBeanUtilsBean2()); DateConverter dateConverter = newDateConverter(); dateConverter.setPattern("yyyy-MM-dd HH:mm:ss"); ConvertUtils.register(dateConverter, Date.class); BeanUtils.setProperty(destObject, destField, BeanUtils.getProperty(srcObject, srcField)); }
表单操作 Action 类分为抽象类和具体实现类。在抽象类中对创建和保存表单对象进行封装,定义两个模板方法 create() 和 save(),在调用子类的 createObject() 和 saveObject() 方法时,还另外根据关联关系同步冗余数据。具体在需要初始化时调用方法 redundantDataUpdateMgr. initRedundantData(),在需要同步更新时调用 redundantDataUpdateMgr. updateRedundantData()。
清单 5. 表单操作抽象类 AbstractFormAction.java
/** * 冗余字段操作管理类实例 */ private RedundantDataUpdateMgr redundantDataUpdateMgr = RedundantDataUpdateMgr.getInstance(); /** * 表单创建操作,包含自动初始化表单数据操作 */ public Object create(Map paramMap) { Object object = createObject(paramMap); // 根据字段映射配置关系自动对新创建的对象进行初始化 return redundantDataUpdateMgr.initRedundantData(formId,actionId,object); } /** * 表单保存操作,包含自动同步表单数据操作 */ public void save(Object object) { object = this.saveObject(object); // 根据字段映射配置关系自动对新创建的对象进行更新 redundantDataUpdateMgr.updateRedundantData(formId,actionId,object); } /** * 抽象方法,由子类具体实现 */ protected abstract Object createObject(Map paramMap); /** * 抽象方法,由子类具体实现 */ protected abstract Object saveObject(Object object);
清单 6. 表单操作实现类 FormAAction.java
public class FormAAction extendsAbstractFormAction{ @Override protected Object createObject(Map paramMap) { // TODOAuto-generated method stub FormObjectA formObject = newFormObjectA(); formObject.setId("A"); String workflowInstanceId = (String)paramMap.get(AbstractFormAction.KEY_WORKFLOW_INSTANCE_ID); formObject.setWorkflowInstanceId(workflowInstanceId); return formObject; } @Override protected Object saveObject(Object formObject) { // TODOAuto-generated method stub MockDAOService.add(formObject); return formObject; } }
在最终使用时只需要通过 FormAction 子类,调用 AbstractFormAction 类中的 create() 方法和 save() 方法,就可在创建和保存表单对象的时候,自动同步冗余字段。
总结
本文介绍了一种可应用在多表单场景下的冗余数据同步的解决方案,通过配置化管理来降低编程复杂度,提高可扩展性,并且对冗余字段的同步统一管理,降低耦合性,并且易于维护。长流程工作流审批流程是本方案的一个典型应用场景,本方案也是一个通用的冗余字段数据管理和维护的解决方案,只要是一个应用的设计上存在冗余字段,而在应用运行的过程中需要对这些冗余字段进行同步初始化和更新操作,都可运用本方案。
下载
描述 | 名字 | 大小 |
---|---|---|
代码示例 | source_code.zip | 14k |