数据持久性的设计模式
 

2010-01-18 作者:Jeremy Miller 来源:microsoft.com

 

内容

数据访问是开发人员之间热门主题。 毫无疑问您听过大量的观点在特定数据访问技术和持久性框架,但内容最佳的方式使用您的项目中的这些工具? 您应使用何种条件选择合适的工具为您的项目? 是否需要使用之前,概念上了解有关这些工具? 如果您您手上了过多的时间,想要编写自己的持久性工具内容是否需要知道?

unsurprisingly,所有这些问题答案是检查基础的设计模式,持久化。

域模型

如果您认为有关如何将结构和在系统中表示业务逻辑,有两个主要的选择。 本文,我很大程度上假设所选组织到实体对象的业务逻辑,域模型方法。

从该正式的说明域模型是包含行为和数据域的一个对象模型。

是例如当前项目包括客户关系管理。 (CRM) 我们有情况以及用户和解决方案的实体对象包含两个数据并实现业务规则涉及的数据。 域模型范围是只是一组非常丰富 jealously 保护一个窄接口 (铁杆域驱动开发) 后面的原始数据的模型的数据结构的 anemic 模型。 在您的域模型内此范围很大程度上复杂业务逻辑,在您的系统中确实是和如何流行的报告的几或数据项位于您的系统要求。

域模型模式的足够讨论超出了范围本文。 强烈建议读取的 Martin Fowler 模式企业应用程序体系结构帐簿有关组织业务逻辑的主要设计模式的好讨论的第 2 章。

快速入门之前一下认出您的系统中的数据库和数据访问代码的角色在两种主要方法:

  • 数据库是应用程序和业务资产 keystone。 数据访问代码和甚至应用程序或服务代码是只是与外部世界连接数据库的机制。
  • 中中间层和用户界面或服务层的在业务对象是应用程序,并且该数据库是可以可靠地保存在会话之间的业务对象的状态的一个手段。

个人,我就像第一个,datacentric 视点和在.NET 社区中理解使我想重点第二个视点。 在第二个视点中您通常使用的中间层中的实体对象。 一种方法或其他,您可能要做对象 / 关系映射 (O / RM) 映射到数据库表,反之亦然,业务实体中的数据。 您可能会做它手工,但很可能使用某种类型的工具。 这些工具是很好,和可能可以节省开发时间的许多,但有一些问题,您应该知道,总的为了了解一种工具如何协同工作下实际上很好。 希望,studying 本文中的模式有助于对这两个帐户。

我是一个大 proponent Agile 实践,如测试驱动的开发、 行为驱动开发、 依赖编程和连续的设计。 我说这只是清除我非常具体的斜线采用我会将讨论本文,文本模式的方面,它可能会显示。

将对象映射到数据库

我已决定我与实体中间层中的系统对象的具有标识、 数据,和相关的问题的模型。 在这些实体对象或使用这些实体对象的域服务中,将实现我的业务逻辑。 很好,但如何进行您无缝地移动数据在数据库和实体对象之间来回?

一个的关系数据库 in the case of 您要将字段和我们的对象的属性移动到表和字段在数据库中。 您可以编写此代码完全由现有,写出单独 INSERT、 UPDATE、 SELECT,和删除 SQL 语句但是您将快速实现您要重复自己在代码中非常有些。 即,您重复您指定的对象属性或字段中数据应存储到数据库表中的特定列。

这是一个对象 / 关系映射器 (O / RM) 中的步骤的。 当您使用 O / RM 时,您只需创建到数据库表的对象属性的映射,并允许使用元数据找出 SQL 语句是,如何将数据从该对象移动到 SQL 语句,O / RM 工具。

让我们获取具体并查看一个很基本的示例。 我当前的项目有一个地址类,如下所示的:

public class Address
{
  public long Id { get; set; }
  public string Address1 { get; set; }
  public string Address2 { get; set; }
  public string City { get; set; }
  public string StateOrProvince { get; set; }
  public string Country { get; set; }
  public string PostalCode { get; set; }
  public string TimeZone { get; set; }
}

