Implementing NHibernate session hooks

- Configuration events (ISessionConfigurationEvents) and
 - Interceptors (ISessionInterceptor/AbstractSessionInterceptor).

--HG--
branch : 1.x
This commit is contained in:
Piotr Szmyd
2013-05-07 20:40:24 +02:00
parent 46ed7e40f8
commit 159fc5feec
9 changed files with 422 additions and 21 deletions

View File

@@ -0,0 +1,233 @@
using System.Collections;
using NHibernate;
using NHibernate.SqlCommand;
using NHibernate.Type;
namespace Orchard.Data {
/// <summary>
/// Abstract implementation of a per-session NHibernate session interceptor.
/// </summary>
public abstract class AbstractSessionInterceptor : ISessionInterceptor {
/// <summary>
/// Called just before an object is initialized
/// </summary>
/// <param name="entity"/><param name="id"/><param name="propertyNames"/><param name="state"/><param name="types"/>
/// <remarks>
/// The interceptor may change the <c>state</c>, which will be propagated to the persistent
/// object. Note that when this method is called, <c>entity</c> will be an empty
/// uninitialized instance of the class.
/// </remarks>
/// <returns>
/// <see langword="true"/> if the user modified the <c>state</c> in any way
/// </returns>
public virtual bool OnLoad(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
return false;
}
/// <summary>
/// Called when an object is detected to be dirty, during a flush.
/// </summary>
/// <param name="currentState"/><param name="entity"/><param name="id"/><param name="previousState"/><param name="propertyNames"/><param name="types"/>
/// <remarks>
/// The interceptor may modify the detected <c>currentState</c>, which will be propagated to
/// both the database and the persistent object. Note that all flushes end in an actual
/// synchronization with the database, in which as the new <c>currentState</c> will be propagated
/// to the object, but not necessarily (immediately) to the database. It is strongly recommended
/// that the interceptor <b>not</b> modify the <c>previousState</c>.
/// </remarks>
/// <returns>
/// <see langword="true"/> if the user modified the <c>currentState</c> in any way
/// </returns>
public virtual bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) {
return false;
}
/// <summary>
/// Called before an object is saved
/// </summary>
/// <param name="entity"/><param name="id"/><param name="propertyNames"/><param name="state"/><param name="types"/>
/// <remarks>
/// The interceptor may modify the <c>state</c>, which will be used for the SQL <c>INSERT</c>
/// and propagated to the persistent object
/// </remarks>
/// <returns>
/// <see langword="true"/> if the user modified the <c>state</c> in any way
/// </returns>
public virtual bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
return false;
}
/// <summary>
/// Called before an object is deleted
/// </summary>
/// <param name="entity"/><param name="id"/><param name="propertyNames"/><param name="state"/><param name="types"/>
/// <remarks>
/// It is not recommended that the interceptor modify the <c>state</c>.
/// </remarks>
public virtual void OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
}
/// <summary>
/// Called before a collection is (re)created.
/// </summary>
public virtual void OnCollectionRecreate(object collection, object key) {
}
/// <summary>
/// Called before a collection is deleted.
/// </summary>
public virtual void OnCollectionRemove(object collection, object key) {
}
/// <summary>
/// Called before a collection is updated.
/// </summary>
public virtual void OnCollectionUpdate(object collection, object key) {
}
/// <summary>
/// Called before a flush
/// </summary>
/// <param name="entities">The entities</param>
public virtual void PreFlush(ICollection entities) {
}
/// <summary>
/// Called after a flush that actually ends in execution of the SQL statements required to
/// synchronize in-memory state with the database.
/// </summary>
/// <param name="entities">The entitites</param>
public virtual void PostFlush(ICollection entities) {
}
/// <summary>
/// Called when a transient entity is passed to <c>SaveOrUpdate</c>.
/// </summary>
/// <remarks>
/// The return value determines if the object is saved
/// <list>
/// <item>
/// <see langword="true"/> - the entity is passed to <c>Save()</c>, resulting in an <c>INSERT</c>
/// </item>
/// <item>
/// <see langword="false"/> - the entity is passed to <c>Update()</c>, resulting in an <c>UPDATE</c>
/// </item>
/// <item>
/// <see langword="null"/> - Hibernate uses the <c>unsaved-value</c> mapping to determine if the object is unsaved
/// </item>
/// </list>
/// </remarks>
/// <param name="entity">A transient entity</param>
/// <returns>
/// Boolean or <see langword="null"/> to choose default behaviour
/// </returns>
public virtual bool? IsTransient(object entity) {
return null;
}
/// <summary>
/// Called from <c>Flush()</c>. The return value determines whether the entity is updated
/// </summary>
/// <remarks>
/// <list>
/// <item>
/// an array of property indicies - the entity is dirty
/// </item>
/// <item>
/// an empty array - the entity is not dirty
/// </item>
/// <item>
/// <see langword="null"/> - use Hibernate's default dirty-checking algorithm
/// </item>
/// </list>
/// </remarks>
/// <param name="entity">A persistent entity</param><param name="currentState"/><param name="id"/><param name="previousState"/><param name="propertyNames"/><param name="types"/>
/// <returns>
/// An array of dirty property indicies or <see langword="null"/> to choose default behavior
/// </returns>
public virtual int[] FindDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) {
return null;
}
/// <summary>
/// Instantiate the entity class. Return <see langword="null"/> to indicate that Hibernate should use the default
/// constructor of the class
/// </summary>
/// <param name="entityName">the name of the entity </param><param name="entityMode">The type of entity instance to be returned. </param><param name="id">the identifier of the new instance </param>
/// <returns>
/// An instance of the class, or <see langword="null"/> to choose default behaviour
/// </returns>
/// <remarks>
/// The identifier property of the returned instance
/// should be initialized with the given identifier.
/// </remarks>
public virtual object Instantiate(string entityName, EntityMode entityMode, object id) {
return null;
}
/// <summary>
/// Get the entity name for a persistent or transient instance
/// </summary>
/// <param name="entity">an entity instance </param>
/// <returns>
/// the name of the entity
/// </returns>
public virtual string GetEntityName(object entity) {
return null;
}
/// <summary>
/// Get a fully loaded entity instance that is cached externally
/// </summary>
/// <param name="entityName">the name of the entity </param><param name="id">the instance identifier </param>
/// <returns>
/// a fully initialized entity
/// </returns>
public virtual object GetEntity(string entityName, object id)
{
return null;
}
/// <summary>
/// Called when a NHibernate transaction is begun via the NHibernate <see cref="T:NHibernate.ITransaction"/>
/// API. Will not be called if transactions are being controlled via some other mechanism.
/// </summary>
public virtual void AfterTransactionBegin(ITransaction tx) {
}
/// <summary>
/// Called before a transaction is committed (but not before rollback).
/// </summary>
public virtual void BeforeTransactionCompletion(ITransaction tx) {
}
/// <summary>
/// Called after a transaction is committed or rolled back.
/// </summary>
public virtual void AfterTransactionCompletion(ITransaction tx) {
}
/// <summary>
/// Called when sql string is being prepared.
/// </summary>
/// <param name="sql">sql to be prepared </param>
/// <returns>
/// original or modified sql
/// </returns>
public virtual SqlString OnPrepareStatement(SqlString sql) {
return sql;
}
/// <summary>
/// Called when a session-scoped (and <b>only</b> session scoped) interceptor is attached
/// to a session
/// </summary>
/// <remarks>
/// session-scoped-interceptor is an instance of the interceptor used only for one session.
/// The use of singleton-interceptor may cause problems in multi-thread scenario.
/// </remarks>
/// <seealso cref="M:NHibernate.ISessionFactory.OpenSession(NHibernate.IInterceptor)"/><seealso cref="M:NHibernate.ISessionFactory.OpenSession(System.Data.IDbConnection,NHibernate.IInterceptor)"/>
public virtual void SetSession(ISession session) {
}
}
}

