WCF 示例 - 第 XV 章 - RavenDB 实现





5.00/5 (5投票s)
工作单元和存储库 RavenDB 实现示例
![]() | ![]() | |
第十四章 | 第十六章 |
系列文章
WCF 示例是一系列文章,介绍如何使用 WCF 进行通信和 NHibernate 进行持久化,来设计和开发 WPF 客户端。该系列介绍描述了文章的范围,并高层次地讨论了架构解决方案。该系列的源代码可在CodePlex找到。本文档将介绍 RavenDB 用于持久化目的。
章节概述
对于刚接触该系列文章的读者,eDirectory 解决方案的持久化组件围绕工作单元和存储库模式进行设计。TransManager
负责工作单元的实现,因此它提供了对 IRepositoryLocator
的访问,后者提供了对单个实体存储库的访问。存储库基于在需要时由存储库定位器创建的通用实现。这些通用存储库公开了基本的 CRUD 方法以及一个 IQueryble
方法的端点,该方法由持久化 Linq 提供程序利用。
以下是服务器组件中使用的代码示例:
public CustomerDto UpdateCustomer(CustomerDto dto)
{
01 return ExecuteCommand(locator => UpdateCustomerCommand(locator, dto));
}
private CustomerDto UpdateCustomerCommand(IRepositoryLocator locator, CustomerDto dto)
{
02 var instance = locator.GetById<customer>(dto.Id);
03 instance.Update(locator, dto);
return Customer_to_Dto(instance);
}
第 01 行:用于更新现有实例的客户服务的入口点
第 02 行:使用 Dto.Id
属性,我们使用 Locator
解析客户实例
第 03 行:然后,我们将更新逻辑委托给领域类,传递定位器和 Dto 实例
到目前为止,在本章之前,我们已经看到了两种持久化组件的实现:InMemory
和 NHibernate
。在本章中,我们将介绍一种新实现:RavenDB。
RavenDB 是一个面向文档的数据库,它便于对象的持久化。在 NHibernate 中,需要在实体类和数据库表之间进行映射。但在 RavenDB 中,实体和持久化框架之间不需要额外的映射。
本文讨论了实现一组定制的持久化组件以使 eDirectory 应用程序能够从 RavenDB 文档存储中保存和读取对象是多么容易。在本示例中,我们使用嵌入式文档存储实现,并且还将讨论如何将其设置为在内存模式下执行测试。
作为一种声明,我决定不更改系列中一直使用的领域实体,因此,您可能会发现 RavenDB 解决方案的最佳设计实践在此示例中并未严格遵守。尽管如此,本文仍应是一个关于如何使 RavenDB 正常工作的良好示例。
另外,与 NHibernate 一样,eDirectory 解决方案中的 RavenDB 实现也是通过使用 Linq 提供程序和通用存储库来利用的;在实际场景中,这种方法可能过于严格,并且在更复杂的场景中可能需要定制的存储库实现。
除了提到的约束之外,本文提出的架构在许多情况下都可以很好地工作,或者至少可以作为您项目的起点。
以前
向 eDirectory
解决方案添加了一个名为 eDirectory.RavenDB
的新项目。然后使用 NuGet 获取对RavenDB Embedded 的引用。值得注意的是,添加了一个生成后事件,以便将所有生成构件复制到 libs 文件夹,从而方便在客户端项目中进行使用。
RavenDB 存储库
存储库的实现与 NHibernate 在很多方面非常相似
public class RepositoryRavenDB<TEntity>
: IRepository<TEntity>
{
private readonly IDocumentSession _sessionInstance;
private readonly string _idPrefix;
public RepositoryRavenDB(IDocumentSession session)
{
_sessionInstance = session;
01 _idPrefix = GetPrefix();
}
private static string GetPrefix()
{
var typeName = typeof (TEntity).Name;
var flag = typeName.Last().Equals('s');
02 return typeName +
(flag
? "es/"
: "s/");
}
#region Implementation of IRepository<TEntity>
public TEntity Save(TEntity instance)
{
03 _sessionInstance.Store(instance);
return instance;
}
public void Update(TEntity instance)
{
}
public void Remove(TEntity instance)
{
04 _sessionInstance.Delete(instance);
}
public TEntity GetById(long id)
{
05 return _sessionInstance.Load<TEntity>(_idPrefix + id);
}
public IQueryable<TEntity> FindAll()
{
06 return _sessionInstance.Query<TEntity>();
}
#endregion
}
第 01 行:为每个存储库/实体类型生成一个前缀,供 Load
方法使用。
第 02 行:前缀以复数形式结尾,对于以“S
”结尾的实体名称,我们需要额外的检查。
第 03 行:当调用 Store
方法时,会在 RavenDB 中保存一个新实例,无需额外的映射即可正常工作,这是一个很棒的功能。此外,RavenDB 根据名称约定检测实体中的 Id 属性,并在方法返回后填充该属性。
第 04 行:Delete
方法用于从数据库中删除实体。
第 05 行:Load
方法是通过 Id 检索实体的优化机制,这是需要使用前缀的地方。
第 06 行:FindAll
方法委派给 RavenDB Linq 提供程序,与 NHibernate 实现中的做法完全相同。
另外一个值得注意的方面是,RavenDB 实现不需要 Update
方法。
存储库定位器
定位器的实现非常简单,与我们之前看到的非常相似。
public class RepositoryLocatorRavenDB
: RepositoryLocatorBase, IResetable, IStoreInitialiser
{
private readonly IDocumentSession _sessionInstance;
public RepositoryLocatorRavenDB(IDocumentSession session)
{
_sessionInstance = session;
}
#region Overrides of RepositoryLocatorBase
protected override IRepository<T> CreateRepository<T>()
{
01 return new RepositoryRavenDB<T>(_sessionInstance);
}
#endregion
#region Implementation of IResetable
02 ...
#endregion
#region Implementation of IStoreInitialiser
02 ...
#endregion
}
第 01 行:基类定位器调用此方法以获取给定类型的存储库实例。
第 02 行:这两个部分暂时省略,因为它们的目的仅用于测试,我们将在本文后面讨论。
事务管理器
同样,此类实现与 NHibernate 的实现非常相似。
public class TransManagerRavenDB
: TransManagerBase
{
private readonly IDocumentSession _sessionInstance;
public TransManagerRavenDB(IDocumentSession session)
{
_sessionInstance = session;
01 Locator = new RepositoryLocatorRavenDB(_sessionInstance);
}
#region Overriden Base Methods
public override void CommitTransaction()
{
base.CommitTransaction();
02 _sessionInstance.SaveChanges();
}
public override void Rollback()
{
base.Rollback();
03 _sessionInstance.Advanced.Clear();
}
protected override void Dispose(bool disposing)
{
...
}
private void Close()
{
...
}
#endregion
}
第 01 行:构造函数中传递了一个 IDocumentSession
,用于创建 RepositoryLocatorRavenDB
。
第 02 行:要提交自事务管理器创建以来的所有更改,我们需要调用 IDocumentSession
中的 SaveChanges
方法。
第 03 行:如果需要回滚,则使用 Clear
来丢弃所有更改。
值得注意的是,没有像 NHibernate 那样的特定机制来启动事务。
事务管理器工厂
此组件负责两个关键角色:创建 IDocumentSession
和 TransManager
。
public class TransManagerFactoryRavenDB
: ITransFactory
{
private IDocumentStore _documentStore;
private IDocumentStore DocumentStore
{
get
{
03 if (_documentStore != null) return _documentStore;
_documentStore = InitialiseDocumentStore();
return _documentStore;
}
}
private IDocumentStore InitialiseDocumentStore()
{
var store = new EmbeddableDocumentStore { DataDirectory = "eDirectory" };
01 store.Initialize();
return store;
}
#region Implementation of ITransFactory
public ITransManager CreateManager()
{
02 return new TransManagerRavenDB(DocumentStore.OpenSession());
}
#endregion
}
第 01 行:创建一个名为“eDirectory
”的嵌入式文档存储实例,这将自动在您的部署文件夹中生成一个 RavenDB 实例。在对存储执行任何操作之前,Initialize
方法至关重要。
第 02 行:这是工厂方法,它返回一个事务管理器,并将一个新会话传递给其构造函数。与 NHibernate 一样,此会话实例对定位器和存储库以外的代码是不可用的。这是正确实现工作单元的关键。
第 03 行:您可能希望增强此方法以避免竞态条件,例如,建议使用延迟初始化文档存储。
如何配置客户端 UI
为了让 WPF 客户端使用 RavenDB,请按照以下说明操作:
- 确保所有项目都能正确生成,您可能需要手动删除 Debug 或 Release 文件夹中的所有文件。
- 修改客户端 (
eDirectory.WPF
) 中的 App.config 文件,将SpringConfigFile
设置为:file://RavenDBConfiguration.xml - 执行客户端项目
- 使用“带地址的客户视图”创建一个新客户
此时,在创建第一个客户实例后,您可能会看到一个空的网格,如下所示:
我们创建了第一个客户

