EntityFramework之领域驱动设计实践(九)

仓储的实现:深入篇

早在年前的时候就已经在CSAI博客发表了上一篇文章:《仓储的实现:基础篇》。苦于日夜奔波于工作与生活之间,一直没有能够抽空继续探讨仓储的实现细节,也让很多关注EntityFramework和领域驱动设计的朋友们备感失望。

闲话不多说,现在继续考虑,如何让仓储的操作在相同的事物处理上下文中进行。DDD引入仓储模式,其目的之一就是能够通过仓储隐藏对象持久化的技术细节,使得领域模型变得更为“纯净”。由此可见,仓储的实现是需要基础结构层的组件支持的,表现为对数据库的操作。在传统的关系型数据库操作中,事务处理是一个很重要的概念,虽然从目前某些大型项目看,事务处理会降低效率,但它保证了数据的完整性。关系型数据库仍然是目前数据持久化机制的主流,事务处理的实现还是很有必要的。

为了迎合仓储模式,就需要对经典的ObjectContext使用方式作一些调整。比如,原本我们可以非常简单地使用using (EntitiesContainer ec = new EntitiesContainer())语句来界定LINQ to Entities的操作范围,并使用ObjectContext的SaveChanges成员方法提交事务,而在引入了仓储的实现中,就不能继续采用这种经典的使用方式。这让EntityFramework看上去变得很奇怪,也很牵强,我相信很多网友会批评我的做法,因为我把问题复杂化了。

其实,这应该是关注点不同罢了。关注EntityFramework的开发人员,自然觉得经典的调用方式简单明了,而从DDD的角度看呢?只能把关注点放在仓储上,而把EntityFramework当成是仓储的一种技术选型(当然从DDD角度讲,我们完全可以不选择EntityFramework,而去选择其它技术)。所以本文暂且抛开EntityFramework,继续在上文的基础上,讨论仓储的实现。

前面提到,仓储的实现需要考虑事务处理,而且根据DDD的经验,针对每一个聚合根,都需要有个仓储对其进行持久化以及对象重新组装等操作。为此,我的想法是,将仓储操作“界定”在某一个事务处理上下文(Context)中,仓储的实例是由Context获得的,这有点像EntityFramework中ObjectContext与EntityObject的关系那样。由于仓储是来自于transaction context,所以它知道目前处于哪个事务上下文中。我定义的这个transaction context如下:

隐藏行号 复制代码 Transaction Context
  1. public interface IRepositoryTransactionContext : IDisposable
    
  2. {
    
  3.     IRepository<TEntity> GetRepository<TEntity>()
    
  4.         where TEntity : EntityObject, IAggregateRoot;
    
  5.     void BeginTransaction();
    
  6.     void Commit();
    
  7.     void Rollback();
    
  8. }
    
  9. 
    

上面第三行代码定义了一个接口方法,这个方法的主要作用就是返回一个针对指定聚合根实体的仓储实例。剩下那三行代码就很明显了,那是标准的transaction操作:启动事务、提交事务以及回滚事务。

在设计上,可以根据需要,选择合适的技术来实现IRepositoryTransactionContext。我们现在讨论的是EntityFramework,所以我将给出EntityFramework的具体实现。当然,如果你不选用EntityFramework,而是用NHibernate实现数据持久化,这样的设计同样能够使你达到目的。以下是基于EntityFramework的实现:EdmRepositoryTransactionContext的伪代码。

