posts - 94, comments - 346, trackbacks - 10, articles - 0
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Fireasy.Data系列——线程内IDatabase的传递

Posted on 2012-06-02 01:26  faib  阅读(...)  评论(... 编辑 收藏

    在前面的一篇文章中,提到 IProviderService 接口的时候,我们附加了一个 ProviderContext,该对象中仅包含了一个当前的 IDatabase。因为在使用插件的时候,或多或少会用到 IDatabase 来进行处理。

    但是,这感觉这是个累赘,也不雅观,本篇期望达到的目的是,在定义一个IDatabase的变量域范围内,任何代码都能够通过一个静态方法就能够获取到 IDatabase,而无需将 IDatabase带着满街跑。

    借助TransactionScope的思想,来实现一个 DatabaseScope,目的就是解决 IDatabase 的传递问题。

 

    一、Scope<T> 类

    这个类可以作为其他扩展,它已经解决了最基本的问题。该类主要由静态的Current属性与外界进行联系,同时还是临时数据的存放容器。

 

    /// <summary>
    
/// 抽象类,用于在当前线程内标识一组用户定义的数据,能够确保这些数据在当前线程内唯一。
    
/// </summary>
    
/// <typeparam name="T"></typeparam>
    public abstract class Scope<T> : IDisposable where T : Scope<T>
    {
        private readonly Dictionary<stringobject> m_data = new Dictionary<stringobject>();
        private readonly bool m_isSingleton;

        [ThreadStatic]
        private static Stack<T> m_stack = new Stack<T>();

        /// <summary>
        
/// 获取当前线程范围内的当前 <typeparam name="T"></typeparam> 对象。
        
/// </summary>
        public static T Current
        {
            get
            {
                var stack = GetScopeStack();
                return stack.Count == 0 ? null : stack.Peek();
            }
        }

        /// <summary>
        
/// 初始化类的新实例。
        
/// </summary>
        
/// <param name="singleton">是否为单例模式。</param>
        protected Scope(bool singleton = true)
        {
            m_isSingleton = singleton;
            var stack = GetScopeStack();
            if (singleton)
            {
                if (stack.Count == 0)
                {
                    stack.Push((T)this);
                }
            }
            else
            {
                stack.Push((T)this);
            }
        }

        /// <summary>
        
/// 在当前范围内添加一个数据。
        
/// </summary>
        
/// <typeparam name="TV"></typeparam>
        
/// <param name="key">键名。</param>
        
/// <param name="data">数据值。</param>
        public void SetData<TV>(string key, TV data)
        {
            m_data.AddOrReplace(key, data);
        }

        /// <summary>
        
/// 获取当前范围内指定键名的数据。
        
/// </summary>
        
/// <typeparam name="TV"></typeparam>
        
/// <param name="key">键名。</param>
        
/// <returns></returns>
        public TV GetData<TV>(string key)
        {
            object data;
            m_data.TryGetValue(key, out data);
            if (data is TV)
            {
                return (TV)data;
            }
            return default(TV);
        }

        /// <summary>
        
/// 清除当前范围内的所有数据。
        
/// </summary>
        public void ClearData()
        {
            m_data.Clear();
        }

        /// <summary>
        
/// 清除当前范围内指定键名的数据。
        
/// </summary>
        
/// <param name="keys">一组表示键名的字符串。</param>
        public void RemoveData(params string[] keys)
        {
            if (keys == null)
            {
                return;
            }
            foreach (var key in keys.Where(key => m_data.ContainsKey(key)))
            {
                m_data.Remove(key);
            }
        }

        /// <summary>
        
/// 释放当前范围的数据。
        
/// </summary>
        public virtual void Dispose()
        {
            var stack = GetScopeStack();
            if (stack.Count > 0)
            {
                if (m_isSingleton)
                {
                    //单例模式下,要判断是否与 current 相等
                    if (stack.Peek().Equals(this))
                    {
                        stack.Pop();
                    }
                }
                else
                {
                    stack.Pop();
                }
            }
        }

        private static Stack<T> GetScopeStack()
        {
            //如果不是 web 程序,则使用 ThreadStatic 标记的静态变量
            if (HttpContext.Current == null)
            {
                return m_stack ?? (m_stack = new Stack<T>());
            }
            //如果是 web 程序,则使用 HttpContext.Current.Items
            else
            {
                var tt = "__scope:" + typeof(T).FullName;
                if (HttpContext.Current.Items[tt] == null)
                {
                    HttpContext.Current.Items[tt] = new Stack<T>();
                }
                return (Stack<T>)HttpContext.Current.Items[tt];
            }
        }
    }

    在构造 Scope 时,singleton起到一个很好的作用,如果该值为 true,则在同一线程中,多次定义 Scope时只会将第一个 Scope 设置为 Current,TransactionScope 就需要这种模式;如果该值为 false,则使用一个栈来保存 Scope ,这样能够保证在多个 T 变量范围内,能够准确的取到相应的 T。

    在 web 程序中,ThreadStatic 标记的变量无法初始化,因此需要使用HttpContext.Current.Items 集合来放置这个栈。

 

    二、DatabaseScope 类

    继承自 Scope<T> 类,提供一个只读属性 Database。并且构造时传入 singleton 为 false,即不使用单例。

 

    /// <summary>
    
/// 在当前线程范围内检索 <see cref="IDatabase"/> 对象。
    
/// </summary>
    public sealed class DatabaseScope : Scope<DatabaseScope>
    {
        /// <summary>
        
/// 初始化 <see cref="DatabaseScope"/> 类的新实例。
        
/// </summary>
        
/// <param name="database">当前的 <see cref="IDatabase"/> 对象。</param>
        internal DatabaseScope(IDatabase database)
            : base (false)
        {
            Database = database;
        }

        /// <summary>
        
/// 返回当前线程内的 <see cref="IDatabase"/> 对象。
        
/// </summary>
        public IDatabase Database { getprivate set; }
    }

 

     三、Database的改动

     需要在 Database 实例里构造一个 DatabaseScope 对象,并且在 Database 销毁时也将 scope 销毁。

     构造函数的改动:

 

        private DatabaseScope m_scope;

        protected Database()
        {
            m_tranStack = new TransactionStack();
        }

        /// <summary>
        
/// 初始化 <see cref="Database"/> 类的新实例。
        
/// </summary>
        
/// <param name="connectionString">数据库连接字符串。</param>
        
/// <param name="provider">数据库提供者。</param>
        public Database (ConnectionString connectionString, IProvider provider)
            : this ()
        {
            Guard.ArgumentNull(provider, "provider");
            Provider = provider;
            ConnectionString = connectionString;
            m_scope = new DatabaseScope(this);
        }

 

    Dispose 函数的改动:

 

        /// <summary>
        
/// 释放所使用的非托管资源。
        
/// </summary>
        
/// <param name="disposing">为 true 则释放托管资源和非托管资源;为 false 则仅释放非托管资源。</param>
        protected virtual void Dispose(bool disposing)
        {
            if (m_disposed)
            {
                return;
            }
            if (disposing)
            {
                if (Transaction != null)
                {
                    Transaction.Dispose();
                    Transaction = null;
                }
                if (Connection != null)
                {
                    Connection.Dispose();
                    Connection = null;
                }
            }
            m_scope.Dispose();
            m_disposed = true;
        }

 

    四、建立测试程序

 

    [TestFixture]
    public class DatabaseScopeTests
    {
        /*
         IDatabase 必须显示或隐式的Dispose
        
*/
        [Test]
        public void Test()
        {
            using (var database = DatabaseFactory.CreateDatabase("sqlserver"))
            {
                TestNested1();

                //这里输出的是sqlserver的连接字符串
                Print("sqlserver");
            }

            //DatabaseScope.Current 已经卸载了
            Print(null);
        }

        private void TestNested1()
        {
            using (var database = DatabaseFactory.CreateDatabase("mysql"))
            {
                //这里输出的是mysql的连接字符串
                Print("mysql");

                TestNested2();
            }
        }

        private void TestNested2()
        {
            using (var database = DatabaseFactory.CreateDatabase("oracle"))
            {
                //这里输出的是oracle的连接字符串
                Print("oracle");
            }
        }

        private void Print(string instanceName)
        {
            if (DatabaseScope.Current == null)
            {
                Console.WriteLine("DatabaseScope.Current 已经卸载了");
                return;
            }
            Console.WriteLine("=======" + instanceName + "=======");
            Console.WriteLine("连接字符串:" + DatabaseFactory.GetByScope().ConnectionString);
            Console.WriteLine("SyntaxProvider:" + DatabaseFactory.GetByScope().Provider.GetService<ISyntaxProvider>());
            Console.WriteLine("SchemaProvider:" + DatabaseFactory.GetByScope().Provider.GetService<ISchemaProvider>());
        }
    }

    运行测试程序,输出为:

 

=======mysql=======
连接字符串:Data Source=localhost;database=test;User Id=root;password=faib;pooling=true;
SyntaxProvider:Fireasy.Data.Syntax.MySqlSyntax
SchemaProvider:Fireasy.Data.Schema.MySqlSchema
=======oracle=======
连接字符串:Data Source=local;User ID=Northwind;Password=faib;
SyntaxProvider:Fireasy.Data.Syntax.OracleSyntax
SchemaProvider:Fireasy.Data.Schema.OracleSchema
=======sqlserver=======
连接字符串:data source=(local);user id=sa;password=123;initial catalog=Northwind;
SyntaxProvider:Fireasy.Data.Syntax.MsSqlSyntax
SchemaProvider:Fireasy.Data.Schema.MsSqlSchema
DatabaseScope.Current 已经卸载了

 

    通过这番改造,所有的插件都不需要传递 IDatabase 对象了,真是方便,如:

 

    /// <summary>
    
/// 实用于MsSql的数据库备份与恢复。
    
/// </summary>
    public sealed class MsSqlBackup : IBackupProvider
    {
        /// <summary>
        
/// 对指定的数据库进行备份。
        
/// </summary>
        
/// <param name="option">备份选项。</param>
        public void Backup(BackupOption option)
        {
            Guard.ArgumentNull(option, "option");
            Guard.ArgumentNull(DatabaseScope.Current, "DatabaseScope.Current");

            using (var connection = DatabaseFactory.GetByScope().CreateConnection())
            {
                try
                {
                    if (string.IsNullOrEmpty(option.Database))
                    {
                        option.Database = connection.Database;
                    }

                    connection.OpenClose(() =>
                        {
                            var sql = string.Format("BACKUP DATABASE {0} TO DISK = '{1}'", option.Database, option.FileName);
                            using (var command = DatabaseFactory.GetByScope().Provider.CreateCommand(connection, null, sql))
                            {
                                command.ExecuteNonQuery();
                            }
                        });
                }
                catch (Exception exp)
                {
                    throw new BackupException(exp);
                }
            }
        }

        /// <summary>
        
/// 使用指定的备份文件恢复数据库。
        
/// </summary>
        
/// <param name="option">备份选项。</param>
        public void Restore(BackupOption option)
        {
            Guard.ArgumentNull(option, "option");
            var sb = new StringBuilder();
            sb.AppendFormat("RESTORE DATABASE {0} FROM DISK = '{1}'", option.Database, option.FileName);
            using (var connection = DatabaseFactory.GetByScope().CreateConnection())
            {
                try
                {
                    connection.TryOpen();
                    using (var command = DatabaseFactory.GetByScope().Provider.DbProviderFactory.CreateCommand())
                    {
                        command.CommandText = sb.ToString();
                        command.Connection = connection;
                        command.ExecuteNonQuery();
                    }
                }
                catch (Exception exp)
                {
                    throw new BackupException(exp);
                }
            }
        }
    }

    DatabaseFactory.GetByScope是对DatabaseScope.Current的有效判断,如果 Current 不有时,抛出一个异常。

 

        /// <summary>
        
/// 从 <see cref="DatabaseScope"/> 中获取 <see cref="IDatabase"/> 对象。
        
/// </summary>
        
/// <returns></returns>
        public static IDatabase GetByScope()
        {
            if (DatabaseScope.Current == null)
            {
                throw new UnableGetDatabaseScopeException();
            }
            return DatabaseScope.Current.Database;
        }

 

    但是,当使用 Linq 查询延迟加载时,如果处理不当,这个 DatabaseScope 将无效。以下分两种情况进行分析:

    (1)、正常的情况,直接使用 EntityPerisiter。

 

        /// <summary>
        
/// 获取模板列表。
        
/// </summary>
        
/// <param name="type">模板类别。</param>
        
/// <param name="ownerId">所属ID。</param>
        
/// <param name="categoryId">分类ID。</param>
        
/// <param name="keyword">关键字。</param>
        
/// <param name="state">状态。</param>
        
/// <param name="pager">分页参数。</param>
        
/// <returns></returns>
        public IEnumerable<Template> GetTemplates(TemplateType type, string ownerId, string categoryId = nullstring keyword = null, TemplateState? state = null, IDataPager pager = null)
        {
            using (var persister = new EntityPersister<TemplateModel>())
            {
                var category = !string.IsNullOrEmpty(categoryId) ? persister.Query<TemplateCategoryModel>(s => s.OwnerId == ownerId && s.CategoryId == categoryId).FirstOrDefault() : null;

                var list = persister.Query(s => s.OwnerId == ownerId && s.Type == type && s.State != TemplateState.Invalid)
                    .AssertWhere(category != null, s => s.CategoryId.StartsWith(category.InnerId))
                    .AssertWhere(!string.IsNullOrEmpty(keyword), s => s.Name.Contains(keyword))
                    .AssertWhere(state != null, s => s.State == state.Value)
                    .OrderBy(s => s.Name)
                    .Segment(pager as IDataSegment);
                return list.ToList().Select(ConvertTemplate);
            }
        }

 

    伴随着 EntityPerister 的 Dispose ,IDatabase才被 Dispose,因此这个Linq查询并不存在延迟的问题,在这期间,解释器和执行器都能够从 DatabaseScope得到 IDatabase。

 

    (2)、使用三层架构

    首先看一下DA层的代码定义,这个类非常简单:

 

    /// <summary>
    
/// 管理员 数据访问类。
    
/// </summary>
    public partial class UserDA : EntityPersister<User>
    {
        /// <summary>
        
/// 初始化 <see cref="UserDA" /> 类的新实例。
        
/// </summary>
        
/// <param name="instanceName">实例名。</param>
        public UserDA(string instanceName = null)
          : base (instanceName)
        {
        }
    }

 

    再看一下BL层的代码定义:

 

    /// <summary>
    
/// 管理员 业务逻辑类
    
/// </summary>
    public partial class UserBL
    {
        private readonly string _instanceName;

        /// <summary>
        
/// 初始化 <see cref="UserBL" /> 类的新实例。
        
/// </summary>
        
/// <param name="instanceName">实例名。</param>
        public UserBL(string instanceName = null)
        {
            _instanceName = instanceName;
        }

        #region 模板生成的方法
        /// <summary>
        
/// 创建 <see cref="UserDA" /> 的新实例。
        
/// </summary>
        
/// <returns></returns>
        protected UserDA CreateDAObject()
        {
            return new UserDA(_instanceName);
        }

        /// <summary>
        
/// 使用 <see cref="UserDA" /> 来操作一组方法。
        
/// </summary>
        
/// <param name="action">要执行的方法。</param>
        protected void UsingDA(Action<UserDA> action)
        {
            using (var da = CreateDAObject())
            {
                if (action != null)
                {
                    action(da);
                }
            }
        }

        /// <summary>
        
/// 使用 <see cref="UserDA" /> 来返回一个对象。
        
/// </summary>
        
/// <param name="action">要执行的函数。</param>
        protected T UsingRetDA<T>(Func<UserDA, T> func)
        {
            using (var da = CreateDAObject())
            {
                if (func != null)
                {
                    return func(da);
                }
            }
            return default(T);
        }

        /// <summary>
        
/// 使用 <see cref="UserDA" /> 上下文来操作一组方法。
        
/// </summary>
        
/// <param name="action">要执行的方法。</param>
        protected void UsingDAContext(Action<UserDA> action)
        {
            using (var context = new EntityPersistentScope())
            {
                using (var da = CreateDAObject())
                {
                    if (action != null)
                    {
                        action(da);
                    }
                }
                context.Commit();
            }
        }

        /// <summary>
        
/// 将一个新的实体对象创建到数据库。
        
/// </summary>
        
/// <param name="entity">要创建的实体对象。</param>
        public void Create(User entity)
        {
            UsingDA(da => da.Create(entity));
        }

        /// <summary>
        
/// 将实体对象的改动保存到数据库。
        
/// </summary>
        
/// <param name="entity">要保存的实体对象。</param>
        public void Save(User entity)
        {
            UsingDA(da => da.Save(entity));
        }

        /// <summary>
        
/// 将一组实体对象的更改保存到数据库。不会更新实体的其他引用属性。
        
/// </summary>
        
/// <param name="entities">要保存的实体序列。</param>
        public void Save(IEnumerable<User> entities)
        {
            UsingDA(da => da.Save(entities));
        }

        /// <summary>
        
/// 使用一个参照的实体对象更新满足条件的一序列对象。
        
/// </summary>
        
/// <param name="predicate">用于测试每个元素是否满足条件的函数。</param>
        
/// <param name="entity">保存的参考对象。</param>
        public void Update(User entity, Expression<Func<User, bool>> predicate = null)
        {
            UsingDA(da => da.Update(entity, predicate));
        }

        /// <summary>
        
/// 将指定的实体对象从数据库中移除。
        
/// </summary>
        
/// <param name="entity">要移除的实体对象。</param>
        
/// <param name="fake">如果具有 IsDeletedKey 属性,则提供对数据假删除的支持。</param>
        public void Remove(User entity, bool fake = true)
        {
            UsingDA(da => da.Remove(entity));
        }

        /// <summary>
        
/// 根据主键值将对象从数据库中移除。
        
/// </summary>
        
/// <param name="primaryValues">主键的值。数组的长度必须与实体所定义的主键相匹配。</param>
        
/// <param name="fake">如果具有 IsDeletedKey 属性,则提供对数据假删除的支持。</param>
        public void Remove(object[] primaryValues, bool fake = true)
        {
            UsingDA(da => da.Remove(primaryValues, fake));
        }

        /// <summary>
        
/// 将满足条件的一组对象从数据库中移除。
        
/// </summary>
        
/// <param name="predicate">用于测试每个元素是否满足条件的函数。</param>
        
/// <param name="fake">如果具有 IsDeletedKey 的属性,则提供对数据假删除的支持。</param>
        public void Remove(Expression<Func<User, bool>> predicate = nullbool fake = true)
        {
            UsingDA(da => da.Remove(predicate, fake));
        }

        /// <summary>
        
/// 返回满足条件的一组实体对象。
        
/// </summary>
        
/// <param name="predicate">用于测试每个元素是否满足条件的函数。</param>
        
/// <returns></returns>
        public QuerySet<User> Query(Expression<Func<User, bool>> predicate = null)
        {
            return UsingRetDA(da => da.Query(predicate));
        }

        /// <summary>
        
/// 返回满足条件的一组对象。
        
/// </summary>
        
/// <typeparam name="T">对象类型。</typeparam>
        
/// <param name="predicate">用于测试每个元素是否满足条件的函数。</param>
        
/// <returns></returns>
        public QuerySet<T> Query<T>(Expression<Func<T, bool>> predicate = null)
        {
            return UsingRetDA(da => da.Query<T>(predicate));
        }

        /// <summary>
        
/// 根据自定义的T-SQL语句查询返回一组对象,
        
/// </summary>
        
/// <typeparam name="T">对象类型。</typeparam>
        
/// <param name="queryCommand">查询命令。</param>
        
/// <param name="setment">数据分段对象。</param>
        
/// <param name="parameters">查询参数集合。</param>
        
/// <returns></returns>
        public IEnumerable<T> Query<T>(IQueryCommand queryCommand, IDataSegment setment = null, ParameterCollection parameters = nullwhere T : new()
        {
            return UsingRetDA(da => da.Query<T>(queryCommand, setment, parameters));
        }

        /// <summary>
        
/// 返回满足条件的一组实体对象。
        
/// </summary>
        
/// <param name="condition">一般的条件语句。</param>
        
/// <param name="orderBy">排序语句。</param>
        
/// <param name="setment">数据分段对象。</param>
        
/// <param name="parameters">查询参数集合。</param>
        
/// <returns></returns>
        public IEnumerable<User> Query(string condition, string orderBy, IDataSegment setment = null, ParameterCollection parameters = null)
        {
            return UsingRetDA(da => da.Query(condition, orderBy, setment, parameters));
        }

        /// <summary>
        
/// 使用主键值查询返回一个实体。
        
/// </summary>
        
/// <param name="primaryValues">主键的值。数组的长度必须与实体所定义的主键相匹配。</param>
        
/// <returns></returns>
        public User First(params object[] primaryValues)
        {
            return UsingRetDA(da => da.First(primaryValues));
        }
        #endregion

        #region 自定义代码
        //code-begin

        /// <summary>
        
/// 登录并返回用户信息
        
/// </summary>
        
/// <param name="account"></param>
        
/// <param name="password"></param>
        
/// <returns></returns>
        public User Login(string account, string password)
        {
            return Query(s => s.Enabled && s.Account == account && s.Password == password).FirstOrDefault();
        }
        //code-end
        #endregion
    }

    在页面端进行用户登录验证时,是直接调用Login方法的,但此时,发抛出 UnableGetDatabaseScopeException 异常。我们来分析一下:

    在Query方法内,是通过使用 using 一个 DA 对象来进行查询的,Query方法返回的是一个 QuerySet<User> 集合,它支持 Linq 的查询,因此,从 Query 调用来看,存在着延迟,在没有返回数据之前,DA已经Dispose了,随之IDatabase也Dispose了,在对 Linq 进行解释的时候,已经取不到 IDatabase了。

    解决这个问题的一种方法是,让BL也实现IDisposable接口,在实例期间,不要销毁 Da 对象,而是在Dispose里进行销毁。另一种方法就是对 Linq的查询返回结果不使用QuerySet<T> 而是将它转换为 List<T>,这样就不存在延迟所产生的影响。