[转载]AAF灵便应用框架简介系列(5):对象的持久化(高级)
在上一篇文章中,我们已经通过一个更实际的例子对AAF的对象持久化机制有了进一步了解。我们看到,用AAF来实现一个实际应用的业务对象模型确实 是一件非常容易的事情。这篇文章则会换个角度,从用户在实际应用中可能面对的问题出发,给出每种问题的解决办法和思路,以便大家对AAF的灵活性有所了 解。因此,本文的内容将由一个个问题和答案组成。大家在阅读本文时,如果有什么问题需要提出,可以通过留言或者其他方式告诉我,我将会根据大家的反馈不断 补充本文。随着问题的推进,大家会慢慢发现:AAF=灵活+易用。
最近有点其它事情,所以AAF系列文章没有更新。今天我们来逐个回答一下以前提出的关于持久化的若干问题。
1)如何把数据对象映射到一个与其对象类型名不同的表名?
答:在AAF中,默认的数据表名与对象类型名一致,在某些时候,我们需要将数据表名映射到其它数据表。有两种方法来进行数据表映射:a)在类型声明前加 Aaf.Agile.AgileObjectStorageAttribute,通过其属性:TableName进行指定;b)在应用目录下的 Config子目录中的Persistence.Config文件中进行设置,设置项的具体路径为:/Configuration/Assemblies /Assembly[@Name=TypeAssemblyName]/Types/Type[@Name=TypeFullName] /Settings/SettingTableName。两种模式基本类似不同在于:i)AgileObjectStorageAttribute具有继 承性,如果衍生对象的类型没有显示的定义自己的新AgileObjectStorageAttribute属性,AAF内核九将为该衍生对象使用继承的设 置;ii)Persistence.Config中的设置没有这种继承性,所有设置仅针对设置所在类型的实例,不影响衍生类型的实 例;iii)Persistence.Config具有更高的优先级,在同时进行了Persistence.Config和 AgileObjectStorageAttribute设置的情况下,AAF使用Persistence.Config的设置。
2)如何把数据对象的字段映射到一个与其字段名不同的数据表字段?
答:与前一问题类似,也可以通过Attribute设置和配置文件设置两种方式实现将数据对象的字段映射到一个与其(默认的)字段名不同的数据表字段。 Attribute的名称为Aaf.Agile.FieldStorageAttribute,设置属性为:FieldName。在 Persistence.Config文件中进行设置的具体路径为:/Configuration/Assemblies /Assembly[@Name=TypeAssemblyName]/Types/Type[@Name=TypeFullName] /TableName/Members/Member[@Name=FieldName]/Settings /Setting[@Name="FieldName"]。两种方式的优先级及继承性与前一问题中的描述类似。
3)如何确定数据对象字段在数据表中的长度和精度?
答:数据字段在数据库中的长度和精度通过相关的DateTypeAttribute指定。外部常用DateTypeAttribute主要 为:StringDateTypeAttribute和DecimalDateTypeAttribute。在不设定属性时,decimal在数据库中的 存储长度和精度取决于对应数据库数据类型的默认设置(如SQL Server的decimal类型, Oracle的number类型)。在不设定类型时,string类型将被默认映射到32位字符串(具体butes值取决于数据库本身的字符集设置)。通 过DecimalDateTypeAttribute可以改变decimal类型映射的默认设置:DecimalDateTypeAttribute设置 的第一个参数为精度(percision),第二个参数为其取值范围(Scale)。通过StringDateTypeAttribute可以改变 string类型映射的默认设置:StringDateTypeAttribute的有多种初始化方式,常用的一种是:public StringDataTypeAttribute(int maxLength); 通过maxLength可以指定该字符串映射的字符长度。
4)如何解决某几种对象类型存放在A数据库,而其它几种对象类型存放在B数据库的问题?
答:对象类型向具体数据库(可能位于不同服务器)的映射是通过Config子目录下的storage.config文件和 persistence.config文件进行的。在AAF里存在一个概念“存储上下文”。一个存储上下文就定义了一种数据连接的连接方式。在SQL Server中,一个存储上下文可以指定到一个数据库服务器中的一个“数据库”;在Oracle中,一个存储上下文可以指定到一个数据库服务。AAF通过 storage.config文件指定存储上下文;通过persistence定义各对象类型使用的存储上下文名称。在不指定存储上下文名称的默认情况 下,数据对象都将被自动映射到storage.config文件中指定的默认存储上下文。这就要求storage.config中至少有一个 name=”"的StorageContext。通过这种存储上下文定义方式,应用可以无需修改代码根据自身需要指定各数据对象类型的存储上下文。
在需要把某些数据对象指定到非默认存储上下文时,我们可以首先在storage.config文件中增加一个storageContext,为其命名(如 “TradeDB”),并对Provider, DataSource, InitialCatalog, UserID, Password这几个基本属性进行正确的设置,然后在persistence.config中对希望映射到这个新定义上下文的数据对象类型设置其 /Configuration/Assemblies/Assembly[@Name=TypeAssemblyName]/Types /Type[@Name=TypeFullName]/Settings/Setting/StorageContext属性。
以下是一个storage.config文件的典型内容:
<?xml version=”1.0″ encoding=”utf-8″ ?>
<StorageContexts>
<StorageContext name=”">
<DriverClass>Aaf.Storage.Imp.SqlServer.Driver, Aaf.Storage.Imp.SqlServer</DriverClass>
<PersistenceDriver>Aaf.Persistence.Imp.SqlServer.PersistenceDriver, Aaf.Persistence.Imp</PersistenceDriver>
<Provider></Provider>
<DataSource>yourdb</DataSource>
<InitialCatalog>AafSample</InitialCatalog>
<UserID>sa</UserID>
<Password>1111</Password>
<Enabled>true</Enabled>
<MinimunConnections>10</MinimunConnections>
<MaximumConnections>20</MaximumConnections>
<ConnectionTimeout>60</ConnectionTimeout>
<ConnectionReaperDelay>600</ConnectionReaperDelay>
</StorageContext>
<StorageContext name=”TradeDB”>
<DriverClass>Aaf.Storage.Imp.SqlServer.Driver, Aaf.Storage.Imp.SqlServer</DriverClass>
<PersistenceDriver>Aaf.Persistence.Imp.SqlServer.PersistenceDriver, Aaf.Persistence.Imp</PersistenceDriver>
<Provider></Provider>
<DataSource>yourdb</DataSource>
<InitialCatalog>TradeDB</InitialCatalog>
<UserID>sa</UserID>
<Password>12345678</Password>
<Enabled>true</Enabled>
<MinimunConnections>10</MinimunConnections>
<MaximumConnections>20</MaximumConnections>
<ConnectionTimeout>60</ConnectionTimeout>
<ConnectionReaperDelay>600</ConnectionReaperDelay>
</StorageContext>
</StorageContexts>
以下是一个典型的persistence.config:
<?xml version=”1.0″ encoding=”utf-8″?>
<Configuration>
<Assemblies>
<Assembly Name=”Aaf.Core.Imp”>
<Types>
<Type Name=”Aaf.Core.Imp.ConfigurationService”>
<Settings>
<Setting Name=”AddWhenGet”>true</Setting>
</Settings>
</Type>
</Types>
</Assembly>
<Assembly Name=”Aaf.Persistence.Imp”>
<Types>
<Type Name=”Aaf.Persistence.Imp.StorageContextMappingService”>
<Settings>
<Setting Name=”IsDirty”>False</Setting>
<Setting Name=”NeedExecuteScript”>False</Setting>
<Setting Name=”DefaultStorageContext” />
</Settings>
</Type>
<Type Name=”Aaf.Persistence.Imp.Persister”>
<Settings>
<Setting Name=”DealPkgSync” />
<Setting Name=”UseStmtCache” />
<Setting Name=”AoRefreshInterval” />
<Setting Name=”LogNotifications” />
</Settings>
</Type>
</Types>
</Assembly>
<Assembly Name=”XX.Core.Imp”>
<Types>
<Type Name=”XX.Core.Imp.Order”>
<Settings>
<Setting Name=”TableName”>XX_Order</Setting>
<Setting Name=”StorageContext”>TradeDB</Setting>
<Setting Name=”NeedExecuteScript” />
<Setting Name=”AoRefreshInterval” />
<Setting Name=”StorageAliases” />
</Settings>
<Members>
<Member Name=”Sum”>
<Settings>
<Setting Name=”FieldName”>OrderSum</Setting>
</Settings>
</Member>
<Member Name=”UserId”>
<Settings>
<Setting Name=”FieldName”>Order_Uid</Setting>
</Settings>
</Member>
…
</Members>
</Type>
</Types>
<Settings>
<Setting Name=”StorageContext” />
</Settings>
</Assembly>
</Assemblies>
</Configuration>
5)如何解决一个对象类型一部分数据存放在数据库(或数据表)A,一部分存放在数据库(或数据表)B的问题
答:对于这个问题,AAF是通过存储别名予以解决的。在回答上个问题时我们给出的persistence.config文件中,我们看到在 XX.Core.Imp.Order类型的Settings标记内有一个名为“StorageAliases”的Setting标记。当我们需要将一个数 据类型分拆到不同数据库时,我们就需要为其指定多个数据别名,并通过每个数据别名映射到不同的存储上下文。下面是一个存储别名设置的示例:
<Setting Name=”StorageAliases”>Current,History,HistorySlide</Setting>
<Setting Name=”Current.StorageContext”>CurrentTradeDB</Setting>
<Setting Name=”Current.TableName”>Order</Setting>
<Setting Name=”History.StorageContext”>HistoryTradeDB</Setting>
<Setting Name=”History.TableName”>Order</Setting>
<Setting Name=”History.StorageContext”>HistoryTradeDB</Setting>
<Setting Name=”History.TableName”>Order_Slide</Setting>
这样,我们就指定好了一个数据对象类型的多个数据库映射。
默认的,该对象的数据类型将保存到默认上下文中的默认数据表中(不是通过存储别名指定的上下文和数据表),当在加载和存储时指定了存储别名时,数据将根据 该指定被保存到特定别名对应的数据库和表中。需要注意的是:数据对象可以在不同的存储别名上创建,但却不能在不同的别名中移动。试图将一个存储别名中的对 象保存到另一个存储别名时,AAF会抛出异常。AAF建议在两种情况下可以考虑使用存储别名:a)数据分拆后基本不会移动,如果(偶然发生,如用户信息, 可以定期根据活跃度搬移到不同数据库)确需移动,这种移动将在后台进行,移动之后应重启所有Web应用,b)同种数据都在一个存储上下文中被创建,只有状 态完全稳定不再改变(如成功完成或交易取消若干天后的订单)的对象才会被后台(如数据库服务器自动任务)自动搬移到特定别名。这种情况下,Web服务器可 以不必重启继续正常运行。
6)如何干预数据对象保存和加载的过程?
答:如果需要干预对象的保存和加载过程,就需要直接或间接(通过继承AgileObjectEx)实现Aaf.Persistence.IPersistenceParticipant接口。该接口的定义如下:
public interface IPersistenceParticipant
{
// AAF加载该对象之后调用,如果对象已在缓存中,这一方法不会被调用。加载之前该对象还不存在,所以不存在BeforLoad
void AfterLoad();
// AAF保存该对象之前调用,因为该对象可能因没什么实际更新因而根本没有真正提交保存到数据库,因而不存在AfterSave
void BeforeSave();
// 一个对象保存时可以连带保存其它灵便对象(IAgileObject实例)或灵便关系(IAgileRelation实例),无论其本身是否实质更新
object[] GetNeedSaveObjects();
// 一个对象保存时可以连带保存指定为type类型的其它灵便对象(IAgileObject实例)或者灵便关系(IAgileRelation实例)
object[] GetNeedSaveObjects(Type type);
// 如果对象不在缓存中,在加载完对象时,可以加载更多的对象。和AfterLoad有点重复,这有其历史原因。
void LoadAdditional(IPersister persister);
// 如果一个对象有实质更新确需保存时,在保存后可以在同一事务中连带保存其它信息。
void SaveAdditional(ITransaction trans, IPersister persister);
// 对象可以通过一个delegate干预Relation的加载,一般建议不要使用。
CheckRelationLoadableDelegate CheckRelationLoadable { get; }
// 对象可以通过一个delegate干预Relation的保存,一般建议不要使用。
CheckRelationSaveableDelegate CheckRelationSaveable { get; }
// 对象可以通过一个delegate干预Relative的加载,一般建议不要使用。
CheckRelativeLoadableDelegate CheckRelativeLoadable { get; }
// 对象可以通过一个delegate干预Relative的保存,一般建议不要使用。
CheckRelativeSaveableDelegate CheckRelativeSaveable { get; }
// 干预加载,决定所有应加载的Relation/Relative是否可加载,一般应返回Aaf.Persistence.ParticipationGrade.Grant,慎用
ParticipationGrade LoadRelationAndRelative { get; }
// 干预保存,决定所有应保存的Relation/Relative是否可保存,一般应返回Aaf.Persistence.ParticipationGrade.Grant,慎用
ParticipationGrade SaveRelationAndRelative { get; }
}
AgileObjectEx提供了IPersistenceParticipant接口的默认实现。因此,通过继承该类并且只覆盖必要方法的做法是AAF的推荐做法。
7)如何缓存数据对象?
答:AAF底层自动对AgileObject对象进行缓存。AAF会管理所有通过Aaf.Persistence.IPersister服务加载和保存的 对象(未保存的新对象不受管理,也不做缓存,因为他可能随时会被创建者在保存之前丢弃,这意味着通过 IPersister.LoadAgileObject/BatchLoad方法是无法加载到未保存新对象的)。如果没有对config子目录下的 cache.config进行配置的话,AAF会“跟踪”所有加载和保存过的对象,只要GC未将其从内存释放,外部调用者就总能在通过 IPersister.LoadAgileObject快速从从内存中找到该对象。但是,这种对象缓存的存续期是不稳定的,取决于GC。为了确保对象缓存 的存续期,可以通过在cache.config中显式指定特定类型的对象缓存数目,或者在应用本身代码中建立对相关AgileObject的长期引用(如 直接或者间接被类型的Static类型或者当前线程引用的属性,关于如何避免对对象被GC清理的更多信息请参看.Net本身关于GC工作原理的帮助)保证 相关对象的长期或者永久引用。在Cache.Config中指定一种类型对象缓存个数的方法很简单:通过/Configuration /Assemblies/Assembly[@Name=TypeAssemblyName]/Types /Type[@Name=TypeFullName]/Settings/Setting/UseCache和
/Configuration/Assemblies/Assembly[@Name=TypeAssemblyName]/Types/Type[@Name=TypeFullName]/Settings/Setting/CacheSize
这两个设置项进行设定:将前者设定为:true,将后者设定为希望的缓存大小,如100,即可。
这里需要说明的是IPersister.LoadAgileObject/BatchLoad的处理过程有所不同:前者在发现了缓存实例(无论是通过 UseCache/CacheSize设定缓存的,还是内存中尚未被清除的)后,除非调用方要求从数据库强制“刷新”对象,AAF内核将立即返回该对象而 根本不会建立访问数据库的连接更不会执行查询;后者则无论各对象是否已存在与数据库中都会首先建立数据连接并执行批量加载的查询语句,只是在获得查询结果 后,AAF内核才会逐个确认对象是否已加载并根据调用者是否要求从数据库强制“刷新”对象决定是否更新该对象。因此,BatchLoad适用于在应用实例 初始化时进行大量数据(特别是一些数量较多但是增长缓慢的“基础”数据)的快速加载,而不适合于运行日常过程中的对象加载。在日常运行过程中需要同时显示 大量数据列表信息时,为了优化性能,一般不建议将所有数据加载到对象,而应通过DataReader生成页面的方式进行(真正实用的电子商务网站应避免使 用DataSet进行数据绑定生成页面)。AAF通过Aaf.UiHelper.IDataReaderVisualizer提供了一种轻量级但是很好用 的内置缓存的DataReader页面生成机制。在这种机制中,AAF根据可控参数设置进行页面的生成和缓存,并且管理DataReader的分配和释放 以避免内存泄露。应用开发人员只需对每一个视图提供两个回调方法即可。实际上,所有或者部分视图可以共用两个回调方法并通过回调参数区分回调是为哪一种视 图和对象类型(一种对象类型集合可以有多种视图)服务。
8)如何快速加载(例如在启动时)某些基础对象数据?
答:通过Aaf.IPersistence.IPersister的BatchLoad方法能够批量加载基础对象数据,这主要适用于,数据较多但不算海量 (几千到几万),增长缓慢但是却频繁使用的基础数据:如企事业/政府内部员工,商品分类信息(不一定包括具体的商品信息,B2C性质的一般可以将具体商品 信息看作基础数据,C2C性质的则应把具体商品看作非基础数据)等。
9)多应用实例(例如多服务器)部署环境下,如何实现各实例间的业务对象缓存同步?
答:业务对象缓存同步是通过SyncService的部署和配置自动进行的。SyncServie是基于Remoting技术的。因此需要对同步服务端和 使用同步的应用服务端进行端口配置并且确保网络设备和防火墙的设置允许在局域网内部使用同步服务的服务端口。在默认状态下,AAF是不需要同步服务的。当 应用需要时,可以进行端口配置、安装并启动同步服务。默认情况下,同步服务会对所有对象类型进行同步跟踪,对于在内存中存在的对象进行同步。必要时,可以 通过sync.config和syncproxy.config对需要同步的类型及其字段进行限制。
10)如何实现数据对象字符串字段的透明化加密存储?
答:AAF持久化服务提供了简单的字段加密服务,并且通过开放的接口IPersistenceEncryptor允许应用开发人员编写自己的加密解密算 法。开发人员在编写好实现了IPersistenceEncryptor接口的程序集之后,只需要把该程序集复制到相应的目录(windows应用为其应 用所在目录,web应用为其bin目录)并在persistence.config中继续相应设置以便系统能够识别该加密解密器。
11)能不能保证加载的同一Id的同类型业务对象在同一应用实例中唯一?
答:可以保证,对于同一Id的同类型业务对象,无论如何并发加载和反复加载,AAF内核保证得到的对象饮用始终指向同一对象实例。
12)对象列表和对象明细的呈现应采用怎样的策略?
答:这一点其实已经在问题7中进行了回答。对于预加载的基础数据,可以直接使用对象生成UI,虽然IAgileObject也可以使用 DataBinding,但是在性能敏感时不见意这样做;对于显示单个或者通常为单个对象的对象明细界面,可以直接使用对象生成UI,关于 DataBinding的考虑与前面类似,如果能和缓存设置结合往往能达到很好的效果;对于快速增长的事务性数据,则建议通过服务器端分页存储过程进行分 页,在应用代码中通过Aaf.UiHelper.IDataReaderVisualizer提供的服务生成内置缓存的页面,这往往能够达到非常奇妙的效 果。
13)如何决定对象之间的关系是否采用IAgileRelation来实现?
答:如果,一个对象和另一对象的关系数量存在一个基本明确的上限,而且该上限在基础性数据中一般不超过10000,在大量的事务性数据中基本不超过 10~20(如订单中的订单条目明细),通常我们建议采用IAgilRelation建立两种对象间的关系。否则,应在对象之间通过Id建立彼此的关连。 这样可以避免因为建立IAgileRelationer带来的自动加载(当然,存储服务也提供了对只加载对象自身特性的支持)和存储负担。当需要将某个对 象的关系对象加载到对象中(如果只是列表显示,还是建议不加载到对象中而是使用前面提及的IDataReaderVisualizer技术)时,可以通过 Aaf.Persistence.IPersister中具有AgileObjectFilter参数的LoadAgileObject方法进行加载。
14)当对象没有变化时,对其保存会发生什么?
答:什么都不会发生,系统不会建立数据库连接,更不会保存。否则,对于新对象,系统会创建之;对于原有对象,AAF内核会自动只将发生变化的字段更新到数据库。
15)保存一个父对象时会连带保存什么?
答:默认的会保存其所有未被标注为拒绝保存的Relation,默认不会保存相关Relative。可以通过针对的IAgileRelation属性的 RelationStorageAttribute设置(NeedSave)以及relative或者普通字段的 FieldStorageAttribute设置(NeedSave)来决定是否要求保存该关联或者字段。加载过程与之类似,可以通过相应 Attribute的NeedLoad设置来要求自动加载或者拒绝自动加载。加载过程中,默认会进行Relative的加载,这和保存过程有所区别。
16)当在父对象下增加了与一个新子对象的关系项时,怎样保存性能最高?
答:如果只是要保存这一变化,可以通过IPersister.SaveObject(new object[]{parent.SomeAgileRelationWithChild, child})来保存。或者将child定义为衍生自AgileObjectEx的类型,那么只需首先调用 child.AddNeedSaveObject(parent.SomeAgileRelationWithChild);并在需要保存时直接调用 IPersister.SaveAgileObject(child);即可。
17)AgileObjectEx和AgileObject有什么不同?
答:AgileObjectEx在AgileObject基础之上提供了IPersistenceParticipant的基本实现,继承自该类型的对象类型可以对存储、加载过程进行干预。
18)保存一个对象时,哪些数据会一道保存?
答:参考问题15的答案
19)加载一个对象时,哪些数据会一道加载?
答:参考问题15的答案
20)当我想在一个事务中保存多个相互之间没有“关系”或关联的对象(甚至分布在不同的数据库)时,我该怎么做?
答:直接调用IPersister.SaveObject(new object[]{obj1, obj2, relation1, relation2, …});系统会自动进行事务管理。目前在分布式事务的管理上只是提供了一种准分布式事务实现。考虑到微软的分布式事务实现包目前只在windows上可 用,所以AAF内核没有使用相关程序集。根据实际应用的效果,对于日均新增发布单>7000,新增订单>12000,交易成功订 单>5000的较大型在线电子商务系统,这一准分布式事务是非常可靠的,没有因为该实现发生过任何数据一致性问题。
21)当我想在一个事务中,既保存AgileObject对象,又想一并提交某种无法“AgileObject对象化”的改变时,我该怎么做?
答:通过继承AgileObjectEx并重写SaveAdditional方法(或者自行实现IPersistenceParticipant接口)来实现。更多信息可参考问题6)的答案。
我暂时想到这么多,先列在这里。等有空的时候我会一一回答。在此期间,欢迎大家提问。提问就是对我的最大鼓励!