65.9K
CodeProject 正在变化。 阅读更多。
Home

NHibernate 的延迟初始化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (3投票s)

2009年2月21日

CPOL

4分钟阅读

viewsIcon

57354

减少多用户 WinForms 应用程序中的 StaleObjectStateException 损坏。

引言

首先,我需要说明的是,本文基于 Steinar Dragsnes 的工作和想法,本文的大部分代码都出自他之手。毋庸置疑,没有他的同意,本文就不会发表。

那么,对于那些使用 NHibernate 开发过 WinForms 应用程序的人来说,可能遇到过会话管理的问题,以及那个臭名昭著的 LazyInitializationException。在 WinForms 应用程序中,通常不会在视图的整个生命周期内保持一个打开的会话,因为视图的生命周期可能会很长。这意味着在访问延迟加载的代理时,很容易遇到 LazyInitializationException。本文将介绍一个延迟初始化静态类的想法。

背景

与 Web 应用程序相比,WinForms 应用程序中的 NHibernate 会话管理相当复杂,因为并非所有会话都可以为窗体的整个生命周期保持打开状态,或者说,不能超过服务方法的持续时间。主要原因是为了简单地防止 NHibernate 会话变得过于庞大(占用大量数据和内存)而失控;另一个重要原因是 StaleObjectStateException,尤其是在处理多用户系统时,您可能需要处理此异常,因为该系统经常会发生并发写入/更新操作(再次感谢 Steinar 提出这一点)。这就是我们遇到的 LazyInitializationException 问题 - 当访问延迟集合或代理时,由于会话未打开,就会抛出该异常。这意味着您需要预料到需要访问数据库来初始化这些延迟实体。处理此问题的一种方法是在您的 DAO 中设置初始化方法。我将在这里展示的代码背后的想法是一个 static 类,它将负责初始化这些实体,并在需要时初始化它们的延迟属性以及属性的属性,依此类推。

关于该主题的更多背景信息可以在我之前关于 NHibernate for WinForms 的文章中找到,我在其中尽力收集了尽可能多的相关数据。

Using the Code

现在,要开始使用以下类,您需要理解几个方法 - 构造函数 LazyInitializer 和备用构造函数 AlternativeCOnstructor

构造函数 - static LazyInitializer() - 使用 NHibernate.Mapping.Attribute 来获取所有具有延迟属性的映射类。该方法文档齐全,无需过多说明。AlternativeConstructor 方法使用 NHibernate.Cfg.Configuration 类来获取所有具有延迟属性的映射域对象 - 此方法最大的优点是它可以用于任何类型的映射系统(HBM 文件、Mapping.Attribute 或 NHibernate Fluent Interfaces);但是,您需要为可能使用的任何延迟属性添加新的查询(我只为 Bag 和多对一/一对一属性提供了查询),而另一种方法(使用 Mapping.Attribute)则可以处理所有可能的延迟属性/属性。(我还应该为 NFI 添加一个方法)。

这些方法会创建一个字典,其中以 Type 作为键,以 PropertyInfoMethodInfo 的列表作为值。然后,此字典将在 ExtractNHMappedProperties 方法中使用,以初始化特定实体的延迟属性。我们通过实体类型获取延迟属性 - 如果我们的字典包含该实体类型,则意味着至少有一个延迟属性...

该方法被递归调用,以便我们可以根据需要深入到实体树。LazyInitialise 方法是执行实体/属性实际初始化的方法,使用 NHibernateUtil.Initialize(proxy);InitializeEntityInitializeCompletelyImmediateLoad 方法是 public 的,用于与 DAO 通信并由 DAO 调用以初始化实体。

我做的另一件事是构建一个所有声明为代理的映射类的列表,以便当 DAO 加载代理类的单个实体时(Session.Load<MyProxyClass>(1);),我会初始化该代理。这样做的原因是,我为所有 DAO 使用了一个通用的父类;因此,在加载我的实体时,我不知道加载的是哪种类型,所以我总是检查实体是否是代理类型,如果是,我就会初始化该实体。如果您不使用通用的父 DAO 类,则无需执行此操作。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Collection;
using NHibernate.Mapping.Attributes;
using NHibernate.Proxy;
using LockMode=NHibernate.LockMode;

