代码改变世界

走进Linq-Linq to SQL源代码赏析 Table的获取过程

2008-08-18 10:02  横刀天笑  阅读(4233)  评论(13编辑  收藏  举报
上一篇我们看到了DataContext是如何初始化的,它需要一个连接对象,还需要一个MappingSource做映射的配置。

DataContext中我们打交道最多的也许就是GetTable<TEntity>()方法了,这个方法会获取一个Table<TEntity>对象,今天我们就来看看这个对象是如何获取的。

 

对于获取Table<TEntity>对象我们还要看看这个DataContext是不是强类型的,关于强类型的DataContext可以看我前面一篇文章,强类型的DataContext里包含有几个Table<TEntity>类型的属性,比如我们的库中有blogsposts等数据库表,那么你可能就会建立Table<Blog>Table<Post>类型的属性(参见前面一篇文章)。在上一章DataContext的初始化里讲到Init方法的最后一行是InitTables方法的调用。我们首先来看看InitTables方法的代码:

/// <summary>
/// 初始化数据库中有几个表
/// 从方法实现中意图来看,这个方法主要在定义了强类型的DataContext才有意义
/// 在强类型的DataContext里一般定义了Table<Post>之类的字段来表示数据库中有几个
/// 表,该方法调用DataContext的GetTable方法设置这些字段的值
/// </summary>
/// <param name="schema"></param>
private void InitTables(object schema)
{
     
//用反射遍历DataContext类(可能是它的子类)里所有的公有实例字段
      foreach (FieldInfo info in schema.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance))
     {
        
//字段类型
         Type fieldType = info.FieldType;
        
//该字段是否是泛型的,并且是Table<>类型的,而且该字段的值为null
        if ((fieldType.IsGenericType && (fieldType.GetGenericTypeDefinition() == typeof(Table<>))) && (((ITable)info.GetValue(schema)) == null))
         {
              
//获取Table<TEntity>中TEntity的具体类型
                Type type = fieldType.GetGenericArguments()[0];
              
//调用DataContext的GetTable方法得到一个ITable对象
                ITable table = this.GetTable(type);
              
//设置值
                info.SetValue(schema, table);
          }
        }
}

代码中的注释说的很详细了,先看看DataContext类里是否有Table<TEntity>的属性,而只有在强类型的DataContext情况下才会有的。所以只有在强类型的情况下才会在初始化DataContext的时候设置这些Table<TEntity>属性的值,从本篇后面的介绍可以看到,获取Table<TEntity>还不是个简单的事情,性能损耗比较大,所以框架默认提供的DataContext比强类型的DataContext更轻型,在上一章中有人问到DataContext是不是轻型的,我觉得如果使用框架提供的是很轻型的,实例化一个没有什么大的性能消耗。下面来看我们经常调用的GetTable方法:

public Table<TEntity> GetTable<TEntity>() where TEntity : class
{
     
this.CheckDispose();
     
//调用MetaModel的GetTable方法获得MetaTable对象
      
//MetaModel代表的是数据库和DataContext之间的映射
      
//而MetaModel代表的是表和对象之间的映射
      MetaTable metaTable = this.services.Model.GetTable(typeof(TEntity));
     
if (metaTable == null)
     {
          
throw Error.TypeIsNotMarkedAsTable(typeof(TEntity));
     }
     
//调用本类的GetTable方法
       ITable table = this.GetTable(metaTable);
      
//关于这里的ITable接口和ElementType属性有更多的讨论
            if (table.ElementType != typeof(TEntity))
            {
                
throw Error.CouldNotGetTableForSubtype(typeof(TEntity), metaTable.RowType.Type);
            }
            
return (Table<TEntity>)table;
        }

        
private ITable GetTable(MetaTable metaTable)
        {
            ITable table;
            
//先查看字典中是否有这个table,该字典是以MetaTable为key,以ITable为value的
            if (!this.tables.TryGetValue(metaTable, out table))
            {
                
//通过检查表之间的关联难验证表的合法性
                ValidateTable(metaTable);
                
//反射ITable对象
                table = (ITable)Activator.CreateInstance(typeof(Table<>).MakeGenericType(new Type[] { metaTable.RowType.Type }), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, nullnew object[] { this, metaTable }, null);
       
//通过反射获取ITable对象后,还将其存储在字典中,可以看到这个字典起一个缓存的作用
        
//以后就可以直接从字典里取了,也就是这个GetTable的过程并不是每次都有反射的性能损耗
                this.tables.Add(metaTable, table);
            }
            
return table;
}

