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脚本。