说一点实践中的 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 一级,可以达到同样的效果。