LLBL Gen 实体映射工具技术原理剖析
应用LLBL Gen作为ORM工具时,经常会遇到想查一个实体所代表的数据库表名,或是想已知一个数据库表名,想知道它在程序中对应的实体名,这两者之间相互查找,这个需求经常会碰到。
前一种需求产生于,系统报错时,会显示调用的堆栈和错误信息,依据最后一层堆栈提供的对象参数,可以查到表名,以此追查数据为什么会出错。
后一种需求,常常想知道业务逻辑算法。比如单价的计算方法,总金额的计算方法,因此需要从数据库中追踪到实体表及其属性,再跟踪属性之间的运算逻辑。
第一种解决方案,可以修改LLBL Gen生成的源代码的模板,把表名生成到实体映射文件的注释中,修改模板
- entityAdapter.template #31行
///Mapped on table: <[ElementTargetObjectName]></summary> - 这种方案,当鼠标放到实体的名称上时,会显示它映射的表名。
- 继续修改LLBL Gen模板,让它能显示出属性对应的数据库字段。
- entityIncludeAdatper.template #549行
///Mapped on <[ CaseCamel TargetType ]> field: <[SourceObjectName]>.<[SourceColumnName]></summary>
- entityIncludeAdatper.template #549行
第二种方案,解析LLBL Gen生成的源项目文件,它的存储格式如下
internal class PersistenceInfoProviderCore : PersistenceInfoProviderBase { /// <summary>Initializes a new instance of the <see cref="PersistenceInfoProviderCore"/> class.</summary> internal PersistenceInfoProviderCore() { Init(); } /// <summary>Method which initializes the internal datastores with the structure of hierarchical types.</summary> private void Init() { this.InitClass((158 + 0)); InitAccountEntityMappings(); } }
具体的每个实体的映射
/// <summary>Inits AccountEntity's mappings</summary> private void InitAccountEntityMappings() { this.AddElementMapping( "AccountEntity", "MIS", @"dbo", "Account", 58 ); this.AddElementFieldMapping( "AccountEntity", "AcctName", "ACCT_NAME", false, "NVarChar", 50, 0, 0, false, "", null, typeof(System.String), 0 ); this.AddElementFieldMapping( "AccountEntity", "AcctNo", "ACCT_NO", false, "NVarChar", 30, 0, 0, false, "", null, typeof(System.String), 1 ); }
分析这个结果的数据存放格式和地方,不过有些遗憾。目前我只能做到,根据实体生成SQL语句,也就是框架的做法,对于根据SQL表名或是字段名称,得到它对应的实体名称或是属性名,这个还没有做到。
第三种技术方案,也就是这里的方案,主动的解析LLBL Gen的设计器项目文件,Framework.llblgenproj
有三个网格,第一个是显示表名和对应的实体名称,下面的一个,显示这个表的主键,右边的则显示表的所有字段,如果字段是主键,则用阴影背景显示。
解析Framework.llblgenproj项目文件,它是Xml文件,首推Linq to xml技术。
首先找到表与实体的映射关联。EntityMappings节点下面的内容,是表与实体映射内容
EntityName=":Attachment" TargetName="Framework:dbo:Attachment"
这一层最容易查找,于是填充第一个网格。
其次找字段,在节点EntityDefinitions下面,分解它的源代码如下所示
foreach (XElement field in item.Elements("Fields").Elements()) { EntityMappingClass mappingClass = new EntityMappingClass() { FieldIndex = Convert.ToInt16(field.Attribute("FieldIndex").Value), Name = Convert.ToString(field.Attribute("Name").Value), Type = Convert.ToString(field.Attribute("Type").Value) }; if (field.Attribute("MaxLength")!=null) mappingClass.MaxLength = Convert.ToDecimal(field.Attribute("MaxLength").Value); if (field.Attribute("Precision")!=null) mappingClass.Precision = Convert.ToInt16(field.Attribute("Precision").Value); if (field.Attribute("IsOptional")!=null) mappingClass.IsOptional = Convert.ToBoolean(field.Attribute("IsOptional").Value); fields.Add(mappingClass); }
对于不一定会存在的节点属性(attribute),要行判断是否存在。
经过上面一层的查找,已经找到实体及其所有的属性,还需要找实体的属性对应的数据库字段,于是继续分析EntityMappings节点。
foreach (XElement field in entity.Elements().First().Elements()) { //<FieldMapping FieldName="AttmFile" TargetFieldName="File" var items = (from item in targetfields where item.Name == field.Attribute("FieldName").Value select item).First(); items.TargetFieldName = field.Attribute("TargetFieldName").Value; }
如上代码所示,找到属性映射的字段值之后,更新它所映射的字段名称。
到此,第三个网格,显示实体的属性,已经完成。这里再加点功能,给是主键的字段背景颜色,以方便识别。
于是,继续查找节点TargetDatabases,它的子节点中有存储字段是否为主键的信息。
foreach (XElement field in xElement.Elements().First().Elements()) { IsPrimaryKey = false; //<Field Name="Recnum" Ordinal="1" IsPrimaryKey="true" IsIdentity="true" DbType="5" Precision="8" /> if (field.Attribute("IsPrimaryKey") != null) IsPrimaryKey = Convert.ToBoolean(field.Attribute("IsPrimaryKey").Value); var items = (from item in targetfields where item.TargetFieldName == field.Attribute("Name").Value select item).First(); items.IsPrimarykKey = IsPrimaryKey; }
于是,第二个网格的数据已经找到。
至此,我已经完整的设计出Entity Mapping功能:根据实体名找出数据库表名,根据表名称,找出对应的程序中的实体名称,二者相互查找。
Linq技术读取Xml文件确实相当方便,但是不可以用XPath功能,不可以从根节点直接定位到某一层的子节点,Linq to Xml只能一层层的钻取,First/Last然后Elements,再钻取。有时候为了验证代码的正确性,常常借助于Visual Studio的Debugger来写代码,在Immediate Window中输入预想的代码,让它来验证代码是否正确。