NHibernate 的延迟初始化






4.75/5 (3投票s)
减少多用户 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 作为键,以 PropertyInfo
或 MethodInfo
的列表作为值。然后,此字典将在 ExtractNHMappedProperties
方法中使用,以初始化特定实体的延迟属性。我们通过实体类型获取延迟属性 - 如果我们的字典包含该实体类型,则意味着至少有一个延迟属性...
该方法被递归调用,以便我们可以根据需要深入到实体树。LazyInitialise
方法是执行实体/属性实际初始化的方法,使用 NHibernateUtil.Initialize(proxy);
。InitializeEntity
、InitializeCompletely
和 ImmediateLoad
方法是 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()
方法。