在前面的一篇文章中,提到 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<
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,即不使用单例。
///
<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 {
get;
private
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 =
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>
///
管理员 数据访问类。
///
</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 =
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>,这样就不存在延迟所产生的影响。