按下 **保存** 按钮后,客户网格为空

如果我们等待 10-15 秒并按下命令按钮刷新,客户记录就会出现。

我们在这里重现的是 RavenDB 中称为陈旧索引状态的情况。似乎当记录第一次在 RavenDB 存储实例中创建时,索引需要很长时间才能更新,并且查询不会返回刚刚创建的记录。值得注意的是,在这种情况下,刷新是在与保存操作不同的请求中完成的。
如果您关闭应用程序并再次重新启动,您会发现此问题不再发生,即使地址实例是第一次创建。您可能需要查看有关此“功能”的官方文档。我们将在测试部分再次讨论它,并看看如何避免这种情况。
其他方面
领域实体需要一个额外的更改,我们需要用 JsonObject
属性标记客户和地址实体,以便 NewtonSoft
库能够正确序列化我们的对象。我们的 Linq 查询也有一个更改,其中“Equals
”必须替换为“==
”;如果使用“Equals
”方法,RavenDB Linq 会遇到问题。
使用 RavenDB 进行测试
如果要在运行测试时使用 RavenDB,我们需要考虑一个关键方面:
- 在测试执行之间清除数据库
内存
但在我们讨论如何清除文档存储之前,有一个有趣的特性值得一提:RavenDb 可以配置为在内存中运行,因此性能得到提高,而不会丢失任何行为/功能。因此,我修改了 TransManagerFactoryRavenDB
以便可以运行内存测试。
public class TransManagerFactoryRavenDB
: ITransFactory
{
...
01 private bool IsSetForTesting { get; set; }
private IDocumentStore InitialiseDocumentStore()
{
var store =
IsSetForTesting
02 ? new EmbeddableDocumentStore
{
RunInMemory = true
}
: new EmbeddableDocumentStore {DataDirectory = "eDirectory"};
store.Initialize();
return store;
}
...
}
第 01 行:可以设置一个新标志来指示必须创建一个 InMemory
实例。
第 02 行:如果设置了该标志,则 RavenDB
实例将在内存中创建。
用于测试的 Spring 配置文件设置了 IsSetForTesting
属性,因此在执行测试时,将使用 InMemory RavenDB
实例。您可能需要查看测试项目中的 TestRavenDBConfiguration
文件以获取更多详细信息。
清除
我从Vladimir Petrov 的博客中借鉴了使用索引的想法,因此在测试期间创建实体实例时,可以使用自定义索引删除它们。RavenDB
索引是通过继承 AbstractIndexCreationTask
类在代码中创建的。
public class AllDocumentsById : AbstractIndexCreationTask
{
public const string Name = "AllDocumentsById";
#region Overrides of AbstractIndexCreationTask
public override IndexDefinition CreateIndexDefinition()
{
return new IndexDefinition
{
01 Name = AllDocumentsById.Name,
02 Map = "from doc in docs let DocId =
doc[\"@metadata\"][\"@id\"] select new {DocId};"
};
}
#endregion
}
第 01 行:我们为索引提供一个众所周知的名称,在删除实体时需要它。
第 02 行:声明索引映射,以便为存储中创建的任何文档创建一个条目。
为了创建索引,测试项目有一些额外的功能来确定 RepositoryLocator
是否实现了 IStoreInitialiser
接口,如果是,它会调用 ConfigureStore
方法。RepositoryLocatorRavenDB
会这样做。
public class RepositoryLocatorRavenDB
01 : RepositoryLocatorBase, IResetable, IStoreInitialiser
{
...
#region Implementation of IStoreInitialiser
public void ConfigureStore()
{
var documentStore = _sessionInstance.Advanced.DocumentStore
as EmbeddableDocumentStore;
if (documentStore == null) return;
02 IndexCreation.CreateIndexes(typeof(AllDocumentsById).Assembly, documentStore);
}
#endregion
}
那么测试是如何使用这个索引的呢?好吧,碰巧 RepositoryLocatorRavenDB
也实现了 IResetable
接口。
public class RepositoryLocatorRavenDB
: RepositoryLocatorBase, IResetable, IStoreInitialiser
{
...
#region Implementation of IResetable
public void Reset()
{
var documentStore = _sessionInstance.Advanced.DocumentStore
as EmbeddableDocumentStore;
if (documentStore == null) return;
01 while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0)
{
Thread.Sleep(10);
}
02 _sessionInstance.Advanced.DatabaseCommands.DeleteByIndex
(AllDocumentsById.Name, new IndexQuery());
}
#endregion
...
}
第 01 行:我将在稍后解释。
第 02 行:它使用 DeleteByIndex
并传入我们自定义索引的名称。
上述索引存在一个问题:当测试执行仅创建一个实体实例时,AllDocumentsById
索引会过时。如果创建了多个实例,问题似乎会得到解决。因此,第 01 行用于确保在调用删除方法时索引不会过时。
将配置设置为使用 RavenDB
为了在运行测试时使用 RavenDB 实现,请确保 App.config 文件已配置,以便 SpringConfigFile
appSetting 设置为 TestRavenDBConfiguration.xml。
结论
将 RavenDB 适配到 eDirectory 解决方案非常容易,实际上只需要几个小时就能正常工作,而让测试正常工作则花费了更多时间,这主要归因于陈旧索引问题。无需在域实体和文档存储之间创建映射是关键区别。
注意:如果要在设计 RavenDB 存储的域时遵循最佳设计实践,您需要摆脱传统的关系型方法,文档应存储尽可能多的信息;这意味着什么?简而言之,在领域术语中,您的文档变成聚合体,并且重复不是“罪过”。如果您一直使用 NHibernate 或任何其他 ORM 框架,这将是一个根本性的思维转变。
其他资源链接
历史
- 2013 年 2 月 4 日:初始版本