隐藏行号 复制代码 EdmRepositoryTransactionContext
  1. internal class EdmRepositoryTransactionContext : IRepositoryTransactionContext
    
  2. {
    
  3.     private ObjectContext objContext;
    
  4.     private Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>();
    
  5. 
    
  6.     public EdmRepositoryTransactionContext(ObjectContext objContext)
    
  7.     {
    
  8.         this.objContext = objContext;
    
  9.     }
    
  10. 
    
  11.     #region IRepositoryTransactionContext Members
    
  12. 
    
  13.     public IRepository<TEntity> GetRepository<TEntity>() where TEntity : EntityObject, IAggregateRoot
    
  14.     {
    
  15.         if (repositoryCache.ContainsKey(typeof(TEntity)))
    
  16.         {
    
  17.             return (IRepository<TEntity>)repositoryCache[typeof(TEntity)];
    
  18.         }
    
  19.         IRepository<TEntity> repository = new EdmRepository<TEntity>(this.objContext);
    
  20.         this.repositoryCache.Add(typeof(TEntity), repository);
    
  21.         return repository;
    
  22.     }
    
  23. 
    
  24.     public void BeginTransaction() 
    
  25.     { 
    
  26.         // We do not need to begin a transaction here because the object context,
    
  27.         // which would handle the transaction, was created and injected into the
    
  28.         // constructor by Castle Windsor framework.
    
  29.     }
    
  30. 
    
  31.     public void Commit()
    
  32.     {
    
  33.         this.objContext.SaveChanges();
    
  34.     }
    
  35. 
    
  36.     public void Rollback()
    
  37.     {
    
  38.         // We also do not need to perform the rollback operation because
    
  39.         // entity framework will handle this for us, just when the execution
    
  40.         // point is stepping out of the using scope.
    
  41.     }
    
  42. 
    
  43.     #endregion
    
  44. 
    
  45.     #region IDisposable Members
    
  46. 
    
  47.     public void Dispose()
    
  48.     {
    
  49.         this.repositoryCache.Clear();
    
  50.         this.objContext.Dispose();
    
  51.     }
    
  52. 
    
  53.     #endregion
    
  54. }
    
  55. 
    

EdmRepositoryTransactionContext被定义为internal,这个设计是合理的,因为Domain层是不需要知道事务上下文的具体实现,它将会被IoC/DI容器注入到Domain层中(本系列文章采用Castle Windsor框架)。在EdmRepositoryTransactionContext的构造函数中,它需要EntityFramework的ObjectContext对象来初始化实例。同样,由于IoC/DI的使用,我们在代码中也是不需要去创建这个ObjectContext的,交给Castle Windsor就OK了。第13行的GetRepository方法简单地采用了Dictionary对象来实现缓存仓储实例的效果,当然这种做法还有待改进。

EdmRepositoryTransactionContext是不需要BeginTransaction的,我们将方法置空,因为EntityFramework的事务会由ObjectContext来管理,同理,Rollback也被置空。

EdmRepository的实现就比较显而易见了,请参见上文。

此外,我们还可以针对NHibernate实现仓储模式,只需要实现IRepositoryTransactionContext和IRepository接口即可,比如:

隐藏行号 复制代码 NHibernateRepositoryTransactionContext实现
  1. internal class NHibernateRepositoryTransactionContext : IRepositoryTransactionContext
    
  2. {
    
  3.     ITransaction transaction;
    
  4.     Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>();
    
  5. 
    
  6.     public ISession Session { get { return DatabaseSessionFactory.Instance.Session; } }
    
  7. 
    
  8.     #region IRepositoryTransactionContext Members
    
  9. 
    
  10.     public IRepository<TEntity> GetRepository<TEntity>() 
    
  11.         where TEntity : EntityObject, IAggregateRoot
    
  12.     {
    
  13.         if (repositoryCache.ContainsKey(typeof(TEntity)))
    
  14.         {
    
  15.             return (IRepository<TEntity>)repositoryCache[typeof(TEntity)];
    
  16.         }
    
  17.         IRepository<TEntity> repository = new NHibernateRepository<TEntity>(this.Session);
    
  18.         this.repositoryCache.Add(typeof(TEntity), repository);
    
  19.         return repository;
    
  20.     }
    
  21. 
    
  22.     public void BeginTransaction()
    
  23.     {
    
  24.         transaction = DatabaseSessionFactory.Instance.Session.BeginTransaction();
    
  25.     }
    
  26. 
    
  27.     public void Commit()
    
  28.     {
    
  29.         transaction.Commit();
    
  30.     }
    
  31. 
    
  32.     public void Rollback()
    
  33.     {
    
  34.         transaction.Rollback();
    
  35.     }
    
  36. 
    
  37.     #endregion
    
  38. 
    
  39.     #region IDisposable Members
    
  40. 
    
  41.     public void Dispose()
    
  42.     {
    
  43.         transaction.Dispose();
    
  44.         ISession dbSession = DatabaseSessionFactory.Instance.Session;
    
  45.         if (dbSession != null && dbSession.IsOpen)
    
  46.             dbSession.Close();
    
  47.     }
    
  48. 
    
  49.     #endregion
    
  50. }
    
  51. 
    
