DDD理论学习系列(12)-- 仓储

DDD理论学习系列——案例及目录html


1. 引言

DDD中Repository这个单词,主要有两种翻译:资源库仓储,本文取仓储之译。web

说到仓储,咱们确定就想到了仓库,仓库通常用来存放货物,而仓库通常由仓库管理员来管理。当工厂生产了一批货物时,只需交给仓库管理员便可,他负责货物的堆放;当须要发货的时候,仓库管理员负责从仓库中捡货进行货物出库处理。当须要库存盘点时,仓库管理员负责核实货物状态和库存。换句话说,仓库管理员负责了货物的出入库管理。经过仓库管理员这个角色,保证了仓库和工厂的独立性,工厂只须要负责生产便可,而至于货物如何存放工厂无需关注。sql

而咱们要讲的仓储就相似于仓库管理员,只不过它负责的再也不是货物的管理,而是聚合的管理,仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便咱们关注于领域模型而不须要考虑如何进行持久化。数据库

2. DDD中的仓储

2.1. 仓储的集合特性

仓储表明一个聚合的集合,其行为与.Net集合同样,仓储用来存储和删除聚合,但同时提供针对聚合的显式查询以及汇总。安全

2.2. 仓储与数据访问层的区别

  1. 仓储限定了只能经过聚合根来持久化和检索领域对象,以确保全部改动和不变性由聚合处理。
  2. 仓储经过隐藏聚合持久化和检索的底层技术实现领域层的的持久化无关性(即领域层不须要知道如何持久化领域对象)。
  3. 仓储在数据模型和领域模型定义了一个边界。

2.3. 仓储举例

下面咱们首先来看一个简单仓储的定义:服务器

namespace DomainModel
{
 public interface ICustomerRepository
 {
 Customer FindBy(Guid id);
 void Add(Customer customer);
 void Remove(Customer customer);
 }
}

一般来讲,仓储由应用服务层调用。仓储定义应用服务执行业务用例时须要的全部的数据访问方法。而仓储的实现一般位于基础架构层,由持久化框架来支撑。如下的仓储实现是借助于ORM框架Nhibernate的ISession接口,它扮演一个的网关角色,负责领域模型和数据模型的映射。session

namespace Infrastructure.Persistence {
    public class CustomerRepository : ICustomerRepository {
        private ISession _session;
        public CustomerRepository (ISession session) {
            _session = session;
        }
        public IEnumerable<Customer> FindBy (Guid id)
            return _session.Load<Order> (id);
        }

        public void Add (Customer customer) {
            _session.Save (customer);
        }

        public void Remove (Customer customer) {
            _session.Delete (customer);
        }
    }
}

从上面咱们能够看出,将领域模型的持久化转移到基础设施层,隐藏了领域模型的技术复杂性,从而使领域对象可以专一于业务概念和逻辑。架构

2.4. 仓储的误解

仓储也存在不少误解,许多人认为其是没必要要的抽象。当应用于简单的领域模型时,能够直接使用持久化框架来进行数据访问。然而当对复杂的领域模型进行建模时,仓储是模型的扩展,它代表聚合检索的意图,能够对领域模型进行有意义的读写,而不是一个技术框架。app

也有不少人认为仓储是一种反模式,由于其隐藏了基础持久化框架的功能。而恰巧这正是仓储的要点。基础持久化框架提供了开放的接口用于对数据模型的查找和修改,而仓储经过使用定义的命名查询方法来限制对聚合的访问。经过使查询显式化,就更容易调整查询,且更重要的是仓储明确了查询的意图,便于领域专家理解。举个例子:咱们在仓储中定义了一个方法GetAllActiveUsers()与sql语句select * from users where isactive = 1var users =db.Users.Where(u=>u.IsActive ==1)相比,很明显仓储的方法命名就能让咱们明白了查询的意图:查询全部处于Active状态的用户。除了查询,仓储仅暴露必要的持久化方法而不是提供全部的CURD方法。框架

2.5. 仓储的要点

仓储的要点并非使代码更容易测试,也不是为了便于切换底层的持久化存储方式。固然,在某种程度上,这也的确是仓储所带来的利好。仓储的要点是保持你的领域模型和技术持久化框架的独立性,这样你的领域模型能够隔离来自底层持久化技术的影响。若是没有仓储这一层,你的持久化基础设施可能会泄露到领域模型中,并影响领域模型完整性和最终一致性。

3. 领域模型 VS 数据模型

若是选择关系型数据库做为持久化存储,咱们能够借助于ORM框架来实现领域模型和数据模型之间的映射和持久化操做。

而ORM又是什么呢?

按照文章开头中的例子,若是仓储对应仓库管理员的角色,那ORM就至关于仓库机器人,而仓库就至关于数据库。为了方便不一样商品的归类存放,对仓库进行分区,分区就至关于数据表。当公司接到一笔订单作发货处理时,销售员将发货通知单告知仓库管理员,仓库管理员再分配ORM机器人进行捡货。很显然,ORM机器人必须可以识别发货通知单,将发货通知单中的商品对应到仓库中存储的货物。这里面发货通知单就至关于领域模型,而仓库中存储的货物就属于数据模型。

