LLBL Gen 元数据编程 LLBL Gen Meta-data Programming
LLBL Gen作为ORM工具,有时候为了能生成一些基础的元数据,也需要了解它的对象及其之前的关系,这在通用的框架代码中的作用更加明显。举例说明,它生成的解决方案视图一般是这样的
现在有如下的需求需要满足,以提供基础的元数据,参考测试代码如下
string AssemblyFile = @"E:\Solution\Enterprise\Bin\Northwind.CRM.BusinessLogic.dll"; string TableName = "Employees"; string projectName = EntityClassHelper.PrefixProjectName(AssemblyFile); string entity = EntityClassHelper.GetEntityName(TableName, AssemblyFile); string entityName = "EmployeeEntity"; string str = EntityClassHelper.TrimEntityName(entityName); IEntity2 currencyEntity = EntityClassHelper.GetEntityObject(TableName, AssemblyFile); string typeName = "EmployeeEntity"; string table = EntityClassHelper.GetSourceTableName(typeName, AssemblyFile); string entityColumnName = EntityClassHelper.GetObjectProperyName(AssemblyFile, TableName, "EmployeeId"); entityColumnName = EntityClassHelper.GetObjectProperyName(AssemblyFile, TableName, "LastName"); List<string> path = EntityClassHelper.GetPrefetchPath(AssemblyFile, TableName); IEntity2 entity2 = EntityClassHelper.GetEntityObject(TableName, AssemblyFile); List<string> relations = EntityClassHelper.GetPrefetchPathEx(entity2);
需求列出如下图表所示
序号 | 需求描述 |
1 | 如何获取LLBL Gen代码生成器生成的项目名称,这可以确定类型所在的命名空间 |
2 | 如何根据数据库表名(Customers),获取它对应的生成的实体名(CustomerEntity) |
3 | 如何根据实体名(CustomerEntity),获取它映射到的数据库表名(Customers) |
4 | 如何根据实体名,获取它的主键属性/字段 |
5 | 如何根据表名及指定的字段表,获取对应的生成的实体的属性 |
6 | 如何获取实体的子集合的关系 |
项目名称空间
这里要获取的就是Root namespace,顶层的命名空间,依照这个名称,从而可以得到实体所在的名称空间。
LLBL Gen从3.x开始,项目文件改用xml配置文件,这为大量的第三方工具的产生提供的便利。如果要获取上图所示的Root namespace,可以采用Xml技术(XmlDocument)或Linq to Xml读取它的配置节
<CodeGenerationCyclePreferences> <OutputType Value="3"> <LastUsedPreferences> <DestinationRootFolder Value="E:\Solution\Development\MIS Solution\Source\Enterprise\BusinessLogic" /> <FrameworkName Value="LLBLGen Pro Runtime Framework" /> <LanguageName Value="C#" /> <PlatformName Value=".NET 3.5" /> <PresetName Value="Northwind" /> <RootNamespace Value="Foundation.Northwind" /> <TemplateGroup Value="Adapter" /> <TemplateBindings> <Binding Name="ISL Template" /> <Binding Name="SD.AdditionalTemplates.DbEditor.Net2.0 v3.x" /> <Binding Name="SD.TemplateBindings.SharedTemplates.NET35" /> <Binding Name="SD.TemplateBindings.SqlServerSpecific.NET20" /> <Binding Name="SD.TemplateBindings.SharedTemplates.NET20" /> <Binding Name="SD.TemplateBindings.General" /> </TemplateBindings> </LastUsedPreferences> </OutputType> </CodeGenerationCyclePreferences>
如上面的Xml文本所示,RootNamespace就是我们需要的顶层命名空间。
观察LLBL Gen生成的项目文件,它的实体定义的代码所下的例子所示
using SD.LLBLGen.Pro.ORMSupportClasses; namespace Northwind.DAL.EntityClasses { // __LLBLGENPRO_USER_CODE_REGION_START AdditionalNamespaces // __LLBLGENPRO_USER_CODE_REGION_END /// <summary>Entity class which represents the entity 'Category'.<br/><br/></summary> [Serializable] public partial class CategoryEntity : CommonEntityBase // __LLBLGENPRO_USER_CODE_REGION_START AdditionalInterfaces // __LLBLGENPRO_USER_CODE_REGION_END { } }
反射生成的解决方案文件类库,获取它的类型,如果是IEntity2(Adapter模式),去掉必须的EntityClasses,前面的部分即是我们需要的顶层(Root namespace)命名空间,实现代码如下所示
Assembly assebly=Assembly.Load(businessLogic); Type[] types = assembly.GetTypes(); string rootNamespace = ""; foreach (Type type in types) { if (!string.IsNullOrEmpty(type.Namespace)) { string nspace = type.Namespace.Substring(type.Namespace.LastIndexOf('.') + 1); if (type.Name.EndsWith("Entity") & nspace == "EntityClasses") { rootNamespace = type.Namespace; int idx = rootNamespace .LastIndexOf("."); rootNamespace = rootNamespace .Substring(0, idx); break; } }
}
变量rootNamespace就是我需要的顶层命名空间。
数据库表与它生成的实体对象名
来观察一下LLBL Gen提供的数据访问接口类型DataAccessAdapter,这提供一个保护的方法成员GetFieldPersistenceInfos,使用Reflector得到它的源代码跟踪进去,看到有数个方法
// Summary: Retrieves the persistence info for the field passed in. // Parameters: // field: Field which fieldpersistence info has to be retrieved // Returns:the requested persistence information protected virtual IFieldPersistenceInfo GetFieldPersistenceInfo(IEntityField2 field); // Summary: Retrieves the persistence info objects for the fields of the entity passed in. // Parameters: entity: // Entity bject which fields the persistence information should be retrieved for // Returns: the requested persistence information
protected virtual IFieldPersistenceInfo[] GetFieldPersistenceInfos(IEntity2 entity); // Summary: Retrieves the persistence info for the fields passed in. // Parameters: fields: Fields for which the persistence info has to be determined // Returns: the requested persistence information protected virtual IFieldPersistenceInfo[] GetFieldPersistenceInfos(IEntityFields2 fields); // Summary: Retrieves the persistence info objects for the fields of the entity passe in. // Parameters: entityName: //Entity name for entity type which fields the persistence information should be retrieved for // Returns: the requested persistence information protected virtual IFieldPersistenceInfo[] GetFieldPersistenceInfos(string entityName);
IFieldPersistenceInfo就是实体属性对应的数据库字段的映射类型,不过这几个方法都是保护类型的,需要在派生类中访问,如下的代码所示
namespace Northwind.DAL { public sealed class DataAccessAdapter : Northwind.DAL.DatabaseSpecific.DataAccessAdapter { public string GetSourceTableName(string entityType) { return base.GetFieldPersistenceInfos(entityType)[0].SourceObjectName; } }
因为同一个实体对应的表,表中的所有字段所属的表名肯定是相同的,所以直接取它的第零个字段的SourceObjectName。这样,我们就做到了根据实体的名称,来获取它应对的数据库表名称。在我的Management Console开发工具中,没有直接从DatabaseSpecific.DataAccessAdapter类型派生,这样的方式会产生很多麻烦。每新建一个项目就要派生一个类型出来,这样不符合工具的含义,观察一下生成的文件PersistenceInfoProvider
internal static class PersistenceInfoProviderSingleton { #region Class Member Declarations private static readonly IPersistenceInfoProvider _providerInstance = new PersistenceInfoProviderCore(); #endregion /// <summary>Dummy static constructor to make sure threadsafe initialization is performed.</summary> static PersistenceInfoProviderSingleton() { } /// <summary>Gets the singleton instance of the PersistenceInfoProviderCore</summary> /// <returns>Instance of the PersistenceInfoProvider.</returns> public static IPersistenceInfoProvider GetInstance() { return _providerInstance; } } }
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((13 + 2)); InitCategoryEntityMappings(); InitCustomerEntityMappings(); }
}
这两个类型暴露了IPersistenceInfoProvider 给外部类型库来获取它的映射关系,这一点我是通过分析LLBL Gen ORM Support Classes得到的。因为LLBL Gen框架要自动生成SQL语句,必然需要一种机制或是映射来保存这种数据库表和实体的映射关系,NHibernate是使用外部xml配置文件的方式,LLBL Gen则直接使有代码,并且代码直接由生成工具维护,不需要开发人员参与,这一点应该比NHibernate更加合理优秀一些。来看一下经过反射后的得到的代码
public abstract class PersistenceInfoProviderBase : IPersistenceInfoProvider { protected PersistenceInfoProviderBase(); protected void AddElementFieldMapping(string elementName, string elementFieldName, string sourceColumnName, bool isSourceColumnNullable, string sourceColumnDbType, int sourceColumnMaxLength, byte sourceColumnScale, byte sourceColumnPrecision, bool isIdentity, string identityValueSequenceName, TypeConverter typeConverterToUse, Type actualDotNetType, int fieldIndex); protected void AddElementMapping(string elementName, string catalogName, string schemaName, string targetName, int numberOfFields); public IFieldPersistenceInfo[] GetAllFieldPersistenceInfos(IEntity2 entity); public IFieldPersistenceInfo[] GetAllFieldPersistenceInfos(string elementName); public IFieldPersistenceInfo GetFieldPersistenceInfo(string elementName, string fieldName); protected void InitClass(int capacity); }
方法AddElementMapping和AddElementFieldMapping添加表及其字段与实体的映射到内部集合中,并通过GetAllFieldPersistenceInfos接口向外公布映射数据。这就解释了方法GetSourceTableName的原理。
Management Console因为要适应新项目的需要,所以不能直接用派生的方式,直接用反射来获取元数据信息。
1 反射项目root namespace名称,得到DatabaseSpecific.PersistenceInfoProviderSingleton类型,反射它的GetInstance()方法得到IPersistenceInfoProvider
2 传入需要的参数,得到IFieldPersistenceInfo类型,观察它的属性,可以看到SourceObjectName,其它的属性如下
public interface IFieldPersistenceInfo { Type ActualDotNetType { get; } string IdentityValueSequenceName { get; } bool IsIdentity { get; } string SourceCatalogName { get; } string SourceColumnDbType { get; } bool SourceColumnIsNullable { get; } int SourceColumnMaxLength { get; } string SourceColumnName { get; } byte SourceColumnPrecision { get; } byte SourceColumnScale { get; } string SourceObjectName { get; } string SourceSchemaName { get; } TypeConverter TypeConverterToUse { get; } }
这些属性的含义,就代表了数据库字段的内存映射,于DataTable不同。DataTable是装载数据,而IFieldPersistenceInfo是装载字段的元数据,也就是下图中的Column Properties
这个类型在生成SQL语句方面有重要的作用,你可以通过查看它的源代码(反射得到,没有加密)看到它的重要作用。
讲到这里,所有的元数据的信息都可以通过上面的接口和方法获取到。
SQL Trace
如果对LLBL Gen生成的SQL语句感兴趣,可以通过Trace机制来跟踪它的SQL输出,配置过程如下所示。修改配置文件
<system.diagnostics> <!-- LLBLGen Trace Trace Level: 0 - Disabled 3 - Info 4 - Verbose <switches> <add name="SqlServerDQE" value="0" /> <add name="ORMGeneral" value="0" /> <add name="ORMStateManagement" value="0" /> <add name="ORMPersistenceExecution" value="0" /> </switches> <trace autoflush="true"> <listeners> <add name="textWriterTraceListener" type="System.Diagnostics.TextWriterTraceListener" initializeData="D:\\erp solution\esolution_log.txt" /> </listeners> </trace> --> </system.diagnostics>
这样配置,可以将LLBL Gen生成的SQL语句输出到文本文件中,不过这种方式不适合即时查看。需要重写一个TraceListener来截获它的SQL输出。
public class ORMTraceListener : TraceListener { static Socket m_socClient; //在可以连接的情况下,才能保证去发送消息 static bool m_serverAvailable=false; static ORMTraceListener() { m_server = false; OnConnect("127.0.0.1", 2907); } public override void Write(string message) { if (!String.IsNullOrEmpty(message) && m_server&&NeedSend(message)) OnSendData(message); } public override void WriteLine(string message) { if (!String.IsNullOrEmpty(message) && m_server && NeedSend(message)) OnSendData(message+Environment.NewLine); } }
这里应用Socket把截取的SQL语句输出到我的监控程序中,修改上面的type为ORMTraceListener 即可。
截取的SQL语句不是标准的SQL Server语句。原因是LLBL Gen是跨数据库平台的,独立于数据库方言,所以要用一种公共的方法来描述生成的SQL语句,这也提供了一种通用SQL语句生成的方法(学技术的同时,也看到了技术的最佳实践,这在以后的工作中会提供相当大的帮助)。SQL语句格式如下所示
Generated Sql query: Query: SELECT DISTINCT [Enterprise].[dbo].[Company].[CompanyCode] FROM [Enterprise].[dbo].[Company]
WHERE ( ( [Enterprise].[dbo].[Company].[Suspended] = @Suspended1)) ORDER BY
[Enterprise].[dbo].[Company].[CompanyCode] ASC Parameter: @Suspended1 : String. Length: 1. Precision: 0. Scale: 0. Direction: Input. Value: "N".
在此基础上,再创建一个解析工具,把这里的SQL解析成可以直接在SQL Server中运行的T-SQL脚本。