隐藏行号 复制代码 NHibernateRepository实现
  1. internal class NHibernateRepository<TEntity> : IRepository<TEntity>
    
  2.     where TEntity : EntityObject, IAggregateRoot
    
  3. {
    
  4.     ISession session;
    
  5. 
    
  6.     public NHibernateRepository(ISession session)
    
  7.     {
    
  8.         this.session = session;
    
  9.     }
    
  10. 
    
  11.     #region IRepository<TEntity> Members
    
  12. 
    
  13.     public void Add(TEntity entity)
    
  14.     {
    
  15.         this.session.Save(entity);
    
  16.     }
    
  17. 
    
  18.     public TEntity GetByKey(int id)
    
  19.     {
    
  20.         return (TEntity)this.session.Get(typeof(TEntity), id);
    
  21.     }
    
  22. 
    
  23.     public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec)
    
  24.     {
    
  25.         throw new NotImplementedException();
    
  26.     }
    
  27. 
    
  28.     public void Remove(TEntity entity)
    
  29.     {
    
  30.         this.session.Delete(entity);
    
  31.     }
    
  32. 
    
  33.     public void Update(TEntity entity)
    
  34.     {
    
  35.         this.session.SaveOrUpdate(entity);
    
  36.     }
    
  37. 
    
  38.     #endregion
    
  39. }
    
  40. 
    

在NHibernateRepositoryTransactionContext中使用了一个DatabaseSessionFactory的类,该类主要用于管理NHibernate的Session对象,具体实现如下(该实现已被用于我的Apworks应用开发框架原型中):

隐藏行号 复制代码 DatabaseSessionFactory实现
  1. /// <summary>
    
  2. /// Represents the factory singleton for database session.
    
  3. /// </summary>
    
  4. internal sealed class DatabaseSessionFactory
    
  5. {
    
  6.     #region Private Static Fields
    
  7.     /// <summary>
    
  8.     /// The singleton instance of the database session factory.
    
  9.     /// </summary>
    
  10.     private static readonly DatabaseSessionFactory databaseSessionFactory = new DatabaseSessionFactory();
    
  11.     #endregion
    
  12. 
    
  13.     #region Private Fields
    
  14.     /// <summary>
    
  15.     /// The session factory instance.
    
  16.     /// </summary>
    
  17.     private ISessionFactory sessionFactory = null;
    
  18.     /// <summary>
    
  19.     /// The session instance.
    
  20.     /// </summary>
    
  21.     private ISession session = null;
    
  22.     #endregion
    
  23. 
    
  24.     #region Constructors
    
  25.     /// <summary>
    
  26.     /// Privately constructs the database session factory instance, configures the
    
  27.     /// NHibernate framework by using the assemblies listed in the configured spaces(paths)
    
  28.     /// that are decorated by <see cref="EntityVisibleAttribute"/>.
    
  29.     /// </summary>
    
  30.     private DatabaseSessionFactory()
    
  31.     {
    
  32.         Configuration nhibernateConfig = new Configuration();
    
  33.         nhibernateConfig.Configure();
    
  34.         nhibernateConfig.AddAssembly(typeof(IAggregateRoot).Assembly);
    
  35.         sessionFactory = nhibernateConfig.BuildSessionFactory();
    
  36.     }
    
  37.     #endregion
    
  38. 
    
  39.     #region Public Properties
    
  40.     /// <summary>
    
  41.     /// Gets the singleton instance of the database session factory.
    
  42.     /// </summary>
    
  43.     public static DatabaseSessionFactory Instance
    
  44.     {
    
  45.         get
    
  46.         {
    
  47.             return databaseSessionFactory;
    
  48.         }
    
  49.     }
    
  50. 
    
  51.     /// <summary>
    
  52.     /// Gets the singleton instance of the session. If the session has not been
    
  53.     /// initialized or opened, it will return a newly opened session from the session factory.
    
  54.     /// </summary>
    
  55.     public ISession Session
    
  56.     {
    
  57.         get
    
  58.         {
    
  59.             ISession result = session;
    
  60.             if (result != null && result.IsOpen)
    
  61.                 return result;
    
  62.             return OpenSession();
    
  63.         }
    
  64.     }
    
  65.     #endregion
    
  66. 
    
  67.     #region Public Methods
    
  68.     /// <summary>
    
  69.     /// Always opens a new session from the session factory.
    
  70.     /// </summary>
    
  71.     /// <returns>The newly opened session.</returns>
    
  72.     public ISession OpenSession()
    
  73.     {
    
  74.         this.session = sessionFactory.OpenSession();
    
  75.         return this.session;
    
  76.     }
    
  77.     #endregion
    
  78. 
    
  79. }
    
  80. 
    