相信基于上面的比喻,咱们对ORM有了基本的认识。ORM,全称是Object Relational Mapping,对象关系映射。ORM的前提是,将对象的属性映射到数据库字段,将对象之间的引用映射到数据库表的关系。换句话说,ORM负责将代码中定义的对象和关系映射到数据库的表结构中去,并在进行数据访问时再将表数据映射到代码中定义的对象,借助ORM咱们不须要去手动写SQL语句就能够完成数据的增删改查。ORM仅仅抽象了关系数据模型,它只是以面向对象的方式来表示数据模型,以方便咱们在代码中轻松地处理数据。

下面咱们来探讨一下数据模型与领域模型的异同。关系数据库中的数据模型,它由表和列组成,它只是简单的存储结构,用于保存领域模型某个时间点的状态。数据模型能够分散在几个表甚至几个数据库中。此外,可使用多种形式的持久化存储,例如文件、web服务器、关系数据库或NoSQL。领域模型是对问题域的抽象,具备丰富的语言和行为,由实体和值对象组成。对于一些领域模型,可能与数据模型类似,甚至相同,但在概念上它们是很是不一样的。ORM与领域模型无关。仓储的做用就是将领域模型与数据模型分开,而不是让它们模糊成一个模型。ORM不是仓储,可是仓储可使用ORM来持久化领域对象的状态。

若是你的领域模型与你的数据模型相似,ORM能够直接映射领域模型到数据存储,不然,则须要对ORM进行额外的映射配置。

4. 仓储的定义和实现

上面也提到过,咱们通常在领域层定义仓储接口,在基础设施层实现仓储,以隔离领域模型和数据模型。

4.1. 仓储方法需明确

仓储是原则上是领域模型与持久化存储之间明确的契约,仓储定义的接口方法不只仅是CURD方法。它是领域模型的扩展,并以领域专家所理解的术语编写。仓储接口的定义应该根据应用程序的用例需求来建立,而不是从相似CURD的数据访问角度来构建。

咱们来看一段代码:

namespace DomainModel {
    public interface ICustomerRepository {
        Customer FindBy (Guid id);
        IEnumerable<Customer> FindAllThatMatch (Query query);
        IEnumerable<Customer> FindAllThatMatch (String hql);
        void Add (Customer customer);
    }
}

以上仓储定义了一个FindAllThatMatch方法以支持客户端以任何方式查询领域对象。这个方法的设计思想无可置否,灵活且能够扩展,可是它并无明确的代表查询的意图,咱们就失去了对查询的控制。为了真正了解如何使用这些方法,开发人员须要跟踪相关调用堆栈,才能知悉方法的意图,更别说出现性能问题时如何着手优化了。由于仓储定义的接口方法过于宽泛且不具体,它模糊了领域的的概念,因此定义这样的一个接口方法是无心义的。

咱们能够以下改造:

namespace DomainModel {
    public interface ICustomerRepository {
        Customer FindBy (Guid id);
        IEnumerable<Customer> FindAllThatAreDeactivated ();
        IEnumerable<Customer> FindAllThatAreOverAllowedCredit ();
        void Add (Customer customer);
    }
}

经过以上改造,咱们经过方法的命名来明确查询的意图,符合通用语言的规范。

4.2. 泛型仓储

在实践中咱们可能会发现,为每个聚合定义一个仓储会致使重复代码,由于大部分的数据操做都是相似的。为了代码重用,泛型仓储就应时而生。

泛型仓储举例:

namespace DomainModel {
    public interface IRepository<T> where T : EntityBase {
        T GetById (int id);
        IEnumerable<T> List ();
        IEnumerable<T> List (Expression<Func<T, bool>> predicate);
        void Add (T entity);
        void Delete (T entity);
        void Edit (T entity);
    }

    public abstract class EntityBase {
        public int Id { get; protected set; }
    }
}

泛型仓储实现:

namespace Infrastructure.Persistence {
    public class Repository<T> : IRepository<T> where T : EntityBase {
        private readonly ApplicationDbContext _dbContext;
        public Repository (ApplicationDbContext dbContext) {
            _dbContext = dbContext;
        }
        public virtual T GetById (int id) {
            return _dbContext.Set<T> ().Find (id);
        }

        public virtual IEnumerable<T> List () {
            return _dbContext.Set<T> ().AsEnumerable ();
        }

        public virtual IEnumerable<T> List (Expression<Func<T, bool>> predicate) {
            return _dbContext.Set<T> ()
                .Where (predicate)
                .AsEnumerable ();
        }

        public void Insert (T entity) {
            _dbContext.Set<T> ().Add (entity);
            _dbContext.SaveChanges ();
        }

        public void Update (T entity) {
            _dbContext.Entry (entity).State = EntityState.Modified;
            _dbContext.SaveChanges ();
        }

