LLBL Gen 元数据编程 LLBL Gen Meta-data Programming

LLBL Gen作为ORM工具,有时候为了能生成一些基础的元数据,也需要了解它的对象及其之前的关系,这在通用的框架代码中的作用更加明显。举例说明,它生成的解决方案视图一般是这样的

image

现在有如下的需求需要满足,以提供基础的元数据,参考测试代码如下

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 如何获取实体的子集合的关系

 

项目名称空间

image

这里要获取的就是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

image

这个类型在生成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脚本。

posted @ 2011-12-27 09:30  信息化建设  阅读(1831)  评论(0编辑  收藏  举报