简单小结一下。通过上面的例子可以看到,仓储的实现是不能依赖于任何技术细节的,因为领域模型并不关心技术问题。这并不是DDD一书中怎么说,我们就得怎么做。事实上的确如此,因为DDD的思想,使得我们应该把关注点放在业务分析与领域建模上,而仓储实现的分离正是这一思想的具体表现形式。不管怎么样,采用其它的手段也罢,我们还是应该遵循“将关注点放在领域”这一宗旨。

接下来看如何在领域层结合IoC框架使用仓储。仍然以Castle Windsor为例。配置文件如下(超长部分我用省略号去掉了):

 

隐藏行号 复制代码 Castle Windsor配置
  1. <castle>
    
  2.   <components>
    
  3.     <!-- Object Context for Entity Data Model -->
    
  4.     <component id="ObjectContext"
    
  5.                service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0,......" 
    
  6.                type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/>
    
  7. 
    
  8.     <component id="GeneralRepository"
    
  9.                service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, ......"
    
  10.                type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCo......">
    
  11.       <objContext>${ObjectContext}</objContext>
    
  12.     </component>
    
  13. 
    
  14.     <component id="TransactionContext"
    
  15.                service="EasyCommerce.Domain.IRepositoryTransactionContext, EasyCommerce.Domain......"
    
  16.                type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepositoryTransactionContext, ...">
    
  17.     </component>
    
  18. 
    
  19.   </components>
    
  20. </castle>
    
  21. 
    

以下是调用代码:

隐藏行号 复制代码 调用方代码
  1. [TestMethod]
    
  2. public void TestCreateCustomer()
    
  3. {
    
  4.     IWindsorContainer container = new WindsorContainer(new XmlInterpreter());
    
  5.     using (IRepositoryTransactionContext tx = container.GetService<IRepositoryTransactionContext>())
    
  6.     {
    
  7.         tx.BeginTransaction();
    
  8.         try
    
  9.         {
    
  10.             Customer customer = Customer.CreateCustomer("daxnet", "12345",
    
  11.                 new Name { FirstName = "Sunny", LastName = "Chen" },
    
  12.                 new Address(), new Address(), DateTime.Now.AddYears(-29));
    
  13. 
    
  14.             IRepository<Customer> customerRepository = tx.GetRepository<Customer>();
    
  15.             customerRepository.Add(customer);
    
  16. 
    
  17.             tx.Commit();
    
  18.         }
    
  19.         catch
    
  20.         {
    
  21.             tx.Rollback();
    
  22.             throw;
    
  23.         }
    
  24.     }
    
  25. }
    
  26. 
    

 

