说到延迟加载(Lazy Load), 有些文章或书籍翻译为懒加载,虽然我不太喜欢这个翻译,但是这个“懒”字能贴近生活的。很多事情我们懒得去做,如果事情没有发生,我们就赚到了。
延迟加载,Martin Flower在《企业应用架构模式》中给了这样一个定义:一个对象,它虽然不包含所需要的所有数据,但是知道怎么获取这些数据。
为了理解这句话,还是先来举个场景,在某些时候,从数据库里得到一条记录,需要与数据库建立连接,网络请求,执行SQL,关闭连接,费了很大的力气,很大的代价,把所需的数据拿到手,但是悲剧的事情发生了,这个记录的实际数据从不曾用到,这种情况下,能不能"懒一下",需要使用数据的时候我才去获取数据。怎么才能做到在需要的时候获得数据?并通过什么标示来获得数据呢?
还是来一段代码,因为在我这个码农的眼里,“有代码才有真相”:
class LazyLoadDemo { static void Main(string[] args) { Session session = new Session(); Person person = session.Load<Person>(1); string name = person.Name; } }
这里我用Session这个对象来完成数据库的操作,Load方法将延迟加载数据,用代码来解释延迟加载的定义就是:
1.一个对象,它虽然不包含所需要的所有数据
码农翻译:sesson.Load<Person>(1)返回的对象,不包含所有的数据,就意味着此时Load方法没有向数据发起请求获得所有数据
2.但是知道怎么获取这些数据
码农翻译:person对象虽然不包含所有数据,但它知道怎样从数据库获得所有的数据,也就是说在访问person.Name时,person对象才向数据库发出请求去获得所有数据,那怎么凭什么标示去获得呢?不难想到是通过Load方法的参数1,这里一般而言是主键,这样就有了数据的唯一标示。
讲到这里,可能有些朋友如果从来接触过ORM,可能有些困惑:
1.只知道了主键的值,不知道主键对应的字段怎么查询?
2.不知道person对应的表怎么查询?
3.怎么知道Name对应数据的字段
从上述的问题里可能已经看出“对应”这个词频繁出现,这正是ORM的核心思想,ORM全称Object/Relation Mapping,关系对象映射,在关系数据库的世界里,它有着它自己的语言,比如SQL.在面向对象的世界里,有它自己的语言。比如C#.这样就造成了不能像面向对象那样去操作数据库,需要一个对应来起到了将两个世界连接起来的桥梁。怎样起到的桥梁作用?我参照NHibernate的mapping文件跟大家讲解:
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"> <class name ="Person" table="Person"> <id name="Id" column ="id"> <generator class ="native"/> </id> <property name ="Name" type="string" column="name"/> </class> </hibernate-mapping>
这个mapping是个XML文件,想必大家都能很好的理解。
1.xml文件class元素,name的值代表了C#类的名字,table的值代表了数据库表的名字。
码农翻译:这样person对象就知道了向哪张表查询数据。
2.xml文件Id元素,代表主键。name代表在类中的映射,column代表对象的列。
码农翻译:这样person对象就知道了表的主键。
3.xml文件property元素,代表非主键的元素。对应同理可得。
码农翻译:这样person对象就知道表的其他字段。
我相信对于mapping文件的三个解释,很好的回答了开始提出的三个问题,仔细想想,person知晓了表的字段,表名(外键),几乎知晓了表的一切当然它能知道怎么样得到数据,但是新的问题出现了,我们怎么去实现这个延迟加载?怎样让person直到访问属性的时候才加载呢?我相信看过这个系列第一遍文章的朋友已经有想法,可以采用动态代理
来实现。即开始Load方法返回的只是一个Person类的代理,代理的伪代码大概这样:
class PersonProxy:Person { private bool initialized; public override string Id { get { return base.Id; } set { base.Id = value; } } public override string Name { get { if (!initialized) { /*Query*/ } return base.Name; } set { base.Name = value; } } }
有了前面的分析,我相信大家都跃跃欲试了,想自己来实现,那我们还犹豫什么一起来实现吧,这里我就一步一步地来实现一个简易的版本:
首先,故事的主角之一数据库,表,是并不可少的,来建数据库,建表吧!
create database LazyDemo use LazyDemo Go create table Person ( Id int primary key identity(1,1), Name varchar(20) not null, Salary money not null, Description varchar(200) ) Go insert into Person values('码农1','3000','不懂动态代理') insert into Person values('码农2','5000','他了解动态代理') insert into Person values('码农3','8000','他能实现延迟加载')
Ok,让我们离开关系模型的世界,进入面向对象的世界,先建立一个Person类,并对它进行映射。
public class Person { /*注意一定必须是Virtual,不了解可以看这个系列第一遍文章*/ public virtual string Id { get; set; } public virtual string Name { get; set; } public virtual decimal Salary { get; set; } public virtual string Description { get; set; } }
person的映射person.hbm.xml,并把它嵌入程序集,作为程序集的资源。
<?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"> <class name ="Person" table="Person"> <id name="Id" column ="Id"> <generator class ="native"/> </id> <property name ="Name" type="string" column="Name"/> <property name ="Salary" type="decimal" column="Salary"/> <property name ="Description" type="string" column="Description"/> </class> </hibernate-mapping>
由于配置文件被嵌入程序集中,所以需要一个类将Xml读出,并缓存起来,这里的实现保证容器是线程安全的。
public static class MappingContainer { private static IDictionary<Type, XElement> xmls = new Dictionary<Type, XElement>(); private static object syncObj = new object(); public static XElement GetMappingXml(Type type) { if (type == null) { throw new NullReferenceException("type can't be null!"); } if (!xmls.ContainsKey(type)) { AddMappingXml(type, GetMappingXmlForAssembly(type)); } return xmls[type]; } private static void AddMappingXml(Type type, XElement xEle) { if (!xmls.ContainsKey(type)) { lock (syncObj) { if (!xmls.ContainsKey(type)) { xmls.Add(type, xEle); } } } } private static XElement GetMappingXmlForAssembly(Type type) { Stream xmlStream = type.Assembly.GetManifestResourceStream(GetMappingXmlName(type)); if (xmlStream == null) { throw new InvalidOperationException("Entity should have mapping xml embeded the assembly!"); } return XElement.Load(xmlStream); } private static string GetMappingXmlName(Type type) { return type.FullName + ".hbm.xml"; } }
此时,得到xml之后,需要类来解析xml得到相应的表的信息,采用对Type扩展方法让程序有更好的可读性。
namespace LazyDemo { public static class MappingXmlParser { public static string GetTableName(this Type type) { return GetXmlEleValue(type, "class", "table"); } public static string GetIdentityPropName(this Type type) { return GetXmlEleValue(type, "id", "name"); } public static string GetIdentityColumnName(this Type type) { return GetXmlEleValue(type, "id", "column"); } public static IDictionary<string, string> GetPropertyMappingDic(this Type type) { IEnumerable<XElement> propElems = from elem in GetMappingXml(type).DescendantsAndSelf("property") select elem; return propElems.ToList().ToDictionary( key => key.Attribute("name").Value, value => value.Attribute("column").Value); } private static XElement GetMappingXml(Type type) { if (type == null) { throw new ArgumentNullException("Entity can't be null!"); } /*xml should validate by xml schema*/ return MappingContainer.GetMappingXml(type); } private static string GetXmlEleValue(Type type, string eleName, string attrName) { var attrs = from attr in GetMappingXml(type).DescendantsAndSelf(eleName).Attributes(attrName) select attr; return attrs.Count() > 0 ? attrs.First().Value : string.Empty; } } }
从Xml中得到了数据表的信息,需要一个类对数据库进行操作,这里我尽量依赖于抽象,使得数据库尽量与平台无关,可以在将来的时候进行扩展。
public abstract class AbstractDbContext:IDbContext { protected IDbConnection dbConnection; protected AbstractDbContext(IDbConnection dbConnection) { this.dbConnection = dbConnection; } public DataReaderInfo Query(string sqlStr) { dbConnection.Open(); IDbCommand dbCommand = dbConnection.CreateCommand(); dbCommand.CommandText = sqlStr; return new DataReaderInfo(dbCommand.ExecuteReader(CommandBehavior.CloseConnection),sqlStr); } }
在Sql平台下的参数,只需要简单基础抽象类即可,并且数据连接写在配置文件中。
public class SqlContext:AbstractDbContext,IDbContext { public SqlContext() : base(new SqlConnection(ConfigurationUtil.GetConnectionString())) { } }
现在需要能对Session.Load方法产生代理的方法,这里还是使用Castle来产生代理:
public class ProxyFactoryImpl<TEntity>:IProxyFactory { private ProxyGenerator proxyGenerator; private ISession session; private IdentityInfo identityInfo; public ProxyFactoryImpl(IdentityInfo identityInfo,ISession session) { proxyGenerator = new ProxyGenerator(); this.session = session; this.identityInfo = identityInfo; } public TEntity GetProxy<TEntity>() { if (typeof(TEntity).IsClass) { return (TEntity)proxyGenerator.CreateClassProxy( typeof(TEntity), new Type[] { typeof(ILazyIntroduction) }, new LazyLoadIntercetor(identityInfo, session)); } return (TEntity)proxyGenerator.CreateInterfaceProxyWithoutTarget( typeof(TEntity), new Type[] { typeof(ILazyIntroduction) }, new LazyLoadIntercetor(identityInfo, session)); } }
这里注意:GetProxy<TEntity>()中对CreateClassProxy的调用的第二个参数,特别重要,它的意思是被代理的实体在运行时实现参数所指定的接口,这里我所指定的接口是ILazyIntroduction
public interface ILazyIntroduction { ILazyInitializer LazyInitializer { get; } } public interface ILazyInitializer { bool IsInitialzed { get; } }
这样我就可以对实体是否初始化进行检查,因为动态代理在实现时被我做了手脚实现了ILazyIntroduction接口,这里与AOP中的Introduction的通知有点类型,以后有机会再详细讲解。
public class LazyUtil { public static bool IsInitialized(object entity) { return ((ILazyIntroduction)entity).LazyInitializer.IsInitialzed; } }
现在最核心的部分,拦截器要登场,它才是导演,它包括了所有的拦截逻辑,现在让它闪亮登场吧:
public class LazyLoadIntercetor:IInterceptor,ILazyInitializer { private IdentityInfo identityInfo; private bool initialized; private ISession session; private object instance; public bool IsInitialzed { get { return initialized; } } public LazyLoadIntercetor(IdentityInfo identityInfo, ISession session) { this.identityInfo = identityInfo; this.session = session; } public void Intercept(IInvocation invocation) { if (IsCheckInitialize(invocation)) { invocation.ReturnValue = this; return; } if (!initialized) { if (invocation.Arguments.Length == 0) { if (IsCallIdentity(invocation)) { invocation.ReturnValue = identityInfo.Value; return; } this.instance = GetDataFormDB(invocation); initialized = true; } } invocation.ReturnValue = invocation.Method.Invoke(instance, invocation.Arguments); } private bool IsCheckInitialize(IInvocation invocation) { return invocation.Method.Name.Equals("get_LazyInitializer"); } private bool IsCallIdentity(IInvocation invocation) { return invocation.Method.Name.Equals("get_" + identityInfo.Name); } private object GetDataFormDB(IInvocation invocation) { object instance = session.QueryDirectly(invocation.InvocationTarget.GetType().BaseType, identityInfo.Value); return instance; } private void DirectlyLoadFormDB(IInvocation invocation) { object instance = session.QueryDirectly(invocation.InvocationTarget.GetType().BaseType, identityInfo.Value); invocation.ReturnValue = invocation.Method.Invoke(instance,invocation.Arguments); } }
最重要的还是Intercept方法,这里有几个条件,简单地讲解下我的思路:
1.IsCheckInitialize即当调用LazyUtil.IsInitialized的方法时,直接访问this。
2.如果不满足1,就要判断数据是否初始化,即是否从数据库加载,如果没有并且不是访问的主键,向数据库查询,如果是访问主键直接返回。
这样,往事具备,只欠Session啦:
public class SessionImpl : ISession { public IDbContext DbContext { get; set; } private IProxyFactory proxyFactory; public SessionImpl() { DbContext = new SqlContext(); } public TEntity Load<TEntity>(object id) { IdentityInfo identityInfo = new IdentityInfo(id, typeof(TEntity).GetIdentityPropName()); proxyFactory = new ProxyFactoryImpl<TEntity>(identityInfo, this); return proxyFactory.GetProxy<TEntity>(); } public object QueryDirectly(Type type, object id) { object instance = Activator.CreateInstance(type); SetIdVal(instance, id); SetPropertyVal(id, instance); return instance; } private void SetPropertyVal(object id, object instance) { string sqlText = instance.GetType().ParserSqlText(id); instance.SetPropertyFromDB(DbContext.Query(sqlText)); } private void SetIdVal(object instance, object id) { PropertyInfo idPropertyInfo = instance.GetType().GetProperty(instance.GetType().GetIdentityPropName()); idPropertyInfo.SetValue(instance, id, null); } }
好了,进行简单的集成测试。
[TestFixture] class TestLazyLoad { private ISession session; private Person person; [SetUp] public void Initialize() { session = new SessionImpl(); person = session.Load<Person>(1); } [Test] public void TestIsInitialzed() { Assert.IsFalse(LazyUtil.IsInitialized(person)); } [Test] public void TestAccessIdentity() { Assert.IsFalse(LazyUtil.IsInitialized(person)); Assert.AreEqual(1, person.Id); Assert.IsFalse(LazyUtil.IsInitialized(person)); } [Test] public void TestAccessOtherProperty() { Assert.IsFalse(LazyUtil.IsInitialized(person)); Assert.AreEqual("码农1", person.Name); Assert.IsTrue(LazyUtil.IsInitialized(person)); Assert.AreEqual(3000m, person.Salary); } }
这里可以看到TestAccessOtherProperty()想数据库进行了查询,发出了Sql,而且,虽然这个方法中访问了2次属性,但是还是只执行了一次SQL.
能看到这里,也许你还有的糊涂,那我就把代码共享出来,执行代码时不要忘记修改数据库连接。