我设置为地址类映射时, 我需要指定地址类映射到的表、 如何将对象的地址标识 (主关键字),和数据库表的属性映射。

例如,我映射地址的使用 Fluent NHibernate 工具。

我故意做 longhand 方式显示所有详细信息映射。 (实际的使用情况我使用 通过配置的约定 若要消除许多在 repetitiveness)。 下面的代码:

public class AddressMap : ClassMap<Address>
{
  public AddressMap()
  {
    WithTable("Address");
    UseIdentityForKey(x => x.Id, "id");

    Map(x => x.Address1).TheColumnNameIs("address1");
    Map(x => x.Address2).TheColumnNameIs("address2");
    Map(x => x.City).TheColumnNameIs("city");
    Map(x => x.StateOrProvince).TheColumnNameIs("province_code");
    Map(x => x.Country).TheColumnNameIs("country_name");
    Map(x => x.PostalCode).TheColumnNameIs("postal_code");
  }
}

现在,所做对象模型的映射数据库模型,内容必须实际执行该的映射并您大约有两种选择: 活动记录或数据映射器。

活动记录

在选择一个持久性策略时,第一个的决策,您需要将是放置负责执行映射的位置。 您有两种截然不同的选择: 可以是进行每个实体类本身负责将的映射,或者使用一个完全独立的类如何映射到数据库。

第一个选项称为活动记录模式: 封装数据库表或视图中的行对象封装数据库访问,并添加该数据的域的逻辑。 活动记录方法直接的实体对象拖到将持久性方法。 在这种情况下地址类可能会需要保存,更新,和删除以及查询地址对象的数据库的静态加载方法等的方法。

活动记录模式的典型用法是实质上是使数据库表中的一行一个强类型化的类包装。 在这种情况下,活动记录类通常是一个完全镜像数据库结构 (这会因工具)。 许多活动记录实现将直接从数据库结构生成实体对象。

最著名的活动记录实现是作为 Rails Web 开发框架拼音文字的一部分的 ActiveRecord 工具 (即 IronRuby.NET 编程中可用 ; 请参阅" 入门 IronRuby 和 RSpec,第 1 部分 "). 如果我使用 ActiveRecord,我将首先创建一个名为数据库中的地址的表。 然后,如果我关心有访问地址数据和方法查找,保存,并插入地址数据的完整地址类形式将此拼音类相似与以下:

class Address < ActiveRecord::Base

end

拼音实现使用 metaprogramming 动态创建字段和与数据库表中名为在运行时的地址 (类名地址的 pluralized 窗体) 匹配的查询方法。

但少数的情况例外.NET 实现的活动记录模式通常通过工作使用代码生成创建.NET 类直接数据库表的映射。 此方法的好处是很非常容易使同步数据库和对象模型。

了解编程

依靠编程,讲解通过 favoring"拉"设计通过"推"设计消除浪费的努力开发项目中。 这意味着持久性应只是设计和等生成满足业务要求 (根据请求) 的需求的基础结构问题,而不是生成您认为,应用程序需要更高版本的数据访问层代码 (推送)。

在我的经验,这意味着您应该通过垂直开发增量开发系统通过一次而不是一次生成系统一水平层构建一个功能。 使用该基础结构可以确保这种方式开发为超过您绝对需要通过将所有基础结构代码生成在系统中的一个功能。 水平工作之前您编写用户界面、 服务或业务逻辑层构建数据访问层或数据库时, 风险以下:

  • 不获取足够的反馈早期从项目利益相关者因为没有任何工作功能来演示
  • 编写功能可能不实际获取生成不必要的基础结构的代码
  • 生成错误的数据访问代码,因为您没有理解业务要求早期在或之前创建其他图层,将更改要求