namespace MyApplication.Data
{
    public static class LazyInitializer
    {
        #region fields
        /// <summary>
        /// A container of all types containing lazy properties
        /// with a list of their respective lazy properties.
        /// </summary>
        private static IDictionary<Type, List<PropertyInfo>> 
                propertiesToInitialise = 
                new Dictionary<Type, List<PropertyInfo>>();
        private static IList<Type> proxyTypes = new List<Type>();
        #endregion
        
        /// <summary>

        /// Fully initialize the instance of T type with a primary key equal to id.
        /// </summary>
        /// <typeparam name="T">The type to resolve and load</typeparam>
        /// <param name="id">The primary key of the type to load</param>

        /// <param name="session">The session factory
        /// used to extract the current session</param>
        /// <returns>The fully initialized entity</returns>
        public static T ImmediateLoad<T>(int id, ISession session)
        {
            T entity = session.Load<T>(id);
            return InitializeCompletely(entity, session);
        }

        /// <summary>

        /// Convenience method for loading the complete
        /// object graph for an already initialized entity,
        /// where parts of the entity's object graph may be proxy instances.
        /// </summary>
        /// <remarks>
        /// This is done by recursively looping through all NHibernate
        /// mapped properties in the object graph and 
        /// examining if they are lazy loaded (represented by a proxy).
        /// It is at this moment unknown
        /// whether this approach is inefficient. This must be tested.
        /// </remarks>
        /// <param name="entity">

        /// The entity to initialize. This must be an initialized
        /// entity object that holds lazy properties. From
        /// the LazyInitializer's scope, the entity is the top node in the object graph.
        /// </param>
        /// <param name="session">The current session</param>
        /// <returns>The fully initialized entity</returns>

        public static T InitializeCompletely<T>(T entity, ISession session)
        {
            // Okay, first we must identify all the proxies we want to initialize:
            ExtractNHMappedProperties(entity, 0, 0, true, session);
            return entity;
        }

        /// <summary>
        /// Convenience method for loading proxies in entity
        /// object graph. Providing fetch depth to speed up
        /// processing if only a shallow fetch is needed.
        /// </summary>
        /// <remarks>
        /// This is done by recursively looping through
        /// all properties in the object graph and 
        /// examining if they are lazy loaded (represented
        /// by a proxy). It is at this moment unknown
        /// wether this approach is inefficient. This must be tested.
        /// </remarks>

        /// <param name="entity">
        /// This is done by recursively looping through
        /// all NHibernate mapped properties in the object graph and 
        /// examining if they are lazy loaded (represented
        /// by a proxy). It is at this moment unknown
        /// wether this approach is inefficient. This must be tested.
        /// </param>
        /// <param name="maxFetchDepth">The search depth.</param>
        /// <param name="session">The current session</param>

        /// <returns>A partly initialized entity,
        /// initialized to max fetch depth</returns>
        public static T InitializeEntity<T>(T entity, 
                        int maxFetchDepth, ISession session)
        {
            // Let's reduce the max-fetch depth to something tolerable...
            if (maxFetchDepth < 0 || maxFetchDepth > 20) maxFetchDepth = 20;
            // Okay, first we must identify all the proxies we want to initialize:
            ExtractNHMappedProperties(entity, 0, maxFetchDepth, false, session);
            return entity;
        }

        /// <summary>
        /// Search the object graph recursively for proxies,
        /// until a certain threshold has been reached.
        /// </summary>

        /// <param name="entity">The top node in the object
        /// graph where the search start.</param>
        /// <param name="depth">The current depth from
        /// the top node (which is depth 0)</param>
        /// <param name="maxDepth">The max search depth.</param>

