When it comes to Lazy Load, some articles or books are translated as Lazy Load. Although I don't like this translation very much, the word "Lazy" can be close to life. We are too lazy to do many things. If things do not happen, we will make money.
Delayed loading: Martin Flower defines an object that does not contain all the required data, but knows how to obtain the data.
To understand this sentence, let's take a scenario as an example. In some cases, to get a record from the database, you need to establish a connection with the database, make network requests, execute SQL statements, and close the connection, it takes a lot of effort and a lot of cost to get the required data, but the tragedy happened, and the actual data of this record is never used. In this case, can I get the data when I need to use the data. How can we obtain data as needed? What are the indicators used to obtain data?
There is still a piece of code, because in the eyes of my codenon, "the truth is only possible with code ":
class LazyLoadDemo { static void Main(string[] args) { Session session = new Session(); Person person = session.Load<Person>(1); string name = person.Name; } }
Here I use the Session object to complete database operations. The Load method will delay loading data. The definition of delayed loading is explained by code:
1. An object that does not contain all the required data
The object returned by sesson. Load <Person> (1) does not contain all the data. This means that the Load method does not initiate a request to the data to obtain all the data.
2. But I know how to get the data.
Although the person object does not contain all the data, it knows how to obtain all the data from the database, that is, accessing person. when Name is used, the person object sends a request to the database to obtain all the data. How can this problem be obtained based on the mark? It is not difficult to think of parameter 1 through the Load method. Here it is generally the primary key, so there is a unique identifier of the data.
At this point, some friends may be confused if they have never been familiar with ORM:
1. I only know the value of the primary key. I don't know how to query the fields corresponding to the primary key?
2. I don't know how to query the table corresponding to person?
3. How to know the data field of Name
From the above problems, we can see that the word "" frequently appears. This is the core idea of ORM. The full name of ORM is object/Relation Mapping, and relational object ing, in the world of relational databases, it has its own language, such as SQL. in the object-oriented world, you have your own language. For example, C #. In this way, the database cannot be operated as the object-oriented method, and a corresponding bridge is required to connect the two worlds. How can it serve as a bridge? I will refer to the nhing file of nhib.pdf to explain to you:
<?xml version="1.0" encoding="utf-8" ?>
This mapping is an XML file, which must be well understood by everyone.
1. The class element of the XML file. The value of name represents the name of the C # class, and the value of table represents the name of the database table.
This way, the person object will know which table to query data.
2. xml file ID element, representing the primary key. Name indicates the ing in the class, and column indicates the column of the object.
In this way, the person object will know the table's primary key.
3. The property element of the XML file, representing the element of a non-primary key. It is also available.
In this way, the person object will know other fields in the table.
I believe that for the three explanations of the Mapping File, I have answered the three questions I raised. Think about them carefully. The person knows the table field, table name (foreign key ), I know almost everything about the table. Of course, I know how to get the data. But when a new problem arises, how can we implement this delayed loading? How to make the person not loaded until the access attribute is accessed? I believe that my friends who have read the first article in this series have an idea to use dynamic agents.
. That is, the returned result of the Start load method is only a person-class proxy. The pseudocode of the proxy is like this:
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; } } }
With the previous analysis, I believe everyone is eager to try it and want to implement it by themselves. Let's hesitate to implement it together. Here I will implement a simple version step by step:
First of all, the database, table, is one of the protagonists of the story. You can create a database and create a table!
Create Database lazydemouse lazydemogocreate table person (ID int Primary Key Identity (1, 1), name varchar (20) not null, salary money not null, description varchar (200 )) goinsert into person values ('Code farm 1', '000000', 'Do not understand dynamic agent') insert into person values ('Code farm 2', '20140901 ', 'He knows about dynamic proxies') insert into person values ('coden3', '123', 'he can implement delayed loading ')
OK. Let's leave the world of Relational Models and enter the object-oriented world. First, create a person class and map it.
Public class person {/* Note that it must be virtual. If you are not familiar with this series, read the first article */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 ing person. HBM. XML, and embed it into the Assembly as the Assembly resource.
<?xml version="1.0" encoding="utf-8" ?>
Because the configuration file is embedded in the Assembly, a class is required to read the XML and cache it. The implementation here ensures that the container is thread-safe.
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"; } }
After obtaining the XML, you need a class to parse the XML to obtain the corresponding table information. The type extension method is used to make the program more readable.
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; } }}
The data table information is obtained from XML and a class is required to operate the database. Here I try to rely on abstraction, so that the database is not related to the platform as much as possible and can be expanded in the future.
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); } }
Parameters on the SQL platform only need to be simple basic abstract classes, and data connections are written in the configuration file.
public class SqlContext:AbstractDbContext,IDbContext { public SqlContext() : base(new SqlConnection(ConfigurationUtil.GetConnectionString())) { } }
Now we need to be able to generate proxy methods for the session. Load Method. Here we still use Castle to generate Proxy:
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)); } }
Note: The second parameter for calling createclassproxy in getproxy <tentity> () is particularly important. It means that the object to be proxy implements the interface specified by the parameter at runtime, here, the interface I specified is ilazyintroduction.
public interface ILazyIntroduction { ILazyInitializer LazyInitializer { get; } } public interface ILazyInitializer { bool IsInitialzed { get; } }
In this way, I can check whether the object is initialized, because I have implemented the ilazyintroduction interface when implementing the dynamic proxy, which is somewhat different from the introduction notification in AOP, I will have the opportunity to explain it in detail later.
public class LazyUtil { public static bool IsInitialized(object entity) { return ((ILazyIntroduction)entity).LazyInitializer.IsInitialzed; } }
At present, the most important part is that the interceptor is coming to the market. It is the director. It includes all the interception logic. Now let it go to the market:
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); } }
The most important thing is the intercept method. Here are several conditions for a brief explanation of my ideas:
1. ischeckinitialize: This is directly accessed when the lazyutil. isinitialized method is called.
2. If 1 is not met, it is necessary to determine whether the data is initialized, that is, whether the data is loaded from the database. If there is no primary key and it is not the primary key accessed, It is queried to the database, and if it is the primary key accessed, it is directly returned.
In this way, the past is ready, and only the session is owed:
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); } }
Okay. perform a simple integration test.
[TestFixture] class TestLazyLoad {private ISession session; private Person person; [SetUp] public void Initialize () {session = new SessionImpl (); person = session. load <Person> (1);} [Test] public void testisinitialed () {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 ("codenong 1", person. name); Assert. isTrue (LazyUtil. isInitialized (person); Assert. areEqual (3000 m, person. salary );}}
Here we can see that TestAccessOtherProperty () was queried by the database, and an SQL statement was sent. Besides, although this method accessed the attribute twice, it only executed the SQL statement once.
You may be confused, so I will share the code and do not forget to modify the database connection when executing the code.
Code download