请求的设计也意味着您选择的数据访问策略应确定由应用程序的需求。 提供一个选择,我需要选择 O / RM 系统我已在此建模域模型中的业务逻辑的。 报告的应用程序我将而只使用 SQL 和数据集完全忽略持久性工具。 对于大部分数据输入的系统,我可以选择一个活动记录工具。 关键是数据访问和持久性形状由其使用者的需求。

数据映射器

因为该活动的记录模式可能会很好,通常是将不同于数据库模型的一个对象结构有价值。 可以将多个类映射到同一个数据库表。 您可能不适合您要对 Express 一些业务逻辑 (通常会出现在我最后一个的多个项目) 中对象的形状的旧数据库架构。

这是我将在此选择数据映射器模式: 一层对象和数据库之间移动数据,同时使它们保持为独立于彼此和在映射器的映射器对象类本身。 数据映射器模式去除大部分持久性从实体对象的类外部实体的责任。 与一个数据映射器模式中,您访问,查询,并使用某种类型的存储库 (本文稍后讨论) 保存实体的对象。

如何在选择二者之间? 向数据映射器解决方案,我个人的偏差的是很强。 该备用,活动记录是最适用于具有简单域逻辑 CRUD 大量应用程序的系统 (创建、 读取、 更新和删除,即),和域模型不需要分化得从数据库结构的情况。 活动记录由于它暗示的工作就是多个常见的.NET 团队 datacentric 方式可能更喜欢的许多.NET 开发团队,并且 frankly,得更好地支持.NET 本身。 我将为活动记录工具描述在.NET 空间中的大多数持久性工具。

而在另一方面,数据映射器是更适合于具有复杂的域的逻辑的系统的域模型的形状将分化得从数据库模型。 数据映射器还 decouples 从持久性存储您域模型类。 这可能是重要的情况下,您需要完全重新使用域模型与其他数据库引擎、 架构或甚至指向不同的存储机制。

最重要我为灵活的 Practitioner,数据映射器方法使我能够与测试驱动开发的效率比一个活动记录解决方案设计独立于数据库对象模型无法。 这一重要到需要设计,因为它允许团队能够通过对象模型中设计第一次在重构工具,并单元测试技术通常更有效,然后在对象模型处于稳定时更高版本创建数据库模型的增量方法的团队。 数据映射器模式也是更适用域驱动获得.NET 社区中的普及中设计体系结构方法。

使用一个存储库

我已增大时设置作为一名开发人员在 Windows DNA 天,我将所在 mortal 担心忘记关闭我的代码中的 ADO 连接对象。 对我第一个的大企业项目,我编码直接对 ADO。 每次我需要请求,或保存到数据库的数据,我必须执行以下:

  1. 找到从某种配置的连接字符串
  2. 打开新连接到数据库
  3. 创建 Command 对象或 Recordset 对象
  4. 执行 SQL 语句或存储的过程
  5. 关闭该连接可以释放数据库资源
  6. 和哦是的,将解决数据访问代码处理一些足够错误

第一个问题是代码的重复,我已编写 absurd 量。 第二个问题是我必须编写代码正确每个单个的时间因为忘记关闭单个连接未能,遗憾的是做,驱动器关键系统脱机在重负载下。 没有人想要将其代码折叠超出较差的数据库连接有利于的人,但它仍然频繁发生。

我下的项目中我的同事,我获得更智能化。 我们实现将执行所有安装程序、 拆卸和错误处理代码 ADO Connection 对象的一个类。 在系统中的任何其他类可以通过此中心类调用存储的过程进行交互与数据库。 代码,以及更好地还,我们不 plagued 问题由忘记关闭数据库连接,因为我们需要获取该连接权限管理代码一次,我们编写更低。

我的团队是创建用于访问域对象使用集合的类似于接口映射层 mediates 域和数据之间的存储库图案的一个粗略实现。

该存储库模式基本上,只是意味着持久性系统上放置一个外观,以便可以 shield 不必知道您的应用程序代码的其余持久性的工作原理。 我当前的项目一个存储库使用类似 图 1 中的公用接口。

