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

Antler:用于您喜欢的 .NET ORM 的抽象(第二部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (6投票s)

2014年8月6日

CPOL

3分钟阅读

viewsIcon

14349

downloadIcon

153

使用相同的语法处理不同的 ORM。深入了解 Antler 框架的细节。

引言

上一篇文章中,我向 CodeProject 社区介绍了 Antler 框架的高级概述。
现在我想深入内部,向您展示该框架的设计细节。

框架以可插拔的方式实现,由一个 Antler.Core 库和许多适配器组成。核心库包含所有必要的抽象和共享功能,而适配器库包含不同 ORM 和 IoC 容器的具体实现。

这种可插拔的结构允许在项目中的不同 ORM/数据库和 IoC 容器之间轻松切换,并使用通用的习惯性语法来处理它们。

目前,通过 NuGet 可提供 NHibernate、EntityFramework、Linq2Db、Castle Windsor、StructureMap 适配器。

在本文中,我们将深入探讨 Antler 框架的 Unit-of-work 和 ORM 适配器的实现细节,并以 Antler.NHibernate 适配器为例。

使用示例

所有数据库操作都通过 UnitOfWork 类进行,该类代表底层数据库事务。

例如,具有多个操作的 UnitOfWork

UnitOfWork.Do(uow => 
               { 
                var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
                                                      Any(t => t.Description == "Hockey");
                if (!hasHockeyTeams) 
                { 
                 uow.Repo<Team>().Insert(new Team() {Name = "Penguins", Description = "Hockey"});
                 uow.Repo<Team>().Insert(new Team() {Name = "Capitals", Description = "Hockey"}) 
                }});

带有简单查询的 UnitOfWork

var hockeyTeams = UnitOfWork.Do(uow => uow.Repo<Team>().AsQueryable().
                                                        Where(t => t.Description == "Hockey").
                                                        ToList());

UnitOfWork 完全可配置。我们可以在应用程序级别在引导程序中配置 UnitOfWork 的行为。例如,让我们配置一个使用 Castle Windsor 容器和 NHibernate + Oracle 存储的应用程序,以便在任何 UnitOfWork 结束时执行 Rollback 而不是 Commit。

var configurator = new AntlerConfigurator();
configurator.UseWindsorContainer()
            .UseStorage(NHibernateStorage.Use.WithDatabaseConfiguration(
                        OracleDataClientConfiguration.Oracle10.
                        ConnectionString(Config.ConnectionString).
                        DefaultSchema(Config.DbSchemaName).ShowSql()).
                        WithMappings("Sport.Mappings")).
                        SetUnitOfWorkDefaultSettings(new UnitOfWorkSettings() 
                                                         { RollbackOnDispose = true });

并且不需要在应用程序级别进行配置,您也可以配置特定的 UnitOfWork。

UnitOfWork.Do(uow =>
              {
               var hasHockeyTeams = uow.Repo<Team>().AsQueryable().
                                                       Any(t => t.Description == "Hockey");
               if (!hasHockeyTeams)
               {
                uow.Repo<Team>().Insert(new Team() { Name = "Penguins", Description = "Hockey" });
                uow.Repo<Team>().Insert(new Team() { Name = "Capitals", Description = "Hockey" });
               }
              }, new UnitOfWorkSettings(){ RollbackOnDispose = true }); 

您可以通过 UnitOfWorkSettings 类禁用提交、在检测到嵌套 UnitOfWork 时抛出异常、指定要使用的具体存储以及其他功能。这些功能在测试项目中可能很有用。因为,不幸的是,在企业项目中,我们有时无法生成测试数据库来运行测试。因此,像 RollbackOnDispose 这样的选项可能非常方便。

值得一提的另一件事是,我们可以配置我们的应用程序拥有多个存储,以允许它们之间的数据传输。例如

var configurator = new AntlerConfigurator();
configurator.UseBuiltInContainer()
            .UseStorage(EntityFrameworkStorage.Use.
                        WithConnectionString("Data Source=.\\SQLEXPRESS;
                        Initial Catalog=Database1;Integrated Security=True").WithLazyLoading().
                        WithDatabaseInitializer(
                        new DropCreateDatabaseIfModelChanges<DataContext>()).
                        WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store1")
            .UseStorage(EntityFrameworkStorage.Use.
                        WithConnectionString("Data Source=.\\SQLEXPRESS;
                        Initial Catalog=Database2;Integrated Security=True").
                        WithMappings(Assembly.Load("Blog.Mappings.EF")), "Store2");

现在我们可以在应用程序中使用两个存储。例如,让我们将员工信息从一个存储传输到另一个存储。

var userFromSourceStorage = UnitOfWork.Do(uow => uow.Repo<Employee>().GetById(gpin), 
                                         new UnitOfWorkSettings {StorageName = "Store1"}); 

UnitOfWork.Do(uow => 
                { 
                  var foundUserInCurrentStorage = uow.Repo<Employee>().GetById(gpin); 
                  if (foundUserInCurrentStorage == null) 
                   {
                     uow.Repo<Employee>().Insert(userFromSourceStorage);
                   } 
                 }, new UnitOfWorkSettings{StorageName = "Store2"});

使用 UnitOfWork 的首选方式是使用 Do 方法中指定的 lambda 表达式。但有时我们可能需要像这样获取当前的 UnitOfWork。

var hasHockeyTeams = UnitOfWork.Current.Value.Repo<Team>().AsQueryable().
                                                           Any(t=>t.Description == "Hockey");
if (!hasHockeyTeams)
 {
   UnitOfWork.Current.Value.Repo<Team>().Insert(new Team() 
                                                  { Name = "Penguins", Description = "Hockey" });
   UnitOfWork.Current.Value.Repo<Team>().Insert(new Team() 
                                                  { Name = "Capitals", Description = "Hockey" });
  }

我们可以这样做,因为我们使用线程静态字段来将 UnitOfWork 保留在线程级别。当然,在这个例子中,在使用之前检查当前上下文中是否有 UnitOfWork 是一个好习惯。

实现细节

让我们看看 UnitOfWork 类。
public class UnitOfWork: IDisposable
    {
        public ISessionScope SessionScope { get; private set; }

        [ThreadStatic]
        private static UnitOfWork _current;
        public static Option<UnitOfWork> Current
        {
            get { return _current.AsOption(); }            
        }

        public bool IsFinished
        {
            get { return _current == null; }
        }
        
        public bool IsRoot { get; private set; }
                
        public Guid Id { get; private set; }
        public UnitOfWorkSettings Settings { get; private set; }
        
        public static Func<string, ISessionScopeFactory> SessionScopeFactoryExtractor 
                                                                        { get; set; }
                        
        private UnitOfWork(UnitOfWorkSettings settings)
        {
            Settings = settings ?? UnitOfWorkSettings.Default;

            Assumes.True(SessionScopeFactoryExtractor != null, "SessionScopeFactoryExtractor 
                                                               should be set before using 
                                                               UnitOfWork. Wrong configuraiton?");
            
            Assumes.True(!string.IsNullOrEmpty(Settings.StorageName), "Storage name can't be null 
                                                                       or empty. 
                                                                       Wrong configuration?");    
            
            var sessionScopeFactory = SessionScopeFactoryExtractor(Settings.StorageName);
            
            Assumes.True(sessionScopeFactory != null, "Can't find storage with name {0}. 
                                                       Wrong storage name?",Settings.StorageName);
            SetSession(sessionScopeFactory);
        }
                
        private void SetSession(ISessionScopeFactory sessionScopeFactory)
         {
            Requires.NotNull(sessionScopeFactory, "sessionScopeFactory");
            
             if (_current == null)
             {
                 SessionScope = sessionScopeFactory.Open();
                 IsRoot = true;                 
             }
             else
             {
                 if (Settings.ThrowIfNestedUnitOfWork)
                     throw new NotSupportedException("Nested UnitOfWorks are not 
                                                      supported due to UnitOfWork Settings 
                                                      configuration");

                 SessionScope = _current.SessionScope;
                 IsRoot = false;
             }
                                                  
            _current = this;            
            Id = Guid.NewGuid();            
         }

        /// <summary>
        /// Start database transaction.
        /// </summary>   
        public static void Do(Action<UnitOfWork> work, UnitOfWorkSettings settings = null)
        {
            Requires.NotNull(work, "work");
            
            using (var uow = new UnitOfWork(settings))
            {
                work(uow);
            }
        }
        
        /// <summary>
        /// Start database transaction and return result from it.
        /// </summary>   
        public static TResult Do<TResult>(Func<UnitOfWork, TResult> work, UnitOfWorkSettings 
       settings = null)
        {
            Requires.NotNull(work, "work");

            using (var uow = new UnitOfWork(settings))
            {
                return work(uow);
            }
        }

        /// <summary>
        /// Commit/Rollback transaction(depending on the configuration) explicitly. Will be called
        /// automatically in the end of the "Do" block.
        /// </summary> 
        public void Dispose()        
        {
            if (Marshal.GetExceptionCode() == 0)
            {
                if (Settings.RollbackOnDispose)
                    Rollback();
                else
                    Commit();
            }
            else
            {
                if (IsRoot && !IsFinished)
                  CloseUnitOfWork();
            }
        }

        /// <summary>
        /// Commit database transaction explicitly(not necessarily to use in standard 
        /// configuration, because transaction will be committed anyway in the end of the 
        /// "Do" block).
        /// </summary>  
        public void Commit()
        {                        
                Perform(() =>
                    {
                        if (Settings.EnableCommit) 
                            SessionScope.Commit();
                    });
        }

        /// <summary>
        /// Rollback database transaction explicitly. As alternative you can use 
        /// RollbackOnDispose setting to rollback transaction automatically in the end of the "Do"
        /// block(may be useful in testing).
        /// </summary>  
        public void Rollback()
        {
            Perform(() => SessionScope.Rollback());            
        }

        private void Perform(Action action)
        {
            Requires.NotNull(action, "action");
            if (IsRoot && !IsFinished)
            {
                try
                {
                    action();
                }
                finally 
                {
                    CloseUnitOfWork();
                }                                
            }
        }

        private void CloseUnitOfWork()
        {            
            SessionScope.Dispose();
            _current = null;                           
        }

        /// <summary>
        /// Get Repository object to perform queries/operations on database.
        /// </summary> 
        public IRepository<TEntity> Repo<TEntity>() where TEntity: class
        {
            return SessionScope.CreateRepository<TEntity>();
        }                   
    }

UnitOfWork 类位于 Antler.Core 库中(该库没有 NuGet 依赖项),因此 UnitOfWork 需要与具体的 ORM 实现完全解耦。通常注入依赖项的正确方法是使用构造函数,但在这种情况下,我们不希望每次创建 UnitOfWork 时都这样做。因此,具体的 ISessionScopeFactory(见下面的示例)依赖项通过静态属性传入 UnitOfWork 类,这是上述流畅配置的结果。

ISessionScopeFactory 实现具有单个 Open 方法,该方法在 UnitOfWork 开始时被调用以创建 ISessionScope 的具体实现。NHibernate 的 ISessionScopeFactory 实现如下所示:

public class NHibernateSessionScopeFactory: ISessionScopeFactory, ISessionScopeFactoryEx
    {
        private readonly ISessionFactory _sessionFactory;
        private ISession _session;
        
        public NHibernateSessionScopeFactory(ISessionFactory sessionFactory)
        {
            Requires.NotNull(sessionFactory, "sessionFactory");            
            _sessionFactory = sessionFactory;
        }
        
        public ISessionScope Open()
        {            
            if (_session == null)
              return new NHibernateSessionScope(_sessionFactory);

            return new NHibernateSessionScope(_session);
        }

        void ISessionScopeFactoryEx.SetSession(ISession session)
        {
            Requires.NotNull(session, "session");            
            _session = session;
        }

        void ISessionScopeFactoryEx.ResetSession()
        {
            _session = null;
        }
    }

这里唯一值得注意的是,我们允许显式设置/重置会话。但此选项很少直接在应用程序中使用 - 主要在测试项目中,当您需要在多个 UnitOfWork 之间保持单个会话时,例如编写使用内存数据库(Sqlite)的集成测试。

NHibernate 的 ISessionScope 实现如下所示:

public class NHibernateSessionScope: ISessionScope
    {
        private readonly ISession _session;
        private readonly ITransaction _transaction;
        private readonly bool _ownSession;
        
        public NHibernateSessionScope(ISessionFactory sessionFactory)
        {                        
            Requires.NotNull(sessionFactory, "sessionFactory");
            
            _session = sessionFactory.OpenSession();            
            _transaction = _session.BeginTransaction();
            _ownSession = true;
        }

        public NHibernateSessionScope(ISession session)
        {
            Requires.NotNull(session, "session");
            
            _session = session;
            _transaction = _session.BeginTransaction();
            _ownSession = false;
        }

        public void Commit()
        {
            AssertIfDone();
            try
            {
                _transaction.Commit();
            }
            catch (HibernateException)
            {
                _transaction.Rollback();
                throw;
            }            
        }
        
        public void Rollback()
        {
            AssertIfDone();
            _transaction.Rollback();
        }
        
        private void AssertIfDone()
        {
            Assumes.True(!_transaction.WasCommitted, "Transaction already was commited");
            Assumes.True(!_transaction.WasRolledBack, "Transaction already was rolled back");
        }

        public IRepository<TEntity> CreateRepository<TEntity>() where TEntity:class
        {
            return new NHibernateRepository<TEntity>(_session);
        }

        public TInternal GetInternal<TInternal>() where TInternal : class
        {
           var internalSession = _session as TInternal;
           Assumes.True(internalSession != null, "Can't cast Internal Session to TInternal type");
           return internalSession;
        }

        public void Dispose()
        {                        
            _transaction.Dispose();            
            if (_ownSession)                            
              _session.Dispose();                            
        }       
    }

ISessionScope 的具体实现实际上是底层 ORM 会话的包装器,UnitOfWork 使用它来创建、提交、回滚事务以及获取特定 ORM 的 IRepository 实现。此外,您可以深入了解以获取内部 ORM 的会话,以执行 Antler 统一语法不支持的一些特定 ORM 操作。

NHibernate 的 IRepository 实现如下所示:

public class NHibernateRepository<TEntity>: IRepository<TEntity> where TEntity: class
    {
        private readonly ISession _session;
        public NHibernateRepository(ISession session)
        {
            Requires.NotNull(session, "session");
            _session = session;
        }

        public virtual IQueryable<TEntity> AsQueryable()
        {
            return _session.Query<TEntity>();
        }

        public TEntity GetById<TId>(TId id)
        {
            return _session.Get<TEntity>(id);  
        }
        
        public TEntity Insert(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            _session.Save(entity);
            return entity;
        }

        public TId Insert<TId>(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            Requires.True(typeof(TId).IsValueType, "Only value type Ids are 
                                                    supported(int, decimal etc.)");

            return (TId)_session.Save(entity);
        }

        public TEntity Update(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            return _session.Merge(entity);
        }

        public void Delete(TEntity entity)
        {
            Requires.NotNull(entity, "entity");
            _session.Delete(entity);
        }

        public void Delete<TId>(TId id)
        {
            var entity = GetById(id);
            if (entity != null)
            {
                _session.Delete(entity);
            }            
        }        
    }

IRepository 实现允许通过 ORM 的会话执行标准的 set 操作。

结论

如果您有兴趣,可以在GitHub上找到 NHibernate、EntityFramework 和 Linq2Db 的适配器实现。

如果您想为 Antler 尚未支持的另一个(您自己的?)ORM 或 IoC 容器实现适配器,请随意。

或者您也可以直接从 NuGet 安装该框架。

核心库,以及NHibernateEntityFramework、 Linq2DbCastle WindsorStructureMap的适配器。

上一篇文章(第一部分)

© . All rights reserved.