抽象ORM框架






4.45/5 (9投票s)
通过 Repository/Unit Of Work 模式抽象 ORM 框架
引言
本教程旨在解释如何将对象/关系映射 (ORM) 框架以一种通用的方式集成到我们的项目中,从而在尽可能低的级别处理通用情况。这也确保了使用此解决方案的开发人员不会被过多的细节所困扰。
在本教程中,我使用 Entity Framework 作为 ORM 框架。但是,稍作调整,完全可以用其他框架(例如 NHibernate)替换它。
抽象框架
为了抽象所选的 ORM 框架,我们选择了 Repository 和 Unit of Work 模式的组合。
Martin Fowler 有以下定义
- 一个 Repository 介于域和数据映射层之间。它允许我们在自己的端将实体作为本地集合来处理,并在幕后执行必要的持久化/检索操作。
- Unit of Work 维护受业务事务影响的对象列表,并协调变更的写入和并发问题的解决。
换句话说,Unit of Work 包含对一个或多个实体的修改,它结合了对多个存储库所做的修改。这些修改共同作用,以确保底层数据源保持一致状态。要么所有修改都成功,要么所有修改都失败。为了实现这一点,我们(重)使用数据库事务。
下图包含所使用的类及其依赖关系的概述。
UnitOfWork<T>
一个通用实现,它将一个或多个已添加/删除/更新的对象集关联起来。它提供了一个函数('Execute
'),允许多个存储库(通过对对象集的引用完成)作为一个整体进行管理。成功执行将确保封闭的事务被提交。
可以递归使用 UnitOfWork
,顶层的 UnitOfWork
中的事务将负责提交或回滚。最终,所有修改的结果要么全部成功,要么全部失败。
Repository<T>
一个存储库的通用实现,具有在底层数据源中管理指定实体类型的功能。该数据源由对象集表示。
提供了一些方法,允许对底层实体执行 CRUD 风格的操作。如果可能,它们提供了传递 LINQ 表达式的可能性,以提供自定义这些操作的方式。
IObjectSetCreator<T>
这为 Repository
提供了通过 UnitOfWork
创建对象集实例的可能性,而无需指定所使用的 ObjectContext
。
ObjectContext
这为我们提供了与包含所用实体的数据源的连接。特定类型 DataModelObjectContext
包含有关数据源和所用实体定义的信息。
运行解决方案的准备工作
必备组件
- SQL Express 2012,命名实例 'SQLEXPRESS',启用 Windows 安全
- Visual Studio 2010
我将描述前几个步骤,以便对 Entity Framework 经验不足或没有经验的人了解在新解决方案中自己尝试需要采取哪些步骤。
如果只想运行提供的解决方案,可以从第 4 步开始。
步骤 1
在我的示例中,我从一个新解决方案开始,并直接添加了 DataAccess 项目。该项目将包含用于处理 Entity Framework 的功能。
要将 Entity Framework 添加到解决方案中,我们需要 NuGet。可以通过 Visual Studio 中的扩展管理器安装。选择 DataAccess 项目并右键单击引用。选择“管理 NuGet 包”并安装 Entity Framework。
第二步
继续向 DataAccess 项目添加一个新项,即“ADO.NET Entity Data Model”并相应命名。
点击“添加”后,我们必须决定是使用模型优先(model-first)、代码优先(code-first)还是数据库优先(database-first)。在这个例子中,我们将使用模型优先方法并选择“空模型”。
步骤 3
在这个空模型中,我们将添加以下 2 个实体。
我为每个实体添加了一个额外的字符串类型标量属性(Title 和 Name)。我还添加了一个从 Author 到 Book 的关联。这将直接配置所示的多重性。导航属性会自动添加,这将使我们能够轻松访问任何引用的实体。
步骤 4
在模型中的任意位置右键单击并选择“从模型生成数据库”。
选择“新建连接…”
选择要使用的数据源,使用“DataModel”作为数据库名称。保持其余属性不变,然后按“确定”。
复制选定的实体连接字符串,以便我们可以在特定的 DataModelObjectContext
类中使用它。这不再需要在 app.config 文件中保存连接字符串,因此我们可以禁用该选项。特定的 ObjectContext
类用于维护正确的连接字符串,以便更容易在解决方案中拥有多个模型。然而,在本教程中,我们将只有一个模型。
继续点击“下一步”并在 Visual Studio 中执行生成的 SQL 脚本以创建两个表。
步骤 5
DataModelObjectContext
的构造函数如下所示
/// <summary>
/// Default constructor for class
/// </summary>
public DataModelObjectContext()
: base(@"metadata=res://*/DataModel.csdl|res://*/DataModel.ssdl|" +
@"res://*/DataModel.msl;provider=System.Data.SqlClient;" +
@"provider connection string=""Data Source=localhost\SQLEXPRESS;" +
@"Initial Catalog=DataModel;Integrated Security=True""")
{
}
请注意,此连接字符串包含模型的名称以及所用数据源的位置和名称。在您的情况下,这可能会有所不同。
作为开发人员使用 Unit of Work / Repository
作为开发人员,使用提供的 UnitOfWork
和 Repository
类处理实体非常容易。为了说明其易用性,我添加了一些片段,指示如何相应地使用它们。附加的示例解决方案有按钮,允许执行这些片段中的每一个。
存储
以下片段展示了将新作者存储到数据源所需的操作。
using (UnitOfWork<datamodelobjectcontext> unitOfWork =
new UnitOfWork<datamodelobjectcontext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
Author author = new Author()
{
Name = "Stephen Hunt"
};
authorRepository.Add(author);
return true;
});
}
使用两个仓库
以下片段展示了如何以 Stephen Hunt 为作者创建两本书。它基于上一个片段的结果继续,因此会执行查找以获取添加的作者。
正如大家所看到的,它就像创建一本书,设置对作者的引用,最后将其添加到存储库一样简单。正常完成执行流程将自动触发更改的保存并触发封闭事务的提交。
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
// Find the author with the name "Stephen Hunt".
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
Repository<book> bookRepository =
new Repository<book>(unitOfWork);
Book book1 = new Book()
{
Title = "Sliding Void",
Author = author
};
Book book2 = new Book()
{
Title = "In the Company of Ghosts",
Author = author
};
bookRepository.Add(book1);
bookRepository.Add(book2);
}
return true;
});
}
检索
下一个代码片段展示了获取作者和引用书籍所需的操作。这需要一个额外的操作来确保从数据源中检索所需的导航属性。这样,可以更好地控制导航属性的检索,这直接影响对底层数据源执行的查询。
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
// Find the author with the name "Stephen Hunt".
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
// Request that the navigation property 'Books' is
// filled.
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
}
return true;
});
}
移除
下一个片段展示了删除实体所需的操作,在本例中是作者的一本书。
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<DataModelObjectContext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
// Find the author with the name "Stephen Hunt".
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
// Request that the navigation property 'Books' is
// filled.
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
Book book = author.Books.FirstOrDefault<book>();
if (book != null)
{
new Repository<book>(unitOfWork).Remove(removedBook);
}
return true;
});
}
数据一致性
在处理数据源时,数据一致性非常重要。这就是 Unit of Work 内部使用事务来确保在 Execute 流中完成的所有突变都是原子性的原因。为了展示这是如何工作的,使用了以下代码片段。
请注意,从作者中删除该书后会抛出异常。这将触发底层事务的回滚,从而回滚“工作单元”中进行的所有突变。
// First we display the books for author 'Stephen Hunt'.
DisplayBooksForAuthor(("Stephen Hunt");
using (UnitOfWork<DataModelObjectContext> unitOfWork =
new UnitOfWork<datamodelobjectcontext>())
{
bool result = unitOfWork.Execute(() =>
{
Repository<author> authorRepository =
new Repository<author>(unitOfWork);
// Find the author with the name "Stephen Hunt".
Author author = authorRepository.Find(
item => item.Name.Equals("Stephen Hunt")).
SingleOrDefault<author>();
if (author != null)
{
// Request that the navigation property 'Books' is
// filled.
unitOfWork.ObjectContext.LoadProperty(
author, item => item.Books);
Book book = author.Books.FirstOrDefault<book>();
if (book != null)
{
new Repository<book>(unitOfWork).Remove(book);
throw new InvalidOperationException();
}
}
return true;
});
}
// Now we display the books for author 'Stephen Hunt' once more.
DisplayBooksForAuthor("Stephen Hunt");
关注点
在 Unit of Work 的上下文中,对存储库的变更执行通过 Func 委托处理。这使我们能够轻松地将通用功能保留在 Unit of Work 中。
/// <summary>
/// Execute the passed function within the context of the unit of work.
/// </summary>
/// <param name="function" />The function to execute.
/// <returns>true when the function was successfully executed and
/// all changes are committed, false otherwise.</returns>
public bool Execute(Func<bool> function)
{
bool result = false;
try
{
using (TransactionScope transactionScope = new TransactionScope())
{
try
{
if (function())
{
// The function was successful, so we can save all
// changes and complete the transaction.
result = SaveChanges();
if (result)
{
try
{
transactionScope.Complete();
}
catch (InvalidOperationException)
{
result = false;
}
}
}
else
{
// Function failed.
}
}
catch (EntityCommandExecutionException)
{
// A command (stored procedure) failed to execute.
}
catch (Exception)
{
// An unspecified exception occurred.
}
}
}
catch (TransactionAbortedException e)
{
// Transaction is aborted.
}
catch (TransactionInDoubtException e)
{
// Transaction is in doubt.
}
}
随附的解决方案
随附的解决方案包含两个项目
- AbstractingDataAccess: 包含 Windows 窗体
- DataAccess: 包含实体框架的特定实现和模型
历史
- 2013 年 7 月 4 日 - 第一个版本。