图 1 储存库接口

public interface IRepository
{
  // Find an entity by its primary key
  // We assume and enforce that every Entity
  // is identified by an "Id" property of
  // type long
  T Find<T>(long id) where T : Entity;

  // Query for a specific type of Entity
  // with Linq expressions.  More on this later
  IQueryable<T> Query<T>();
  IQueryable<T> Query<T>(Expression<Func<T, bool>> where);

  // Basic operations on an Entity
  void Delete(object target);
  void Save(object target);
  void Insert(object target);

  T[] GetAll<T>();
}

当在系统中的一些类需要访问实体对象时,可以只使用 IRepository 按 ID 或使用 LINQ 表达式的实体对象的列表的查询获取该实体。

使用它使用 O / RM,NHibernate 库的 IRepository 的具体类时我请求从内存,连接字符串从一个程序集生成 NHibernate SessionFactory 加载映射定义 (一次和仅一次一个大的性能下降,因为),和换行 NHibernate 在低级 ISession 界面。 从存储库类某些帮助,NHibernate 管理数据库连接生命周期问题。

哇。 这是在后台正在运行的内容很多。 就我已经 swept 与 NHibernate IRepository 接口后面,直接交互的所有好事。 我没有知道的引导 NHibernate 若只是要加载、 保存,并查询对象的内容。 甚至更好因为每个类取决于抽象的 IRepository 接口,用于数据访问和持久性我可以滑动到存根数据库测试过程中在内部对象使用 LINQ 的 IRepository InMemoryRepository 实现。

标识映射

让我们看一个常见的方案。 在传送系统的某种在单个逻辑事务,必须单独,工作的两个完全不同的类,但这两个类将需要事务过程中检索相同的客户实体。 理想情况下,您需要仅在单个事务一个单个的客户对象为每个逻辑客户以便每个对象正在关闭的一致的数据。

持久性工具,防止重复的逻辑引用是标识映射图案的作业。 通过 Martin Fowler 所述,一个标识映射将确保每个对象通过保持每个加载的对象图中一次加载的并且查找引用它们时使用使用映射的对象。

在客户类可能会生成类似 图 2 中的身份映射的一个简单实现。 我故意将出此处锁定只是为了简化代码的线程。 一个实际的实现需要足够的线程安全措施。

图 2 标识映射客户类

public class CustomerRepository
{
  public IDictionary<long, Customer> _customers =
    new Dictionary<long, Customer>();

  public Customer FindCustomer(long id)
  {
    if (_customers.ContainsKey(id))
  {
      return _customers[id];
    }

    var customer = findFromDatabase(id);
    _customers.Add(id, customer);

    return customer;
  }

  private Customer findFromDatabase(long id)
  {
    throw new System.NotImplementedException();
  }
}

在此示例,客户对象由标识其 ID。 当您请求客户 ID 的实例时,在 CustomerRepository 将首先检查以查看它是否具有该特定客户的一个内部词典。 如果是,它将返回现有的客户对象。 否则为 CustomerRepository 将从数据库提取数据、 生成新的客户,将该客户对象存储用于更高版本的请求的词典并返回新客户对象。

幸运的是,您通常不会编写此代码手动因为任何成熟的持久性工具应包含此功能。 必须知道这发生在后台和相应作用域在持久性支持对象。 许多团队将使用管理工具 (StructureMap、 Windsor、 Ninject,及其他) 以确保单个 HTTP 请求或线程中的所有类都使用相同的基本标识映射的一个反转生命周期管理的功能。 工作模式的单位是另一种跨多个类在同一逻辑事务中管理单个标识映射的方法。

只是为了说明这种模式向, 图 3 显示一个标识映射的工作原理的示例。 对当前项目的体系结构编写代码。 下面代码中所示 IRepository 接口的实例包装一个又实现标识映射模式的 NHibernate ISession。 当我运行此测试输出是:

1 passed, 0 failed, 0 skipped, took 5.86 seconds.