        public void Delete (T entity) {
            _dbContext.Set<T> ().Remove (entity);
            _dbContext.SaveChanges ();
        }
    }
}

经过定义泛型仓储和默认的实现,很大程度上进行了代码重用。可是,尝试将泛型仓储应用全部仓储并非一个好的主意。对于简单的聚合咱们能够直接使用泛型仓储来简化代码。但对于复杂的聚合,泛型仓储可能就会不太适合,若是基于泛型仓储的方法进行数据访问,就会模糊对聚合的访问意图。

对于复杂的聚合,咱们能够从新定义:

namespace DomainModel {
    public interface ICustomerRepository {
        Customer FindBy (Guid id);
        IEnumerable<Customer> FindAllThatAreDeactivated ();
        void Add (Customer customer);
    }
}

在实现时,咱们能够引用泛型仓储来避免代码重复。

namespace Infrastructure.Persistence {
    public class CustomerRepository : ICustomerRepository {
        private IRepository<Customer> _customersRepository;
        public Customers (IRepository<Customer> customersRepository) {
            _customersRepository = customersRepository;
        }
        // ....
        public IEnumerable<Customer> FindAllThatAreDeactivated () {
            _customersRepository.List(c => c.IsActive == false);
        }
        public void Add (Customer customer) {
            _customersRepository.Add (customer);
        }
    }
}

经过这种方式,咱们即明确了查询了意图,又简化了代码。

4.3. IQueryable Vs IEnumerable

在定义仓储方法的返回值时,咱们可能会比较疑惑,是应该直接返回数据(IEnumerable)仍是返回查询(IQueryable)以便进行进一步的细化查询?返回IEnumerable会比较安全,但IQueryable提供了更好的灵活性。事实上,若是使用IQueryable做为返回值,咱们仅提供一种读取数据的方法便可进行各类查询。
可是这种方式就会引入一个问题,就是业务逻辑会渗透到应用层中去,并出现大量重复。好比,在实体中咱们通常使用IsActiveIsDeleted属性来表示软删除,而一旦实体中的某条数据被删除,那么UI中基本不会再显示这条数据,那对于实体的查询都须要包含相似Where(c=> c.IsActive)的linq表达式。对于这种问题,咱们最好在仓储中的方法中,好比List()或者ListActive()作默认处理,而不是在应用服务层每次去指定查询条件。
但具体是返回 IQueryable仍是IEnumerable每一个人的见解不一,具体可参考Repository 返回 IQueryable?仍是 IEnumerable?

5. 事务管理和工做单元

事物管理主要是应用服务层的关注点。然而,由于仓储和事物管理紧密相关的。仓储仅关注单一聚合的管理,而一个业务用例可能会涉及到多种的聚合。

事物管理由UOW(Unit of Work)处理。UOW模式的做用是在业务用例的操做中跟踪聚合的全部更改。一旦发生了更改,UOW就使用事务来协调持久化存储。为了确保数据的完整性,若是提交数据失败,则会回滚全部更改,以确保数据保持有效状态。

而关于UOW又是一个复杂的话题,咱们后续再讲。

6. 仓储的反模式(注意事项)

  1. 不要支持临时查询(ad hoc query)
    仓储不该该开放扩展,不要为了支持多种形式的查询,定义比较宽泛的查询方法,它不只不能明确表达仓储查询的意图,更可能会致使查询性能。
  2. 延迟加载是一种设计臭味
    聚合应围绕不变性构建,并包含全部必需的属性去支持不变性。 所以,当加载聚合时,要么加载全部,要么一个也不加载。 若是您有一个关系数据库而且正在使用ORM做为数据模型,那么您可能可以延迟加载一些领域对象属性,这样就能够推迟加载不须要的聚合部分。可是,这样作的问题是,若是您只能部分加载聚合,可能会致使您的聚合边界错误。

  3. 不要使用聚合来实现报表需求
    报表可能会涉及到多个类型的聚合,而仓储是处理单一聚合的。另外仓储是基于事务的,可能会致使报表的性能问题。

7. 总结

  1. 仓储做为领域模型和数据模型的中介,它负责映射领域模型到持久化存储。
  2. 仓储实现了透明持久化,即领域层不须要关注领域对象如何持久化。
  3. 仓储是一个契约,而不是数据访问层。它明确代表聚合所必需的数据操做。
  4. ORM框架不是仓储。仓储是一种架构模式。ORM用来以面向对象的方式来表示数据模型。仓储使用ORM来协调领域模型和数据模型。
  5. 仓储适用于具备丰富领域模型的限界上下文。对于没有复杂业务逻辑的简单限界上下文,直接使用持久化框架便可。
  6. 使用UOW进行事务管理。UOW负责跟踪对象的状态,仓储在UOW协调的事务中进行实际的持久化工做。
  7. 仓储用于管理单个聚合,它不该该控制事务。

参考资料:
领域驱动设计(DDD)的实践经验分享之持久化透明
Repository Pattern--A data persistence abstraction
领域驱动设计(DDD)的实践经验分享之ORM的思考

相关文章
相关标签/搜索