MetaTable metaTable = this.services.Model.GetTable(typeof(TEntity));

这一句是调用MetaModelGetTable方法,MetaModel根据映射的不同有两种实现:AttributeMetaModelMappedMetaModel

得到MetaTable对象后,就可以调用Table<TEntity>的构造函数来获取Table<TEntity>对象了,但是这里却使用的是反射:

table = (ITable)Activator.CreateInstance(typeof(Table<>).MakeGenericType(new Type[] { metaTable.RowType.Type }), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new object[] { this, metaTable }, null);

为什么不直接new呢?原来微软将Table<TEntity>的构造函数设置为私有的了,无法直接使用new调用,

在这里除了构造函数是私有的外,最主要的原因是Table<>是一个泛型类,而这个时候还不知道具体的泛型参数,所以我们只能用泛型了,也就是说即使这里构造函数是私有的,我们也只能用反射构建Table<>的实例。谢谢IVony的提醒。但是这也并不妨碍后面使用的变形的单件模式。

也许是为了防止客户代码任意的创建Table<TEntity>对象,而将构造函数设为私有的吧,也许有人会马上想到,这里应该使用单件模式啊,防止客户端任意创建实例。

 

 

关于设计模式的旁白

 

为什么Table<TEntity>类不使用单件模式?

一个数据库中有几个表,对于每个表对象(Table<TEntity>)我们希望它是单例的,但是系统中并不是只存在一个表对象。在这里微软一方面将表对象的构造函数设为私有的来防止客户端任意的使用new构造表对象的实例,而且没有提供任何公开的接口获取这个实例,另外一方面在DataContext里有一个Dictionary<MetaTable, ITable> tables的字典,用于缓存表对象。

这样就有这样的个示例:

public class Table<TEntity>
    {
        
//私有的构造函数
        private Table()
        { }
    }
    
public class DataContext()
    {
        
private Dictionary<MetaTable, ITable> tables;

        
public DataContext()
        {
            
this.tables = new Dictionary<MetaTable,ITable>();
        }

        
public ITable GetTable(MetaTable metaTable)
        {
            ITable table 
= null;
            
if(!tables.TryGetValue(metaTable,out table)
            {
                
//获取table对象

                
//将刚刚获取的table对象缓存起来,以备后用
                tables.Add(metaTable,table);
            }
            
return table;
        }
}

又是一个不同于传统单件的单件模式,也许叫缓存模式更合适些。

 

关于抽象工厂

打开MetaModel的代码我们会看到,好家伙,有GetFunction方法,GetMetaType方法,GetTable方法,GetFunction方法返回MetaFunctionMetaFunction是一个抽象类,它有AttributeMetaFunctionMappedMetaFunction两个实现,AttributeMetaModel类中的GetFunction方法会返回AttributeMetaFunction,而MappedMetaFunction类里的GetFunction会返回MappedMetaFunction。呵呵,MetaModel好像是抽象工厂,而AttributeMetaModelMappedMetaModel就是具体的工厂,而MetaFunction等都是抽象产品,AttributeMetaFunction都是具体产品:

点击看大图

 

MetaTableMetaFunction等的创建工作交给MetaModel去创建是非常合情合理的,MetaModel表达的是数据库和DataContext之间的映射,数据库当然最清楚它里面有几个表,几个存储过程和用户函数了,而且由于有两种配置映射的方式:Attribute和基于Xml的,那么就有两个不同系列的“产品”,如何能让创建出来的产品都一致(不会造成创建出一个AtttributeMetaFunction和一个MappedMetaTable来)呢?这就是抽象工厂的目的,抽象工厂可以创建一个产品家族,这样保证了产品是一个系列。

 

 

OK,对于DataContext的初始化和Table<TEntity>对象的获取的大致流程已经了解了,下一回我会在更广的层次来说明一下这些方面。主要涉及Provider的初始化。

 

源代码下载,本次源代码在上一篇的基础上又增加了几个涉及的相关类,并加有中文注释,各位可以下载下来在VS里便捷的浏览,注意:是编译不通过的