View File

@@ -0,0 +1,50 @@
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using NHibernate.Cfg;
using Orchard.Utility;
namespace Orchard.Data {
/// <summary>
/// Allows hooking into NHibernate session configuration pipeline.
/// </summary>
public interface ISessionConfigurationEvents : ISingletonDependency {
/// <summary>
/// Called when an empty fluent configuration object has been created,
/// before applying any default Orchard config settings (alterations, conventions etc.).
/// </summary>
/// <param name="cfg">Empty fluent NH configuration object.</param>
/// <param name="defaultModel">Default persistence model that is about to be used.</param>
void Created(FluentConfiguration cfg, AutoPersistenceModel defaultModel);
/// <summary>
/// Called when fluent configuration has been prepared but not yet built.
/// </summary>
/// <param name="cfg">Prepared fluent NH configuration object.</param>
void Prepared(FluentConfiguration cfg);
/// <summary>
/// Called when raw NHibernate configuration is being built, after applying all customizations.
/// Allows applying final alterations to the raw NH configuration.
/// </summary>
/// <param name="cfg">Raw NH configuration object being processed.</param>
void Building(Configuration cfg);
/// <summary>
/// Called when NHibernate configuration has been built or read from cache storage (mappings.bin file by default).
/// </summary>
/// <param name="cfg">Final, raw NH configuration object.</param>
void Finished(Configuration cfg);
/// <summary>
/// Called when configuration hash is being computed. If hash changes, configuration will be rebuilt and stored in mappings.bin.
/// This method allows to alter the default hash to take into account custom configuration changes.
/// </summary>
/// <remarks>
/// It's a developer responsibility to make sure hash is correctly updated when config needs to be rebuilt.
/// Otherwise the cached configuration (mappings.bin file) will be used as long as default Orchard configuration
/// is unchanged or until the file is manually removed.
/// </remarks>
/// <param name="hash">Current hash object</param>
void ComputingHash(Hash hash);
}
}