        /// <param name="loadGraphCompletely">Bool flag indicating
        /// whether to ignore depth params</param>
        /// <param name="session">The current session to the db</param>
        private static void ExtractNHMappedProperties(object entity, int depth, 
                       int maxDepth, bool loadGraphCompletely, ISession session)
        {
            bool search;
            if (loadGraphCompletely) search = true;
            else search = (depth <= maxDepth);

            if (null != entity)
            {
                // Should we stay or should we go now?
                if (search)
                {
                    // Check if the entity is a collection.
                    // If so, we must iterate the collection and
                    // check the items in the collection. 
                    // This will increase the depth level.
                    Type[] interfaces = entity.GetType().GetInterfaces();
                    foreach (Type iface in interfaces)
                    {
                        if (iface == typeof(ICollection))
                        {
                            ICollection collection = (ICollection)entity;
                            foreach (object item in collection) 
                              ExtractNHMappedProperties(item, depth + 1, 
                                maxDepth, loadGraphCompletely, session);
                            return;
                        }
                    }

                    // If we get here, then we know that we are
                    // not working with a collection, and that the entity
                    // holds properties we must search recursively.
                    // We are only interested in properties with NHAttributes.
                    // Maybe there is a better way to specify this
                    // in the GetProperties call (so that we only get an array
                    // of PropertyInfo's that have NH mappings).
                    List<PropertyInfo> props = propertiesToInitialise[entity.GetType()];
                    foreach (PropertyInfo prop in props)
                    {
                        MethodInfo method = prop.GetGetMethod();
                        if (null != method)
                        {
                            object proxy = method.Invoke(entity, new object[0]);
                            if (!NHibernateUtil.IsInitialized(proxy))
                            {
                                LazyInitialise(proxy, entity, session);
                            }

                            if (null != proxy)
                              ExtractNHMappedProperties(proxy, depth + 1, maxDepth, 
                                         loadGraphCompletely, session);
                        }
                    }
                }
            }
        }

        /// <summary>

        /// The core method delegating the hard lazy initialization
        /// work to the hibernate assemblies.
        /// </summary>
        /// <param name="proxy">The proxy to load</param>
        /// <param name="owner">The owning
        /// entity holding the reference</param>

        /// <param name="session">The current session to the db</param>
        private static void LazyInitialise(object proxy, object owner, ISession session)
        {
            if (null != proxy)
            {
                Type[] interfaces = proxy.GetType().GetInterfaces();
                foreach (Type iface in interfaces)
                {
                    if (iface == typeof (INHibernateProxy) || 
                        iface == typeof (IPersistentCollection))
                    {
                        if (!NHibernateUtil.IsInitialized(proxy))
                        {
                            if (iface == typeof (INHibernateProxy))
                                session.Lock(proxy, LockMode.None);
                            else //if (session.Contains(owner)) 
                                session.Lock(owner, LockMode.None);

                            NHibernateUtil.Initialize(proxy);
                        }

                        break;
                    }
                }
            }
        }

        #region ctor

        /// <summary>
        /// An alternative approach to initializes the
        /// <see cref="LazyInitializer"/> class.
        /// </summary>

        /// <remarks>
        /// this method should be called after
        /// the NH Cfg.Configuration object has been configured
        /// and before cfg.BuildSessionFactory(); has been called!
        /// This might be more demanding and difficult
        /// for those who work with DI tools. On the other hand
        /// this approach will work for ANY kind of mapping:
        /// Mapping.Attribute, Hbm and even NHibernate Fluent Interfaces.
        /// </remarks>
        public static void AlternativeConstructor()
        {
            var cfg = new Configuration();
            // get all types (with their lazy props) having lazy 
            // many/one-to-one properties
            var toOneQuery = from persistentClass in cfg.ClassMappings
                             let props = persistentClass.PropertyClosureIterator
                             select new { persistentClass.MappedClass, props }
                                 into selection
                                 from prop in selection.props
                                 where prop.Value is NHibernate.Mapping.ToOne
                                 where ((NHibernate.Mapping.ToOne)prop.Value).IsLazy
                                 group selection.MappedClass.GetProperty(prop.Name) 
                                       by selection.MappedClass;
            // get all types (with their lazy props) having lazy nh bag properties
            var bagQuery = from persistentClass in cfg.ClassMappings
                           let props = persistentClass.PropertyClosureIterator
                           select new { persistentClass.MappedClass, props }
                               into selection
                               from prop in selection.props
                               where prop.Value is NHibernate.Mapping.Collection
                               where ((NHibernate.Mapping.Collection)prop.Value).IsLazy
                               group selection.MappedClass.GetProperty(prop.Name) 
                                     by selection.MappedClass;
            // TODO: add queries of any other
            // mapping attribute you use that might be lazy.

            foreach (var value in toOneQuery)
                propertiesToInitialise.Add(value.Key, value.ToList());
            foreach (var value in bagQuery)
            {
                if (propertiesToInitialise.ContainsKey(value.Key))
                    propertiesToInitialise[value.Key].AddRange(value.ToList());
                else
                    propertiesToInitialise.Add(value.Key, value.ToList());
            }
            // TODO: add treatment of any other mapping
            // attribute you use that might be lazy.
        }

        /// <summary>

