在前面的一篇文章中,提到 IProviderService 接口的时候,我们附加了一个 ProviderContext,该对象中仅包含了一个当前的 IDatabase。因为在使用插件的时候,或多或少会用到 IDatabase 来进行处理。
但是,这感觉这是个累赘,也不雅观,本篇期望达到的目的是,在定义一个IDatabase的变量域范围内,任何代码都能够通过一个静态方法就能够获取到 IDatabase,而无需将 IDatabase带着满街跑。
借助TransactionScope的思想,来实现一个 DatabaseScope,目的就是解决 IDatabase 的传递问题。
一、Scope<T> 类
这个类可以作为其他扩展,它已经解决了最基本的问题。该类主要由静态的Current属性与外界进行联系,同时还是临时数据的存放容器。
/// 抽象类,用于在当前线程内标识一组用户定义的数据,能够确保这些数据在当前线程内唯一。
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class Scope<T> : IDisposable where T : Scope<T>
{
private readonly Dictionary<string, object> m_data = new Dictionary<string, object>();
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,即不使用单例。
/// 在当前线程范围内检索 <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 { get; private set; }
}
三、Database的改动
需要在 Database 实例里构造一个 DatabaseScope 对象,并且在 Database 销毁时也将 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>
/// <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;
}
四、建立测试程序
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>());
}
}
运行测试程序,输出为:
连接字符串: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 对象了,真是方便,如:
/// 实用于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 不有时,抛出一个异常。
/// 从 <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>
/// <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 = null, string 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>
public partial class UserDA : EntityPersister<User>
{
/// <summary>
/// 初始化 <see cref="UserDA" /> 类的新实例。
/// </summary>
/// <param name="instanceName">实例名。</param>
public UserDA(string instanceName = null)
: base (instanceName)
{
}
}
再看一下BL层的代码定义:
/// 管理员 业务逻辑类
/// </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 = null, bool 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 = null) where 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>,这样就不存在延迟所产生的影响。