View File

@@ -0,0 +1,9 @@
using NHibernate;
namespace Orchard.Data {
/// <summary>
/// Describes an NHibernate session interceptor, instantiated per-session.
/// </summary>
public interface ISessionInterceptor : IInterceptor, IDependency {
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using FluentNHibernate;
using FluentNHibernate.Automapping;
@@ -9,7 +10,6 @@ using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Conventions.Helpers;
using FluentNHibernate.Diagnostics;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Engine;
using NHibernate.Event;
using NHibernate.Event.Default;
@@ -17,6 +17,8 @@ using NHibernate.Persister.Entity;
using Orchard.ContentManagement.Records;
using Orchard.Data.Conventions;
using Orchard.Environment.ShellBuilders.Models;
using Orchard.Logging;
using Configuration = NHibernate.Cfg.Configuration;
namespace Orchard.Data.Providers {
[Serializable]
@@ -24,16 +26,30 @@ namespace Orchard.Data.Providers {
public abstract IPersistenceConfigurer GetPersistenceConfigurer(bool createDatabase);
protected AbstractDataServicesProvider() {
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public Configuration BuildConfiguration(SessionFactoryParameters parameters) {
var database = GetPersistenceConfigurer(parameters.CreateDatabase);
var persistenceModel = CreatePersistenceModel(parameters.RecordDescriptors.ToList());
return Fluently.Configure()
.Database(database)
.Mappings(m => m.AutoMappings.Add(persistenceModel))
.ExposeConfiguration(cfg => cfg.EventListeners.LoadEventListeners = new ILoadEventListener[] { new OrchardLoadEventListener() })
.BuildConfiguration()
;
var config = Fluently.Configure();
parameters.Configurers.Invoke(c => c.Created(config, persistenceModel), Logger);
config = config.Database(database)
.Mappings(m => m.AutoMappings.Add(persistenceModel))
.ExposeConfiguration(cfg => {
cfg.EventListeners.LoadEventListeners = new ILoadEventListener[] {new OrchardLoadEventListener()};
parameters.Configurers.Invoke(c => c.Building(cfg), Logger);
});
parameters.Configurers.Invoke(c => c.Prepared(config), Logger);
return config.BuildConfiguration();
}
public static AutoPersistenceModel CreatePersistenceModel(ICollection<RecordBlueprint> recordDescriptors) {

View File

@@ -1,8 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using Orchard.Environment.ShellBuilders.Models;
namespace Orchard.Data.Providers {
public class SessionFactoryParameters : DataServiceParameters {
public SessionFactoryParameters() {
Configurers = Enumerable.Empty<ISessionConfigurationEvents>();
}
public IEnumerable<ISessionConfigurationEvents> Configurers { get; set; }
public IEnumerable<RecordBlueprint> RecordDescriptors { get; set; }
public bool CreateDatabase { get; set; }
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
@@ -17,13 +18,15 @@ namespace Orchard.Data {
private readonly ShellBlueprint _shellBlueprint;
private readonly IAppDataFolder _appDataFolder;
private readonly IHostEnvironment _hostEnvironment;
private readonly IEnumerable<ISessionConfigurationEvents> _configurers;
private ConfigurationCache _currentConfig;
public SessionConfigurationCache(ShellSettings shellSettings, ShellBlueprint shellBlueprint, IAppDataFolder appDataFolder, IHostEnvironment hostEnvironment) {
public SessionConfigurationCache(ShellSettings shellSettings, ShellBlueprint shellBlueprint, IAppDataFolder appDataFolder, IHostEnvironment hostEnvironment, IEnumerable<ISessionConfigurationEvents> configurers) {
_shellSettings = shellSettings;
_shellBlueprint = shellBlueprint;
_appDataFolder = appDataFolder;
_hostEnvironment = hostEnvironment;
_configurers = configurers;
_currentConfig = null;
Logger = NullLogger.Instance;
@@ -161,6 +164,8 @@ namespace Orchard.Data {
}
}
_configurers.Invoke(c => c.ComputingHash(hash), Logger);
return hash;
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using NHibernate;
using NHibernate.Cfg;
using Orchard.Data.Providers;
@@ -22,6 +23,7 @@ namespace Orchard.Data {
private readonly ShellBlueprint _shellBlueprint;
private readonly IHostEnvironment _hostEnvironment;
private readonly IDatabaseCacheConfiguration _cacheConfiguration;
private readonly Func<IEnumerable<ISessionConfigurationEvents>> _configurers;
private readonly IDataServicesProviderFactory _dataServicesProviderFactory;
private readonly IAppDataFolder _appDataFolder;
private readonly ISessionConfigurationCache _sessionConfigurationCache;
@@ -36,7 +38,8 @@ namespace Orchard.Data {
IAppDataFolder appDataFolder,
ISessionConfigurationCache sessionConfigurationCache,
IHostEnvironment hostEnvironment,
IDatabaseCacheConfiguration cacheConfiguration) {
IDatabaseCacheConfiguration cacheConfiguration,
Func<IEnumerable<ISessionConfigurationEvents>> configurers) {
_shellSettings = shellSettings;
_shellBlueprint = shellBlueprint;
_dataServicesProviderFactory = dataServicesProviderFactory;
@@ -44,6 +47,7 @@ namespace Orchard.Data {
_sessionConfigurationCache = sessionConfigurationCache;
_hostEnvironment = hostEnvironment;
_cacheConfiguration = cacheConfiguration;
_configurers = configurers;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
@@ -117,6 +121,8 @@ namespace Orchard.Data {
}
#endregion
parameters.Configurers.Invoke(c => c.Finished(config), Logger);
Logger.Debug("Done Building configuration");
return config;
}
@@ -128,6 +134,7 @@ namespace Orchard.Data {
var shellFolder = _appDataFolder.MapPath(shellPath);
return new SessionFactoryParameters {
Configurers = _configurers(),
Provider = _shellSettings.DataProvider,
DataFolder = shellFolder,
ConnectionString = _shellSettings.DataConnectionString,

View File

@@ -1,20 +1,28 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using NHibernate;
using NHibernate.SqlCommand;
using NHibernate.Type;
using Orchard.Exceptions;
using Orchard.Logging;
using Orchard.Security;
namespace Orchard.Data {
public class SessionLocator : ISessionLocator, ITransactionManager, IDisposable {
private readonly ISessionFactoryHolder _sessionFactoryHolder;
private readonly IEnumerable<ISessionInterceptor> _interceptors;
private ISession _session;
private ITransaction _transaction;
private bool _cancelled;
public SessionLocator(ISessionFactoryHolder sessionFactoryHolder) {
public SessionLocator(
ISessionFactoryHolder sessionFactoryHolder,
IEnumerable<ISessionInterceptor> interceptors) {
_sessionFactoryHolder = sessionFactoryHolder;
_interceptors = interceptors;
Logger = NullLogger.Instance;
}
@@ -96,77 +104,142 @@ namespace Orchard.Data {
var sessionFactory = _sessionFactoryHolder.GetSessionFactory();
Logger.Information("Opening database session");
_session = sessionFactory.OpenSession(new SessionInterceptor());
_session = sessionFactory.OpenSession(new OrchardSessionInterceptor(_interceptors.ToArray(), Logger));
}
class SessionInterceptor : IInterceptor {
class OrchardSessionInterceptor : IInterceptor {
private readonly ISessionInterceptor[] _interceptors;
private readonly ILogger _logger;
private ISession _session;
public OrchardSessionInterceptor(ISessionInterceptor[] interceptors, ILogger logger) {
_interceptors = interceptors;
_logger = logger;
}
bool IInterceptor.OnLoad(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
return false;
if (_interceptors.Length == 0) return false;
return _interceptors.Invoke(i => i.OnLoad(entity, id, state, propertyNames, types), _logger).ToList().Any(r => r);
}
bool IInterceptor.OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) {
return false;
if (_interceptors.Length == 0) return false;
return _interceptors.Invoke(i => i.OnFlushDirty(entity, id, currentState, previousState, propertyNames, types), _logger).ToList().Any(r => r);
}
bool IInterceptor.OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
return false;
if (_interceptors.Length == 0) return false;
return _interceptors.Invoke(i => i.OnSave(entity, id, state, propertyNames, types), _logger).ToList().Any(r => r);
}
void IInterceptor.OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.OnDelete(entity, id, state, propertyNames, types), _logger);
}
void IInterceptor.OnCollectionRecreate(object collection, object key) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.OnCollectionRecreate(collection, key), _logger);
}
void IInterceptor.OnCollectionRemove(object collection, object key) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.OnCollectionRemove(collection, key), _logger);
}
void IInterceptor.OnCollectionUpdate(object collection, object key) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.OnCollectionUpdate(collection, key), _logger);
}
void IInterceptor.PreFlush(ICollection entities) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.PreFlush(entities), _logger);
}
void IInterceptor.PostFlush(ICollection entities) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.PostFlush(entities), _logger);
}
bool? IInterceptor.IsTransient(object entity) {
return null;
if (_interceptors.Length == 0) return null;
return _interceptors.Invoke(i => i.IsTransient(entity), _logger).ToList().FirstOrDefault(c => c.HasValue && c.Value);
}
int[] IInterceptor.FindDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) {
return null;
if (_interceptors.Length == 0) return null;
var retVal = _interceptors.Invoke(i => i.FindDirty(entity, id, currentState, previousState, propertyNames, types), _logger)
.Where(r => r != null)
.SelectMany(r => r)
.ToArray();
return retVal.Length == 0 ? null : retVal;
}
object IInterceptor.Instantiate(string entityName, EntityMode entityMode, object id) {
return null;
if (_interceptors.Length == 0) return null;
return _interceptors.Invoke(i => i.Instantiate(entityName, entityMode, id), _logger).FirstOrDefault(r => r != null);
}
string IInterceptor.GetEntityName(object entity) {
return null;
if (_interceptors.Length == 0) return null;
return _interceptors.Invoke(i => i.GetEntityName(entity), _logger).FirstOrDefault(r => r != null);
}
object IInterceptor.GetEntity(string entityName, object id) {
return null;
if (_interceptors.Length == 0) return null;
return _interceptors.Invoke(i => i.GetEntity(entityName, id), _logger).FirstOrDefault(r => r != null);
}
void IInterceptor.AfterTransactionBegin(ITransaction tx) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.AfterTransactionBegin(tx), _logger);
}
void IInterceptor.BeforeTransactionCompletion(ITransaction tx) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.BeforeTransactionCompletion(tx), _logger);
}
void IInterceptor.AfterTransactionCompletion(ITransaction tx) {
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.AfterTransactionCompletion(tx), _logger);
}
SqlString IInterceptor.OnPrepareStatement(SqlString sql) {
return sql;
if (_interceptors.Length == 0) return sql;
// Cannot use Invoke, as we need to pass previous result to the next interceptor
return _interceptors.Aggregate(sql, (current, i) => {
try {
return i.OnPrepareStatement(current);
}
catch (Exception ex) {
if (IsLogged(ex)) {
_logger.Error(ex, "{2} thrown from ISessionInterceptor by {0}",
i.GetType().FullName,
ex.GetType().Name);
}
if (ex.IsFatal()) {
throw;
}
return current;
}
});
}
void IInterceptor.SetSession(ISession session) {
_session = session;
if (_interceptors.Length == 0) return;
_interceptors.Invoke(i => i.SetSession(session), _logger);
}
private static bool IsLogged(Exception ex) {
return ex is OrchardSecurityException || !ex.IsFatal();
}
}
}

View File

@@ -170,6 +170,7 @@
<Compile Include="ContentManagement\MetaData\Services\ISettingsFormatter.cs" />
<Compile Include="ContentManagement\QueryHints.cs" />
<Compile Include="ContentManagement\Utilities\ComputedField.cs" />
<Compile Include="Data\AbstractSessionInterceptor.cs" />
<Compile Include="Data\Bags\SArray.cs" />
<Compile Include="Data\FetchRequest.cs" />
<Compile Include="DisplayManagement\Arguments.cs" />
@@ -186,6 +187,8 @@
<Compile Include="Data\IDatabaseCacheConfiguration.cs" />
<Compile Include="Data\Migration\AutomaticDataMigrations.cs" />
<Compile Include="Data\Migration\Interpreters\SqlCeCommandInterpreter.cs" />
<Compile Include="Data\ISessionConfigurationEvents.cs" />
<Compile Include="Data\ISessionInterceptor.cs" />
<Compile Include="DisplayManagement\Descriptors\PlacementInfo.cs" />
<Compile Include="DisplayManagement\Descriptors\ResourceBindingStrategy\StylesheetBindingStrategy.cs" />
<Compile Include="DisplayManagement\Descriptors\ShapeDescriptor.cs" />