        /// Initializes the <see cref="LazyInitializer"/> class.
        /// </summary>
        static LazyInitializer()
        {
            // NOTE: you may prefer to pass assemblies as parameters.
            Assembly asm = Assembly.GetAssembly(typeof(MyApplication.Domain.IDomainObject));
            Type[] types = asm.GetTypes();
            foreach (Type type in types)
            {
                List<PropertyInfo> propertyInfos = new List<PropertyInfo>();
                object[] classAttributes = type.GetCustomAttributes(
                                              typeof(ClassAttribute), false);
                object[] joinedSubclassAttribute = 
                         type.GetCustomAttributes(typeof(JoinedSubclassAttribute), 
                    false);
                if (classAttributes.Length > 0 || joinedSubclassAttribute.Length > 0)
                {
                    AddProxies(type, classAttributes, joinedSubclassAttribute);

                    PropertyInfo[] properties = type.GetProperties();
                    foreach (PropertyInfo property in properties)
                    {
                        bool isLazy = false;

                        // Querying for descendants of BaseAttribute on property
                        // level to get all different types of properties
                        // that are used to describe properties. As most properties
                        // contain the Lazy property we will use
                        // reflection when looking up the method info
                        // to be invoked and also try-catching the whole thing as
                        // performance is not of major importance in a prescanning phase.
                        BaseAttribute[] attributes = (BaseAttribute[])
                           property.GetCustomAttributes(typeof(BaseAttribute), false);
                        foreach (BaseAttribute attribute in attributes)
                        {
                            PropertyInfo attributePropertyInfo = 
                                     attribute.GetType().GetProperty("Lazy");
                            if (attributePropertyInfo == null) continue;
                            object lazySetting = 
                              attributePropertyInfo.GetGetMethod().Invoke(
                              attribute, new object[0]);

                            if (lazySetting is bool && (bool)lazySetting)
                                isLazy = AddLazyPropertyToList(property, propertyInfos);
                            if (lazySetting is Laziness && 
                                     ((Laziness)lazySetting) == Laziness.Proxy)
                                isLazy = AddLazyPropertyToList(property, propertyInfos);
                            if (lazySetting is RestrictedLaziness &&

                                   ((RestrictedLaziness)lazySetting) == 
                                                RestrictedLaziness.Proxy)
                                isLazy = AddLazyPropertyToList(property, propertyInfos);

                            // skip iterating through attributes and go to next property
                            // if a lazy specification has been found.
                            if (isLazy) break;
                        }
                    }
                }
                if (propertyInfos.Count > 0)
                    propertiesToInitialise.Add(type, propertyInfos);
            }
        }

        private static bool AddLazyPropertyToList(PropertyInfo property, 
                            List<PropertyInfo> propertyInfos)
        {
            propertyInfos.Add(property);
            return true;
        }

        /// <summary>
        /// Adds the proxy types to the proxyTypes IList.
        /// </summary>
        /// <param name="type">The proxy type.</param>

        /// <param name="classAttributes">The type class attributes.</param>
        /// <param name="joinedSubclassAttribute">The type
        /// joined-subclass attributes</param>
        private static void AddProxies(Type type, object[] classAttributes, 
                            object[] joinedSubclassAttribute)
        {
            if (classAttributes.Length > 0)
            {
                if (((ClassAttribute)classAttributes[0]).Proxy != null)
                    proxyTypes.Add(type);
            }
            else if (((JoinedSubclassAttribute)joinedSubclassAttribute[0]).Proxy != null)
                proxyTypes.Add(type);
        }

        /// <summary>

        /// Initialises the class by calling the static ctor.
        /// </summary>
        public static void StaticInitialiser()
        {
            //Call static ctor.
        }
        
        #endregion
    }
}

关注点

我从这次经历中学到的一个教训是更好地了解 Configuration 类。我强烈建议那些使用 NHibernate 的人至少快速浏览一下它;根据您对 ORM 的使用情况,您可能会发现一些非常有用的技巧。

结论

我在此提供的类远非减少 StaleObjectStateException 损坏的最终方法。减少损坏的方法是缩短会话的生命周期。然而,当这样做时,在访问延迟属性时会话将不会打开;因此,我们使用 LazyInitializer 类。

欢迎您提出任何想法、批评、建议或问题。

历史

  • 2009年2月21日:初稿。
  • 2009年2月23日:对文章进行了少量补充。
  • 2009年3月2日:修复了 AlternativeConstructor() 方法。
© . All rights reserved.