图 3 使用一个标识映射

[Test]
public void try_out_the_identity_map()
{
  // All I'm doing here is getting a fully formed "Repository"
  // from an IoC container and letting an IoC tool bootstrap
  // NHibernate offstage.
  IRepository repository = ObjectFactory.GetInstance<IRepository>();

  // Find the Address object where Id == 1
  var address1 = repository.Find<Address>(1);

  // Find the Address object where Id == 1 from the same Repository
  var address2 = repository.Find<Address>(1);

  // Requesting the same identified Address object (Id == 1) inside the
  // same Repository / Identity Map should return the exact same
  // object
  address1.ShouldBeTheSameAs(address2);

  // Now, let's create a completely new Repository that has a
  // totally different Identity Map
  IRepository secondRepository = ObjectFactory.GetInstance<IRepository>();

  // Nothing up my sleeve...
  repository.ShouldNotBeTheSameAs(secondRepository);

  var addressFromSecondRepository = secondRepository.Find<Address>(1);

  // and this is a completely different Address object, even though
  // it's loaded from the same database with the same Id
  addressFromSecondRepository.ShouldNotBeTheSameAs(address1);
}

延迟和是加载

有关使用持久性工具的最佳操作之一是能够加载根对象 (发票,可能),然后直接定位到其子项 (InvoiceLineItem) 和相关对象只是通过使用父类的属性。 但是,提前或更高版本您将必须关注应用程序的性能。 当您可能需要仅顶层时获取整个对象关系图对象大部分时间或可以省略部分对象的图形不是有效。
这是确定。 在这种情况下可以使用延迟加载模式中初始化对象之前,需要它之前的延迟。

让我们将此放入更具体的条件。 假设您有名为引用的地址对象的客户在使用的类:

public class Customer : DomainEntity
{
// The "virtual" modifier is important. Without it,
// Lazy Loading can't work
public virtual Address HomeAddress { get; set; }
}

在大多数使用涉及客户对象的情况下,代码不需要将 Customer.HomeAddress 属性。 在这种情况下可以设置数据库映射使该 Customer.HomeAddress 属性延迟加载像此 Fluent NHibernate 映射:

public class CustomerMap : DomainMap<Customer>
{
  public CustomerMap()
  {
    // "References" sets up a Many to One
    // relationship from Customer to Address
    References(x => x.HomeAddress)
      .LazyLoad() // This marks the property as "Lazy Loaded"
      .Cascade.All();
  }
}

与打开的延迟加载,客户对象获取没有地址数据中。 但是,为任何调用方尝试第一次访问 Customer.HomeAddress 属性,该数据将以透明方式加载。

注意 Customer.HomeAddress 属性 virtual 修饰符。 并非每个持久性工具这,但 NHibernate 实现通过创建客户重写 HomeAddress 属性以使其延迟加载的动态子类的延迟加载的属性。 HomeAddress 属性需要标记为虚拟,以使子类重写属性。

当然,有其他时候,当您请求的对象,并且知道很可能需要其子项在同一时间。 在这种情况下您可能将选择的是加载并具有与父同时加载的子数据。 很多持久化工具需要优化 Eager 加载方案来提取单个数据库的往返行程中的数据的层次结构的能力的某种。 如果大多数情况下使用客户对象需要 Customer.HomeAddress 数据则您将为好执行一次获取客户和地址数据的是加载。

到目前为止,我应重复该旧 maxim 只有这样才能够可靠地优化性能的应用程序的性能可经验度量值使用分析器的。

虚拟的代理服务器模式

延迟加载通常是通过使用一个虚拟的代理服务器对象,该外观非常类似实际对象更高版本在加载对象实现的。 假设域模型包含一个名为引用的 Customer 对象列表的 CustomerRepresentative 的类。 该类的一部分如 图 4 所示。

图 4 CustomerRepresentative

public class CustomerRepresentative
{
  // I can create a CustomerRepresentative directly
  // and use it with a normal List
  public CustomerRepresentative()
  {
    Customers = new List<Customer>();
  }