测试结果及数据库数据结果:

image

 

image

 

注意】:在使用NHibernate的仓储实现时,由于NHibernate的延迟加载特性,需要将实体的属性设置为virtual,以便由NHibernate产生Proxy Class进而实现延迟加载;但是由EntityFramework自动生成的源代码并不会将实体属性设置为virtual,而采用partial class也无法解决这个问题。因此需要在代码生成技术上做文章。我的做法是,针对edmx产生一个基于T4的代码生成模板,然后修改这个模板,分别在WritePrimitiveTypeProperty和WriteComplexTypeProperty方法中的适当位置加上virtual关键字:

隐藏行号 复制代码 WritePrimitiveTypeProperty
  1.     private void WritePrimitiveTypeProperty(EdmProperty primitiveProperty, CodeGenerationTools code)
    
  2.     {
    
  3.         MetadataTools ef = new MetadataTools(this);
    
  4. #>
    
  5. 
    
  6.     /// <summary>
    
  7.     /// <#=SummaryComment(primitiveProperty)#>
    
  8.     /// </summary><#=LongDescriptionCommentElement(primitiveProperty, 1)#>
    
  9.     [EdmScalarPropertyAttribute(EntityKeyProperty=<#=code.CreateLiteral(ef.IsKey(primitiveProperty))#>, 
  10. IsNullable=<#=code.CreateLiteral(ef.IsNullable(primitiveProperty))#>)]
    
  11.     [DataMemberAttribute()]
    
  12.     <#=code.SpaceAfter(NewModifier(primitiveProperty))#><#=Accessibility.ForProperty(primitiveProperty)#> virtual
  13.  <#=code.Escape(primitiveProperty.TypeUsage)#> <#=code.Escape(primitiveProperty)#>
    
  14.     {
    
  15.         <#=code.SpaceAfter(Accessibility.ForGetter(primitiveProperty))#>get
    
  16.         {
    
  17. <#+             if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[]))
    
  18.                 {
    
  19. #>
    
  20.             return StructuralObject.GetValidValue(<#=code.FieldName(primitiveProperty)#>);
    
  21. 
    
隐藏行号 复制代码 WriteComplexTypeProperty
  1.     private void WriteComplexTypeProperty(EdmProperty complexProperty, CodeGenerationTools code)
    
  2.     {
    
  3. #>
    
  4. 
    
  5.     /// <summary>
    
  6.     /// <#=SummaryComment(complexProperty)#>
    
  7.     /// </summary><#=LongDescriptionCommentElement(complexProperty, 1)#>
    
  8.     [EdmComplexPropertyAttribute()]
    
  9.     [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
    
  10.     [XmlElement(IsNullable=true)]
    
  11.     [SoapElement(IsNullable=true)]
    
  12.     [DataMemberAttribute()]
    
  13.     <#=code.SpaceAfter(NewModifier(complexProperty))#><#=Accessibility.ForProperty(complexProperty)#> virtual 
  14. <#=MultiSchemaEscape(complexProperty.TypeUsage, code)#><#=code.Escape(complexProperty)#>
    
  15.     {
    
  16.         <#=code.SpaceAfter(Accessibility.ForGetter(complexProperty))#>get
    
  17.         {
    
  18.             <#=code.FieldName(complexProperty)#> = GetValidValue(<#=code.FieldName(complexProperty)#>, 
  19. "<#=complexProperty.Name#>", 
  20. false, <#=InitializedTrackingField(complexProperty, code)#>);
    
  21.             <#=InitializedTrackingField(complexProperty, code)#> = true;
    
  22. 
    

始终坚持一个原则:不要在生成的代码上直接修改,一是工作量巨大,另一方面就是,代码是自动生成的,今后模型修改了,代码将会重新生成。

posted @ 2010-07-10 08:52 dax.net 阅读(...) 评论(...) 编辑 收藏