65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF 示例 - 第 XV 章 - RavenDB 实现

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2012年12月15日

CPOL

9分钟阅读

viewsIcon

36801

downloadIcon

812

工作单元和存储库 RavenDB 实现示例

Previous   Next
第十四章   第十六章

系列文章

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 实例

到目前为止,在本章之前,我们已经看到了两种持久化组件的实现:InMemoryNHibernate。在本章中,我们将介绍一种新实现: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 那样的特定机制来启动事务。

事务管理器工厂

此组件负责两个关键角色:创建 IDocumentSessionTransManager

    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,请按照以下说明操作:

  • 确保所有项目都能正确生成,您可能需要手动删除 DebugRelease 文件夹中的所有文件。
  • 修改客户端 (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 日:初始版本
© . All rights reserved.