说一点实践中的 Repository Pattern
这个模式都说烂了,但是为什么几乎都是:
public abstract class RepositoryBase : IDisposable
{
// ... 省略 field 声明
protected RepositoryBase() {
this._context = this.CreateContext();
}
// ... 省略 Add, Delete, Update, Get
public void Dispose() {
this._context.Dispose();
}
}
首先,继承下来的各个 Repository 难道都要用这种方法调用?
using (var repository = new XxxRepository()) {
repository.Add(...);
repository.Delete(...)
// ...
repository.SaveChanges();
}
显然,在实际中,这种小儿科的需求简直太少了。第一,一个稍稍有些流程的业务就会用到多于一个的 Repository,你如何控制事务呢?第二,如此明显的控制 Repository 的生命周期,你的代码中将充斥 using,你真的会这样写吗?
对于第一个问题,有人说,简单啊,这样就行了:
using (var tr = new TransactionScope())
using (var repository1 = new XxxRepository())
using (var repository2 = new YyyRepository())
{
repository1.Add(...);
repository2.Delete(...)
// ...
repository.SaveChanges();
tr.Complete();
}
而事实,如果你使用了这些代码,有可能会直接抛出异常(还算比较好),也有可能悄悄的成功(背后做了很多你不知道的事情)。
如果你没有部署DTS(Distributed Transaction Server),那么,在一个 TransactionScope 中出现了多个ObjectContext,导致 Transaction 升级为一个分布式的 Transaction 时会发生失败。相反,这个 Transaction 正式升级为一个分布式 Transaction,他疯狂的降低性能,并且可能造成一致性问题(如果你碰巧使用了 Mirror)。
所以,上述 Repository 也就能去上上课。实际项目中我们还是需要一些技巧。我提供一种非常平实的方法(但是有效),不用什么花哨的技术,当然还会有更优美的方法等着各位去实现。
首先,我们罗列一下限制条件:
- 我们不能没完没了的持有 Context,应当尽可能早的释放他;
- 一个 Context 只能够在一个线程中使用;
然后我们谈一下期望:
- 我们希望当我们使用 Repository 方法的时候,Context一定是有效的;
- 我们不希望手动控制 Context 的生命周期;
- 我们希望能够在外部灵活的加入事务的范围,令多个 Repository 协同工作。
为了使多个 Repository 协同工作,显然 Context 不应该定义在 Repository 中,而应该在外部进行管理。一个 Context 总是在一个线程使用,并且在事务中,只希望出现一个 Context 实例,则考虑使用线程内存储的方法保存 Context。
// 别声明为 public 的, 我们本意就是隐藏 Context.
internal class ThreadStaticContext
{
[ThreadStatic]
private static YourContextType Context;
public static bool IsAvailable()
{
return Context != null;
}
public static YourContextType GetOrCreated()
{
if (Context == null)
{
// 当然, 咱们可以公布一些静态的属性进行配置, 例如连接字符串什么的.
Context = YourContextType.Create();
}
return Context;
}
public static void Destory()
{
if (Context != null)
{
Context.Dispose();
Context = null;
}
}
}
其次,有两个点,需要确保 Context 的有效,第一,在 Repository 进行数据操作的时候;第二,在 Transaction 范围内。首先在 Repository 基类中定义如下的方法:
public abstract class RepositoryBase
{
protected T Query<T>(Func<YourContextType, T> queryProc)
{
bool isCreatedByMe = false;
if (!ThreadStaticContext.IsAvailable()) {
ThreadStaticContext.GetOrCreated();
isCreatedByMe = true;
}
try
{
return queryProc(ThreadStaticContext.GetOrCreated());
}
finally
{
if (isCreatedByMe)
{
ThreadStaticContext.Destory();
}
}
}
}
这样我们只需要确保使用 Query 方法书写 Repository 中的数据操作:
public User Get(int id)
{
return this.Query(
context => (from u in context.User where u.id == id).SingleOrDefault());
}
而后,我们需要包装一下 TransactionScope,以便在创建 Transaction 的时候确保 Context 的有效性:
public class DataTransaction : IDisposable
{
private bool _isCreatedByMe;
private TransactionScope _transactionScope;
protected DataTransaction()
{
this._transactionScope = new TransactionScope();
try
{
if (ThreadStaticContext.IsAvailable())
{
ThreadStaticContext.GetOrCreated();
this._isCreatedByMe = true;
}
}
catch
{
this._transactionScope.Dispose();
throw;
}
}
public void Complete()
{
this._transactionScope.Complete();
}
public void Dispose()
{
if (this._isCreatedByMe)
{
ThreadStaticContext.Destory();
}
this._transactionScope.Dispose();
}
public static DataTransaction Create()
{
return new DataTransaction();
}
}
这样我们就可以这样使用 Repository 了。
using (var tr = DataTransaction.Create())
{
new RepositoryType1().Add(...);
new RepositoryType2().Update(...);
tr.Complete();
}
不但完全看不到 Context 的影子,连 Context 的生存周期也不用担心。当然,现在我们的 Query 方法中有一个 context 参数,实际上这个参数也是不用的,可以考虑基类做成 RepositoryBase<TTable>其中使用 context.GetTable<TTable> 实现 GetAll(),Submit(),AddOnSubmit(),DeleteOnSubmit(),… 方法。这样 Context 再也不会出现了。这个过程就不赘述了。
如果你手头恰好有好用的依赖注入容器(例如Ninject),可以使用其控制生存期,将Context的生存期控制在 Per Thread 一级,可以达到同样的效果。