  // Or I can pass an IList into it.
  public CustomerRepresentative(IList<Customer> customers)
  {
    Customers = customers;
  }

  // It's not best practice to expose a collection
  // like I'm doing here, but it makes the sample
  // code simpler ;-)
  public IList<Customer> Customers { get; set; }
}

系统使用 CustomerRepresentative 的实例而无需客户列表时,有很多时候。 在这种情况下可以只需构造一个 CustomerRepresentative 与虚拟的代理服务器对象看起来像一个 IList <customer> 对象,使用该虚拟的代理类,而不对 CustomerRepresentative 管进行任何更改。 该虚拟的代理类可能与 图 5 类似。 如 图 6 所示,然后可以通过延迟加载创建一个 CustomerRepresentative 对象。

图 5 CustomerRepresentative 虚拟代理服务器

public class VirtualProxyList<T> : IList<T>
{
  private readonly Func<IList<T>> _fetcher;
  private IList<T> _innerList;
  private readonly object _locker = new object();

  // One way or another, VirtualProxyList needs to
  // find the real list.  Let's just cheat and say
  // that something else will pass it a closure
  // that can find the real List
  public VirtualProxyList(Func<IList<T>> fetcher)
  {
    _fetcher = fetcher;
  }

  // The first call to
  private IList<T> inner
  {
    get
    {
      if (_innerList == null)
      {
        lock (_locker)
        {
          if (_innerList == null)
          {
            _innerList = _fetcher();
          }
        }
      }

      return _innerList;
    }
  }


  IEnumerator IEnumerable.GetEnumerator()
  {
    return inner.GetEnumerator();
  }

  public IEnumerator<T> GetEnumerator()
  {
    return inner.GetEnumerator();
  }

  public void Add(T item)
  {
    inner.Add(item);
  }

  // and the rest of the IList<T> implementation

}

图 6 延迟加载 CustomerRepresentative

public class CustomerRepresentativeRepository
{
  private readonly ICustomerRepository _customers;

  public CustomerRepresentativeRepository(
      ICustomerRepository customers)
  {
    _customers = customers;
  }

  // This method will "find" a CustomerRepresentative, and
  // set up the Virtual Proxy for the Customers
  public CustomerRepresentative Find(long id)
  {
    var representative = findRepresentative(id);
    representative.Customers =
      new VirtualProxyList<Customer>(() =>
      _customers.GetCustomersForRepresentative(id));

    return representative;
  }
}

如本文中模式大部分,虚拟的代理服务器不是这可能要手动,编写但请注意它的存在您的持久性工具的背景中。

执行下一步

当我开始使用 Windows DNA 技术编程时,我可能花上使用原始的 ADO 代码我时间的一半。 目前,持久性编码和基础结构为我的工作组时间的非常小百分比。 因此,内容更改年? 我们使用持久性工具和这些设计模式消除,许多重复编码我们使用来执行。

这些模式的大部分是取自 Martin Fowler 简介册的企业应用程序体系结构模式。 我强烈建议阅读本书,如果您有与编写企业应用程序的任何内容。 由于为长度的限制我不能覆盖单位工时、 规范和持久性不像某些其他重要模式。 此外,还有很多方面将在存储库模式和设计注意事项 (一个存储泛型库与特定,"缩小"存储库的类是否存储库应该甚至公开"保存"方法,等等)。 我将 urge 您研究这些主题以及,和我可能写入继续讨论在后续文章。

最后,我想说.NET 生态系统是比只是实体框架和 LINQ to SQL 更丰富。 我满意 NHibernate 作为数据相对知道少有关持久化的映射。 subSonic 是常见的活动记录实现进行.NET 编程。 iBatis.net 是美妙的现有数据库,或您希望完整的情况下控制编写 SQL 语句。 LLBLGen Pro 是唯一的查询功能非常成熟工具。 许多其他工具还可以使用 LINQ 查询。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织