使用 ADO.NET 且无需企业服务的声明式事务






4.95/5 (18投票s)
2001年10月23日
8分钟阅读

219994

1501
有时,为简单的数据库应用程序制作原型是件好事。此代码通过在非 COM+ 环境中提供 COM+ 的自动事务模型,可能会有所帮助。本示例使用“拦截”为非 COM+ 类提供自动事务支持。
摘要与动机
我正在开发一个简单的内部数据库应用程序,它不需要具备成熟的企业级、高安全性、多线程、对象池、即时激活(JIT Activated)、异构分布式事务应用程序的全部要求(呼~)。它仅使用 SQL Server 作为后端。它不必扩展到支持海量用户。这是一个相当典型的内部简单数据库开发项目。我想要一些 COM+ 的优点,比如声明式事务,换句话说,我希望编写像下面这样简单易读的代码:
[DbConnection]
[Transaction(TransactionOption.Required)]
sealed public class Employees : ContextBoundObject
{
[AutoComplete]
public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
{
return (EmployeeDataSet)Sql.RunSP("Employee_GetById",
"Employees",
new EmployeeDataSet(),
Sql.CreateParameterWithValue("@EmployeeID", nID));
}
}
但我不想面对生成强名称、COM+ 注册、配置等与 COM+ 企业模型相关的所有“麻烦”和“额外步骤”。我知道 .NET 环境下的 COM+(企业级)支持非常易于使用和配置……但对于这样一个简单的项目来说,它似乎还是增加了一些额外的部署工作。同时,我也对学习更多关于 .NET 环境的知识感兴趣,从这个角度来看,我的动机就更有意义了,因为很明显我并没有节省任何时间。*笑*。闲话少说,我最终实现的是对 SQL Server 托管提供程序的“自动事务”支持和额外的“连接”支持(尽管所提供的源代码可以轻松修改以支持其他托管提供程序)。
这样你就可以像上面那样编写代码,而不是像下面这样:
sealed public class Employees
{
public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
{
SqlConnection Connection
= new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["dsn"]);
Connection.Open();
try
{
IDataParameter[] parameters =
{
SQL.CreateParameterWithValue("@EmployeeID", nID)
};
return (EmployeeDataSet)SQL.RunSP("Employee_GetById", parameters,
"Employees", new EmployeeDataSet(),
Connection);
}
finally
{
Connection.Dispose();
Connection = null;
}
}
}
这段代码稍长一些,更难阅读,而且仍然缺少手动事务支持。声明式自动事务的真正好处在于其因果效应,即 A 调用 B,B 调用 C,如果 C 失败,你希望中止整个事务。或者在你事先不知道你的对象将如何组合成事务单元的情况下,它也很有用。
它是如何工作的?
第一步是根据声明的特性(attribute)以某种方式将对象划分到事务中。因此,我们可以为一个类编写注解 [Transaction(TransactionOption.Required)]
,在运行时就会创建一个适当的事务“上下文(Context)
”。这个自定义特性将是一个“类
”特性,它使用“拦截(Interception)
”来挂钩类的方法,以便我们可以将事务支持注入到类中。我还决定支持 [AutoComplete]
这个“方法
”特性。此外,我创建了一个 [DbConnection]
特性来帮助自动化数据库连接。那么,让我们深入探讨吧。
上下文与类特性
我想在深入之前,我们必须简要地谈谈 .NET 的“上下文”。.NET Framework 文档对上下文的定义如下:上下文是一个有序的属性序列,它为驻留于其中的对象定义了一个环境。对于那些被配置为需要某些自动服务(如同步、事务、即时激活、安全性等)的对象,在激活过程中会创建上下文。多个对象可以存在于一个上下文中。哎呀!这听起来很吓人,但实际上,这是一个非常酷且非常有用的概念。基本上,它是一种将共享某些运行时属性的对象组合在一起的方法(不确定这样说是否更好 *笑*)。无论如何,上下文允许我们声明所有这些对象将共享此事务,或者这类对象总是需要一个单独的事务。上下文使我们能够区分并因此划分对象。
如何编写自定义特性的所有细节超出了本文的范围,但本质上它涉及从 System.Attribute
类之一派生并提供您自己的功能。让我们的 [Transaction]
特性工作的关键是派生自 System.Runtime.Remoting.Contexts.ContextAttribute
类,如下所示:
public enum TransactionOption // The transaction options.
{
Disabled = 0,
NotSupported,
Required,
RequiresNew,
Supported
}
[AttributeUsage(AttributeTargets.Class)]
public class TransactionAttribute : ContextAttribute
{
private TransactionOption transactionOption;
public TransactionAttribute(TransactionOption transactionOption)
: base("Transaction")
{
this.transactionOption = transactionOption; // Store the TransactionOption for later.
}
...
并实现方法 bool
IsContextOK(Context ctx, IConstructionCallMessage ctor)
。运行时会调用此方法,以检查此对象的上下文是否与传递给该方法的上下文兼容。因此……这允许我们根据声明的特性来区分我们的对象。
public override boolIsContextOK(Context ctx, IConstructionCallMessage ctor)
{
if(transactionOption == TransactionOption.RequiresNew) // This class always
// requires a new Context
return false;
TransactionProperty transactionProperty
= ctx.GetProperty(Transaction.PropertyName) as TransactionProperty;
if(transactionOption == TransactionOption.Required)
{
// If there is no existing transaction context then create a new one
if(transactionProperty == null)
return false;
}
return true; // The current context is fine!!
}
...
好……到目前为止一切顺利。我们可以根据声明的事务来区分和创建上下文。在继续讨论如何挂钩我们类的方法以注入事务支持之前,我们还必须讨论另一个主题。
上下文属性
上下文,像大多数其他对象一样,拥有定义和保存其状态的属性。好消息是这些属性可以由用户自定义。例如,我希望在上下文中存储一个属性,以便我能判断该上下文是否已经关联了一个事务。在很多方面,这相当于 ASP.NET 中的“<st1:place><st1:placename>会话<st1:placetype>状态”或任何其他类型的“名称-值字典”式查找。.NET Framework 提供了一种方法,让您可以在上下文中存储自定义属性,这是通过调用 System.ContextAttribute
类的以下方法实现的:
public
virtual void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
of the System.ContextAttribute
class. So our implementation is as follows
public override void GetPropertiesForNewContext(IConstructionCallMessage ctor)
{
ctor.ContextProperties.Add(this);
}
唯一的要求是该类实现 IContextProperty
接口,幸运的是 System.ContextAttribute
已经这样做了。
拦截(终于讲到了)
嗯……我漏掉了一小部分。为了让你的类参与到这个“上下文
”机制中,你必须将你的类标记为上下文绑定类(这意味着它在上下文中运行)。要做到这一点,你的类必须派生自 System.ContextBound
。好的。我想我们现在已经掌握了所有的要素……当你创建一个类的新实例时,根据我的理解,过程如下:
ContextAttribute.IsContextOK()
方法被调用。如果返回true
,则该类在传递给它的上下文中创建。- 如果返回
false
,则会创建一个新的上下文。 - 然后系统会在新创建的上下文上调用
GetPropertiesForNewContext()
。我们可以在这里将任何新的“属性
”附加到上下文中。 - 对于每一个属性,系统会测试你是否实现了
System.Runtime.Remoting.Contexts.IContributeObjectSink
接口。 - 此接口包含方法
IMessageSink
GetObjectSink(MarshalByRefObjectobj, IMessageSink nextSink)
,它允许你链接一个自定义的IMessageSink
接口,从而可以拦截对该对象的方法调用。
下面是一些示例代码,GetObjectSink
的实现如下:
...
public IMessageSink GetObjectSink(MarshalByRefObject o, IMessageSinkm_Next)
{
TransactionAttribute transactionProperty = Transaction.ContextProperty;
if(transactionProperty != null)
{
return new DbConnectionMessageSink(this,
new TransactionMessageSink(transactionProperty, m_Next));
}
return new DbConnectionMessageSink(this, m_Next);
}
值得注意的一点是,该实现总是先链接 Connection
特性,以便它的方法挂钩在事务挂钩之前被调用。创建新实例时,即使将 Connection
特性放在 Transaction
特性之前,也无法保证它会先被调用。我们需要一种方法来控制事物的顺序,否则我们可能会在没有相应打开连接的情况下尝试创建事务。你总是希望(蓝色部分为注入的代码):
Connection.OpenConnection()
Transaction.Begin()
MethodCallGoesHere()
Transaction.Commit()
Connection.CloseConnection()
要完成拦截的最后一击,是 IMessageSink
接口的实际实现。当上下文中的对象上的方法被调用时,该方法会通过 IMessageSink
接口进行重定向。正如我们所发现的,这实际上是一个 IMessageSink
实现链,我们的挂钩就是其中之一。对于所有 COM/Remoting 的爱好者来说,这闻起来有点像代理/存根(Proxy/Stub)。总之,我们感兴趣的 IMessageSink
接口上的主要方法是 public IMessageSync ProcessMessage(IMessageimCall)
。通过实现此方法,它允许我们挂钩“同步”方法调用。TransactionAttribute
消息接收器的实际实现如下:
public class TransactionMessageSink : IMessageSink
{
private IMessageSink m_Next;
private TransactionAttribute m_TransactionAttribute;
internal TransactionMessageSink(TransactionAttribute transactionProperty, IMessageSinkims)
{
m_Next = ims;
m_TransactionAttribute = transactionProperty;
}
public IMessageSink NextSink
{
get { returnm_Next; }
}
public IMessageSync ProcessMessage(IMessageimCall)
{
// Perform whatever preprocessing is needed on the message
if (!(imCallisIMethodMessage))
returnm_Next.SyncProcessMessage(imCall);
IMethodMessage imm = imCall as IMethodMessage;
bool bAutoComplete = (Attribute.GetCustomAttribute(imm.MethodBase,
typeof(AutoCompleteAttribute)) != null);
m_TransactionAttribute.DisableCommit();
SqlConnection Connection = (SqlConnection)DbConnectionAttribute.Connection;
if(Connection == null)
return m_Next.SyncProcessMessage(imCall);
#if DEBUGGING_TRXS
Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" +
" Beginning Transaction...");
#endif
SqlTransaction dbTransaction = m_TransactionAttribute.DbTransaction
= Connection.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted);
// Dispatch the call on the object
IMessage imReturn = m_Next.SyncProcessMessage(imCall);
if(dbTransaction != null)
{
IMethodReturnMessage methodReturn = imReturn as IMethodReturnMessage;
Exception exc = methodReturn.Exception;
if (exc != null)
{
m_TransactionAttribute.SetAbort();
}
else
{
if(bAutoComplete)
m_TransactionAttribute.SetComplete();
}
if(!m_TransactionAttribute.Done)
m_TransactionAttribute.SetAbort();
if(m_TransactionAttribute.ContextConsistent)
{
#if DEBUGGING_TRXS
Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" +
" Committing Transaction...");
#endif
dbTransaction.Commit();
}
else
{
#if DEBUGGING_TRXS
Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" +
" Aborting Transaction...");
#endif
dbTransaction.Rollback();
}
dbTransaction.Dispose();
dbTransaction = null;
}
returnimReturn;
}
public IMessageCtrlAsync ProcessMessage(IMessage im, IMessageSink ims)
{
// TODO: Find some way to also allow AsyncMessages to work (and ideally, be tracked)
return m_Next.AsyncProcessMessage(im, ims);
}
}
我的天,这代码真多。一个关键部分是 IMessage imReturn = m_Next.SyncProcessMessage(imCall);
,这个调用将方法调用转发给对象。另一部分只是将事务包装在该调用周围。
整合
文章的标题说是简单的,但我们至今还没看到任何简单的东西。这是因为“简单”指的是“客户端
”这边(理应如此)。那么,让我们重新审视一下文章开头的例子,看看我们免费得到了什么。
[DbConnection]
[Transaction(TransactionOption.Required)]
sealed public class Employees : ContextBoundObject
{
[AutoComplete]
public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet
{
return (EmployeeDataSet)Sql.RunSP("Employee_GetById",
"Employees", new EmployeeDataSet(),
Sql.CreateParameterWithValue("@EmployeeID", nID));
}
}
我们有一个 [DbConnection]
特性,它为我们提供与数据库的连接。我们通过 [Transaction]
特性获得了自动事务支持。我们还有 [AutoComplete]
特性,如果方法调用期间一切顺利,即没有异常,事务就可以自动提交。让我们看看 Sql.RunSP()
方法,了解在托管 SQL Server 内部是如何访问 Connection
和 Transaction
的。
static public DataSetRunSP(string procName, string tableName, DataSet dataSet,
params IDataParameter[] parameters)
{
SqlConnection dbConnection = (SqlConnection)DbConnectionUtil.Connection;
SqlTransaction dbTransaction = (SqlTransaction)ContextUtil.DbTransaction;
if((dbConnection == null) || (dbTransaction == null))
throw new System.Exception("No Connection!");
SqlDataAdapter DSCommand = new SqlDataAdapter();
DSCommand.SelectCommand = newSqlCommand(procName, dbConnection, dbTransaction);
DSCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
if(parameters != null)
{
foreach ( SqlParameter parameter in parameters )
DSCommand.SelectCommand.Parameters.Add( parameter );
}
DSCommand.Fill(dataSet, tableName);
return dataSet;
}
如你所见,有两个辅助类分别用于访问当前连接和当前事务,它们是 DbConnectionUtil
和 ContextUtil
。
实现说明
- 当前支持 SQL Server 托管提供程序
- 仅支持同步方法
- 拦截、上下文等会带来一些开销,但连接和事务的开销和成本可能超过了这些。
- 没有时间提供完整的文档和示例
- 未完全调试。此代码应用作学习工具,未经全面测试不应在生产环境中使用。(重申显而易见的事实)
我希望这能帮助到某些人,我很乐意接收任何错误修复、功能增强等。
历史
- 2001年10月21日:初始版本
许可证
本文没有附加明确的许可证,但可能在文章文本或下载文件中包含使用条款。如有疑问,请通过下面的讨论区联系作者。作者可能使用的许可证列表可以在这里找到。