天策大雷:C#中的延时加载

来源:百度文库 编辑:偶看新闻 时间:2024/05/04 04:59:22
很久以前就听过“延时加载”这个东西,不过没有理解是什么意思,现在算是了解一二了,写点文章作为读书笔记,把自己的想法记录一下,希望对初学者帮助,不管是初学者或者高手如果发现文章那里写得不好或者有更好的思路和做法记得告诉我哦^^。文章打算写成两三篇,这个是第一篇。
在三层结构中我们通常会使用多一个叫做“模型层”的东西,这一层中最主要做的事情是把数据库中的表 (或者其他数据源,例如xml或者自己定义的一种数据格式)转成对应的类,例如有一个文章表,这时候在这一层就会有一个文章类;文章类的属性对应着文章表的列,例如文章标题属性对应文章标题列。 实体类和数据表一一对应是最简单的情况,这时候实体类和实体类是各自独立存在的,没有出现相互引用的关系。 但是,几乎每一个数据库中的表都是存在关联关系的(关系型数据库),例如除了文章表之外,还会有一个文章分类表,假如说每一篇文章都必须属于一个分类,那么在数据库中表现出来的就是文章表中有一个外键字段指向文章分类表的主键 ,在C#代码中表示出来的是文章类中有一个属性(文章分类ID),通过这个属性我们就可以知道文章所属的分类、并且可以准确地通过代码查询数据库,获取一个文章分类实体类的对象,读取到文章所属分类的相关信息。
以上的过程看起来一点问题都没有,整理一下思路,就是读取数据库,获取一个文章类对象,通过文字类对象中的文章分类ID的值,以这个值为查询条件去数据库中读取数据,获取一个文章分类对象,当然对数据库的操作我们通常 是封装在“数据访问层”中。然而从面向对象的角度考虑,我们会希望从文章类包含有文章分类的信息,用代码表示“文章分类”和“文章”两个实体类如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespaceModel
{
// 文章分类实体类
publicclassArticleCategory
{
publicintCategoryID { get; set; }
publicstringCategoryName { get; set; }
}
// 文章实体类
publicclassArticle
{
publicintArticleID { get; set; }
publicstringTitle { get; set; }
publicstringCotnent{ get; set; }
publicDateTime CreateTime { get; set; }
publicintCategoryID { get; set; }
// 文章所属分类
publicModel.ArticleCategory Category{ get; set; }
}
}
从上面的代码可以看到,在文章实体类中出现了一个Model.ArticleCategory类型的属性Category,我们想要的就是通过这个属性直接读取文章所属分类的详细信息。问题出现了,在数据库访问层中我们从数据库中读取数据去实例化一个文章实体类对象之后, 要选择在什么时候去给Category赋值。
选择一:立刻给通过分类ID(CategoryID属性)去获取所属文章分类的对象,然后塞给“文章所属分类”属性(Category) ,然后再返回文章对象。这种方法在有一点不好,就是万一得到文章对象之后根本不用去使用到Category属性……显然这种做法不佳。 选择二:在需要的时候再去读取文章分类,然后给文章类对象的Category属性赋值,但……这和没有这个属性其实也没有什么区别。 选择三:在Category属性的get访问器中实现读取数据库获取文章分类的代码,这样如果没有使用到Category属性的 时候是不会调用到这些代码的,也就不会去访问数据库拿东西了,为了避免每次访问Category属性都去读取数据库, 我们给他增加一个所有字段,得到的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protectedModel.ArticleCategory _category;
publicModel.ArticleCategory Category
{
get
{
if(_category == null)
{
// 创建文章分类数据访问层对象
Dal.ArticleCategory articleCategoryDal = newDal.ArticleCategory();
// 获取文章分类
_category =
articleCategoryDal.GetArticleCategoryByCategoryID(CategoryID);
}
return_category;
}
// set访问器就不需要了
}
乍看起来似乎没问题,但要考虑一点,在三层结构中数据的传输靠的就是“模型层”,“模型层”处于三层之下, 换句话说,“模型层”不会去引用三层中的任何一层,而上面代码中的GetArticleCategoryByCategoryID很显然是在三层之中,也许是在“业务逻辑层”或者“数据访问层”,所以...循环引用了,这种做法也不佳。如何实现对在文章类中对Category属性的数据进行延时加载呢?整理思路,根据需求一步步分析:
乍看起来似乎没问题,但要考虑一点,在三层结构中数据的传输靠的就是“模型层”,“模型层”处于三层之下,
换句话说,“模型层”不会去引用三层中的任何一层,而上面代码中的GetArticleCategoryByCategoryID很显然
是在三层之中,也行是在“业务逻辑层”或者“数据访问层”,所以...循环引用了,这种做法也不佳。
首先:获取到一个文章类对象的时候,只有在读取了Category属性才去访问数据库,不读取是不访问的 其次:读取同一个文章类对象的Category属性的时候只访问一次数据库 最后:在Category属性的get访问器中我们不能调用三层中的方法(严格说是不直接显示调用)
换个角度思考,我们能不能在数据访问层中读取数据、初始化一个文章类对象之后给它一个方法,告诉它如果你要 获取自己所属分类信息(文章分类对象)的时候就调用这个方法来拿,不用的时候就不去调用了,免得多链读取一次数据库。 “给它一个方法”,也就是说把方法传给它咯! 于是想到委托,我们可以在文章类中添加一个委托,这个委托的签名和“通过文章分类ID获取文章分类对象” 方法的签名一致,在Category属性的get访问器中调用这个委托,这样便解决了可以在get访问器中调用到方法去访问数据库, 也自然实现了延时加载!于是修改实体类代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
namespaceModel
{
// 文章分类实体类
publicclassArticleCategory
{
publicintCategoryID { get; set; }
publicstringCategoryName { get; set; }
}
// 文章实体类
publicclassArticle
{
publicintArticleID { get; set; }
publicstringTitle { get; set; }
publicstringCotnent{ get; set; }
publicDateTime CreateTime { get; set; }
publicintCategoryID { get; set; }
// 文章所属分类
protectedModel.ArticleCategory _category;
publicModel.ArticleCategory Category
{
get
{
if(_category == null)
{
if(CategoryLazyLoader != null)
{
_category = CategoryLazyLoader(CategoryID);
}
else
{
_category = null;
}
}
return_category ;
}
}
// 文章分类延时加载器(委托)
publicFuncModel.ArticleCategory> CategoryLazyLoader { get; set; }
}
}
在文章读取数据库得到数据然后创建一个文章类对象之后,我们对CategoryLazyLoader进行赋值就OK了! 文章数据访问类中获取文章的方法大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
// 根据文章ID获取文章实体类对象
public Model.Article GetArticleById(int articleId)
{
// 从数据库中取出数据,得到一个DateRow或者DateRader之类的东东然后初始化一个文章实体类对象
Model.Article article = ... // “...”是代码 - -!
// 创建文章分类数据访问对象
Dal.ArticleCategory articleCategory = new Dal.ArticleCategory();
// 指定延时加载委托
article.CategoryLazyLoader = articleCategory.GetArticleCategoryById;
// 返回文章对象
return article;
}
通过上面方法得到的文章实体类对象中的Category属性就是实现了延时加载的了!
文章写得不短,不过说的东西很简单,细想起来几乎没什么内容,一句话就是使用委托预先得到一个用于获取文章分类的方法,在文章分类属性的get选择器中调用委托返回结果。好了,告诉负责编写UI层代码的同事,调用了业务逻辑层的方法去获取文章实体类对象吧,! 已经帮你把文章分类给封装加进去了,而且使用了延时加载,怎么实现你就不用管,用就行了!于是这个人用的时候囧了,文章实体类对象里面有个委托...... 委托啊!!!干嘛用的!!!???啥意思!@##¥%#¥……%¥&……%&#¥@!#
先写到这里了,下一篇文章再说一些怎么隐藏这个委托,了解延时加载的同学应该也想到方法了...(擦!牙痛啊~~~不给力啊2011~~~~~~)
之前的文章“浅谈C#中的延迟加载(1)——善用委托”中介绍了三层结构中在Model层对实体类的属性实现延迟加载的方法,该方法利用C#中的委托来实现,最后虽然延迟加载的目的得以实现,但是给客户端(例如UI层)暴露了不必要的属性(一个委托对象,我使用了泛型的Fun类来实现)。这篇文章介绍一种方法来隐藏这个属性,同时又可以达到延迟加载的目的,更重要的是这一切都是在之前的基础上来完成的,不需要改变原来使用到实体类的地方的代码。
按照惯例,我们考虑一下想要我们的代码达到什么效果:首先在Model.Acticle(文章实体类)中的Category属性和原来一样,只在需要的时候通过调用委托来获取文章所属分类(Model.ArticleCategory类)。同时这个委托是不被客户端(例如UI层)代码看到的。。。设计模式中有一句话,大致的意思是“通常在两个事物之间加上一个中间层,可以把事情变得简单”,于是我们这样考虑,能不能加多一个中间层来实现呢? 把Category属性定义为虚拟的(virtual),去掉委托,继承Model.Acticle类实现一个子类,把委托放到到这个子类里面来,由个子类里面去实现Category属性的get选择器,客户端访问的是一个用该子类做实例化的Model.Acticle类对象,这样一来客户端代码访问的依旧是Model.Acticle类,但是看不到用于实现延迟加载的委托却又能在Category属性中获取到需要的数据。呵呵,由于我是写文章的人,所以觉得这个思路很好理解,但是看文字的您可能感觉有点晕,下面用代码来说明应该就清楚了。
首先我们修改Model.Acticle类,给他瘦身,瘦身结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 文章实体类
publicclassArticle
{
publicintArticleID { get; set; }
publicstringTitle { get; set; }
publicstringCotnent{ get; set; }
publicDateTime CreateTime { get; set; }
publicintCategoryID { get; set; }
// 所属分类
publicvirtualModel.ArticleCategory Category
{
get;
}
}
对比上一篇文章,可以发现作为延迟加载用的委托不见了,另外就是Category的get选择器中不再有任何逻辑代码,并且该属性被声明为virtual了。
下一步我们创建多一层出来。新建一个名为DModel的类库(注意:该层需要引用Model层,然后被Dal层引用)。接下来在DModel层也创建一个Article类,没错!用他来继承Model.Acticle类。Dmodel.Acticle长成下面这个样子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespaceDModel
{
// 文章
publicclassArticle : Model.Article
{
// 文章所属分类
protected Model.ArticleCategory _category;
publicoverrideModel.ArticleCategory Category
{
get
{
if(_category == null)
{
if(CategoryLazyLoader != null)
{
_category = CategoryLazyLoader(CategoryID);
}
else
{
_category = null;
}
}
return _category;
}
}
// 文章分类延时加载器(委托)
publicFunc CategoryLazyLoader { get; set; }
}
}
是的,看到了么,委托跑这儿来了,并且他实现了Model.Acticle的Category属性的get选择器,里面的逻辑是不是也很熟悉呢^_^。
接着还有一个地方要做点小修改,就是Dal层中获取一个文章实体类的方法,也就是前一篇文章中写到的public Model.Article GetArticleById(int articleId)方法了,修改后如下:
1
2
3
4
5
6
7
8
9
10
11
12
// 根据文章ID获取文章实体类对象
public Model.Article GetArticleById(int articleId)
{
// 从数据库中取出数据,得到一个DateRow或者DateRader之类的东东然后初始化一个文章实体类对象
DModel.Article article = ... // “...”是代码 - -!
// 创建文章分类数据访问对象
Dal.ArticleCategory articleCategory = new Dal.ArticleCategory();
// 指定延时加载委托
article.CategoryLazyLoader = articleCategory.GetArticleCategoryById;
// 返回文章对象
return article;
}
请注意上面代码中这一句,DModel.Article article = ... // “...”是代码 - -!,我们得到一个DModel.Article对象,最后以Model.Article的形式返回(C#中的装箱,是吧~)。再次告诉你的同事吧,你不用去管那个委托了,因为你现在拿到的Mode.Article对象中看不到那玩意,反正对你也没用。果然,看不见了,延迟加载的目的也达到鸟!
总结一下,本文讲的比前一篇文章讲的东西还少,主要记住三点:
1、把Category属性声明为虚拟的;
2、把Category原来的逻辑代码推迟到子类里面实现;
3、用子类(DModel.Acticle)实例话父类(Model.Acticle)。
好了,用这个方法把项目里面类似Category这样的实体类属性修改为virtial吧,创建子类去重写它实现延迟加载,子类只有Dal层知道它的存在,Bll层和UI层对此一无所知,他们还是和原来一样用着,啥都不用修改,但是代码的效率明显有了提高,现在属性没有被使用就不会读取数据库了,数据库的压力也减少了^_^!!!
这一切看起来似乎没问题,直到一个BUG被发现......在一个实体类中,有一个属性,按照之前的逻辑是我们给它赋值的时候,会对其他属性做相应的修改,现在给重写掉了,之前的逻辑,没拉····没拉·······没拉···························。囧TZ~~~ (待续)
讲到把实体类中需要实现延迟加载的属性声明为virtual,然后继承实体类做一个子类,在子类里面实现该属性,配合使用委托来实现比较完美的延迟加载(原本的”模型层“依旧保持在最底层用于贯穿三层结构,同时又可以实现在实体类的属性里面访问到比他高层的”数据访问层“)。文章的最后依旧出现杯具,原因是在对模型的属性实现延迟加载之前,这个属性可能由于我们业务的需要,它并不单单是作为一个存储和读取的功能使用,而是在其get或者set的访问器中都包含这或许复杂或许简单的逻辑代码。
举例:考虑一下这个情景,我们有一个叫做任务单的实体类,其中有两个属性,一个叫做”任务名”,一个叫做“发布时间”,现在有这样的业务规定,任务名称可以为空,但如果任务名称为空的话我们要读取“发布时间”生成一个任务名来代替掉这个空值(例如叫做“Issue20110120191345”),当然这个例子有点牵强,主要是我想不出什么很具体的实例,但是在实际开发中这种情况肯定是有的并且其中的逻辑代码有可能复杂到你难以想象。
沿用前面两篇文章的例子,我模拟了这一现象,对“文章”实体类做了点修改,增加了一个名为GetCategoryRecord的字符串型属性,它的作用基本上可以从字面上看出来,叫做“Category属性get访问器调用记录”。于是“文章“类(基类)修改如下:
c#代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
namespace Model
{
// 文章实体类
public class Article
{
public int ArticleID { get; set; }
public string Title { get; set; }
public string Cotnent{ get; set; }
public DateTime CreateTime { get; set; }
public int CategoryID { get; set; }
///
/// 所属分类
///

protected Model.ArticleCategory _category;
///
/// 所属分类
///

public virtual Model.ArticleCategory Category
{
get
{
GetCategoryRecord += "获取分类;";
return _category;
}
}
///
/// Category属性get访问器调用记录
///

public string GetCategoryRecord { get; set; }
}
}
这里我们关心那两个有写注释的属性,并且出现了一个保护字段_category(这个尤其重要,起到和子类的联通作用)。可以看到现在有这样的业务规则了:Category属性被get一次就会往GetCategoryRecord属性中做点记录。于是我们在设计代码的时候立刻会想到要是继承Model.Article类的基类要是重写这个属性的话势必要保持这个业务不变,否则在实现延迟加载之后肯定会丢掉一些之前设计好的业务逻辑了。修改继承它的子类如下:
c#代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
namespace DModel
{
///
/// 文章
///

public class Article : Model.Article
{
///
/// 所属分类
///

public override Model.ArticleCategory Category
{
get
{
if (base._category == null)
{
if (CategoryLazyLoader != null)
{
base._category = CategoryLazyLoader(CategoryID);
}
else
{
base._category = null;
}
}
return base.Category;
}
}
///
/// 文章分类延时加载器(委托)
///

public Func CategoryLazyLoader { get; set; }
}
}
这里可以看到DModel.Article类的Category属性中对基类的保护字段base._cateogry进行了操作,最后返回的是基类Category。粗看起来似乎有点乱,但是理清一下思路其实如下:基类的Category属性通过返回_category字段的方式返回值,也就是说数据是存在_category字段而不是属性中,但是_category字段怎么才会有值呢,那就是在子类里面通过调用委托拿来的,而这个属性在子类里面不是直接返回的,而是调用基类来返回,这样一来,调用到子类的Category属性的get访问器的时候,先对基类的_categoty字段赋值,然后调用基类的Category属性执行了一些逻辑代码,最后成功地把(已经被赋值的)基类的_categoty字段给返回去。而这一切都是在前面我们实现好的延迟加载的基础上完成的。总结成几个字就是:子类负责延时加载,基类赋值数据存储和返回!呵呵,是不是觉得很简单呢^^
这里可以看到DModel.Article类的Category属性中对基类的保护字段base._cateogry进行了操作,最后返回的是基类Category。粗看起来似乎有点乱,但是理清一下思路其实如下:
基类的Category属性通过返回_category字段的方式返回值,也就是说数据是存在_category字段而不是属性中,但是_category字段怎么才会有值呢,那就是在子类
里面通过调用委托拿来的,而这个属性在子类里面不是直接返回的,而是调用基类来返回,这样一来,调用到子类的Category属性的get访问器的时候,先对_categoty字段赋值,然后调用基类的Category属性实现了一些逻辑代码,最后成功地把(已经被赋值的)_categoty字段给返回去。而这一切都是在前面我们实现好的延迟加载的基础上完成的。呵呵,是不是觉得很简单呢^^
这里可以看到DModel.Article类的Category属性中对基类的保护字段base._cateogry进行了操作,最后返回的是基类Category。粗看起来似乎有点乱,但是理清一下思路其实如下:
基类的Category属性通过返回_category字段的方式返回值,也就是说数据是存在_category字段而不是属性中,但是_category字段怎么才会有值呢,那就是在子类
里面通过调用委托拿来的,而这个属性在子类里面不是直接返回的,而是调用基类来返回,这样一来,调用到子类的Category属性的get访问器的时候,先对_categoty字段赋值,然后调用基类的Category属性实现了一些逻辑代码,最后成功地把(已经被赋值的)_categoty字段给返回去。而这一切都是在前面我们实现好的延迟加载的基础上完成的。呵呵,是不是觉得很简单呢^^
其实这一篇讲的情况不是针对延迟加载这个技术来讲的,在我们的开发过程中经常会遇到这种实现了业务代码的实体类属性,拥有这种属性的模型通常被叫做“充血模型”,如果模型的属性都是简单的get和set的话通常叫做“贫血模型”(当然可能有其他叫法 哈~)。这篇文章是在没啥内容,算是对前两篇文章的一个补充吧,希望没有浪费你的时间哈^_^。
PS:不知道有哪位朋友对反垃圾留言比较有办法呢?听说wp上面很多垃圾留言,也出现了很多反垃圾留言的插件,在asp.net中大家一般是怎么做的呢?希望有了解的朋友和我交流一下,方式不限