持久化模式,第 2 部分: 提高代码重用和改进性能,更多的现代 ORM 工具策略和最佳实践
2009-06-25 15:31
831 查看
更多的现代 ORM 工具策略和最佳实践 |
Ryan Senior (ryan@sourceallies.com), 顾问, Source Allies, Inc. Travis Klotz (travisklotz@sourceallies.com), 顾问, Source Allies, Inc. Jim Majure (jim@sourceallies.com), 顾问, Source Allies, Inc. 2008 年 5 月 22 日 第 1 部分 讨论用现代对象-关系映射(ORM)工具实现一致且紧凑的领域模型和持久化层的基本概念。在第 2 部分中,作者将描述基领域实体、领域模型中的行为以及泛型 DAO 的高级特性。还要提供改进领域模型的数据获取性能的策略。 简介 本系列包含两篇文章,第 1 部分 讨论了 Hibernate 和其他对象-关系映射(ORM)工具的几个基本最佳实践。通过使用通用基领域类和接口、集中的审计和泛型数据访问对象(泛型 DAO),应用程序可以建立更紧凑且可维护的领域模型和持久化层。 通过应用 第 1 部分 中的概念,可以提供新的代码重用机会。在这个部分中,我们首先讨论如何使用 Hibernate 和多态性在领域模型中集成行为。接下来,继续 第 1 部分 对泛型 DAO 的讨论。在应用程序中集成和使用泛型 DAO 之后,您可能会发现更多的通用操作。我们将演示如何在泛型 DAO 中集成数据分页和查询,从而减少代码。最后,讨论改进领域模型性能的策略。如果不采用这些策略,领域模型中配置错误的关联可能会导致大量多余的查询,也可能获取不需要的记录,从而浪费资源。 再论模型:让 ORM 选择行为 因为数据库表本身没有行为,开发人员常常把领域模型实体的行为放在服务或视图层中。但是,这种方式并不合适,因为这违反了面向对象的基本规则:对象拥有行为和数据。如果把行为从对象转移到服务中,对象就成了纯粹的数据容器。另外,把实体的行为放在服务或视图层中,会导致实体的核心逻辑分散在应用程序中的许多地方,导致维护问题。Hibernate 等工具有助于把行为与数据一起放在模型中,这有助于构造领域驱动的模型。 我们继续以 第 1 部分 中使用的职员示例为例。图 1 给出的对象模型定义了工资支付方式功能: 图 1. 工资支付方式计算的对象模型 假设数据库包含一个 Employee表,这个表与 PayRate表相关联。工资支付方式表有一个 employeeType列,这个列有两个有效值: Hourly和 Salary。用来计算职员工资的算法取决于这个列的值,因为小时制职员的工资计算方法与周薪制职员的工资计算方法不一样。周薪制职员每周领取相同的工资,无论他们一周工作了多少小时。对于小时制职员,必须根据他们工作的小时数支付工资,还可能需要计算加班时间。 如果把工资计算代码放在领域实体中,就会产生更紧凑的领域模型,而且避免把逻辑分散在各层中。工资支付方式示例使用单一表继承。为此,首先需要定义一个识别器(discriminator) — 识别器是表中的一列,它告诉 Hibernate 为了表示数据库中的数据应该实例化的对象类型。对于这个示例, employeeType列作为识别器。接下来,必须定义超类,这个类作为这些实体的基类 — 这个示例使用抽象类 PayRate。清单 1 给出这个超类的注解和类声明: 清单 1. 超类的注解和类声明
清单 2. 子类的定义
PayRate表时,如果 employeeType="Salary",Hibernate 就自动创建 SalaryPayRate的实例;如果 employeeType="Hourly",就创建 HourlyPayRate的实例。然后,应用程序代码可以调用 createPayCheck方法,并确保用正确的算法计算工资。Hibernate 作为这些类的工厂,它会在正确的时间创建正确的实例。 在编写查询时,Hibernate 还能够感知多态性。在编写寻找所有周薪制职员的查询时,Hibernate 会在查询中加上 employeeType="Salary",这样就能够获得所需的结果。清单 3 给出一个多态性查询示例;注意,这个查询并没有什么特殊之处: 清单 3. 多态性查询
利用多态性隐藏信息 使用 Hibernate 为模型中的实体选择行为是很不错,但是对于不同的实体常常还需要不同的数据集。如果一些字段对于某个子类有意义,但是对于其他子类没有意义,就可以把它们从超类中删除,并放到它们所属的子类中。这可以避免未来的代码维护者意外使用不属于当前实例的字段。 例如,为了计算小时制职员的工资,需要加班时间信息,而这一信息对于计算周薪制职员的工资是不必要的。加班工资对于周薪制职员没有意义,所以可以把它从 PayRate类转移到 HourlyRate类中。同样, yearlyRate字段不属于 HourlyRate类,可以转移到 SalaryRate类中。 PayRate类定义周薪制职员和小时制职员通用的字段,见清单 4: 清单 4. PayRate类
SalaryRate类继承这些字段并添加一个 yearlyRate字段,见清单 5: 清单 5. SalaryRate类
随着越来越多的应用程序使用泛型 DAO,您会在数据访问层中发现更多的通用功能。提取通用行为并把它们包含在泛型 DAO 中,让类的所有用户通过泛型 DAO 访问这些功能,这样就可以减少代码重复。 常见的两种通用操作是根据搜索参数执行查询和数据库级的分页。 查询 几乎所有持久化实体都需要某种查询功能。通常通过给对象中要搜索的一个或多个字段输入搜索文本(有时候支持通配符功能)来指定查询。一种查询方法是,创建持久化对象的一个实例,然后填充需要用搜索条件搜索的字段。 Hibernate 通过示例(example) 查询支持这种方式。 图 2 给出扩展的 BaseDao接口,它支持按示例查询功能: 图 2. 扩展的 BaseDao接口 在给出这个接口的实现之前,我们先考虑一下如何使用这样的 DAO。请考虑以下代码,这段代码演示了对某个持久化实体执行查询的过程:
getAllByExample()方法的方式: 清单 6. getAllByExample方法的实现
分页 在执行产生大型结果集的数据库查询时,常常对结果进行分页,然后让用户在页面之间导航。有两种实现分页的方式。第一种是执行查询,从数据库获得完整的结果集,然后每次只向用户显示结果集中的一页。由于要传输完整的结果集并把它存储在用户会话中,这种方式需要成本。 另一种方式是在数据库中执行分页。这会减少资源消耗,但是需要比较复杂的编程模型。幸运的是,通过扩展泛型 DAO 可以使这种功能适应各种对象类型。图 3 给出了进一步扩展的 BaseDao接口: 图 3. 支持分页的 BaseDao接口 图 3 引入了 PageInfo类,这个类识别应该获取的数据页面。 getPageAll方法的实现见清单 7: 清单 7. getPageAll方法的实现
在对 Hibernate 应用程序进行性能调优时,大部分时间花在调整 Hibernate 处理实体关联的方式上。我们希望尽可能减少装入应用程序中的数据量,同时尽可能减少需要执行的查询数量。Hibernate 提供了两种处理关联的主要方式:惰性抓取(lazy fetching)和即时抓取(eager fetching)。 惰性抓取 惰性抓取可以尽可能减少从数据库查询到的数据量。如果关联被标为惰性的,那么在装载对象时并不通过关联查询所有数据,而是直到实际使用这个关联时才执行查询。例如, Employee对象有一个与之相关联的 Address对象。这两个对象之间的关联是惰性的,所以当装载 Employee对象时,Hibernate 并不自动装载 Address。相反,当装载 Employee对象时,Hibernate 会创建 Address的一个代理实例。当首次访问这个代理实例时,代理要求 Hibernate 会话查询 Address对象,以后的所有调用都直接交给查询到的实例。如果在 Employee对象的生命周期内没有使用 Address对象,就不需要执行 Address查询。这个特性确保 Hibernate 只在需要时装载数据。 在大多数情况下,应该使用惰性抓取作为默认的关联抓取策略。真实的对象模型往往包含大量复杂的对象关联;如果不使用惰性抓取,那么在装载单一对象时,很容易导致将大量数据装载到会话中。启用惰性抓取很简单,但是根据关联类型的不同,有细微的差异。基于集合的关联( OneToMany和 ManyToMany)在默认情况下设置为惰性的,所以不需要进行配置。单一对象关联( OneToOne和 ManyToOne)在默认情况下不是惰性的。为了对这些关联使用惰性抓取,需要在关联的注解中指定惰性抓取:
尽管惰性抓取应该作为大多数应用程序的主要抓取策略,但是它增加了复杂性。最严重的问题是 LazyInitializationException。惰性的关联可能在最初装载它的父对象之后很长时间才被访问(并获取相关联的实体)。但是,为了查询关联的数据,需要 Hibernate 会话可用并与数据库连接池中的一个连接相连。如果在获取对象之后关闭 Hibernate 会话和相关联的数据库连接,在访问关联之前没有恢复,那么就会发生这种错误。如果对惰性关联执行查询,但是没有相关联的数据库连接,Hibernate 就会抛出 LazyInitializationException异常。有许多处理这个问题的策略,但是这个主题超出了本文的范围(相关讨论参见 参考资料 中的 “Open Sessions in View” 链接)。 要考虑的另一个问题是,在访问惰性关联时要执行多少个查询。当第一次访问一个惰性关联的实例时,执行一个查询。这对于单一对象实例不是什么大问题,但是循环遍历对象列表很容易导致执行大量查询。例如,假设一个示例程序要输出与 10 个职员相关联的地址。如果使用惰性抓取,在默认情况下将执行 11 个查询 — 执行一个查询获取职员的列表,然后为获取每个职员的地址各执行一次查询。这显然会导致严重的性能问题。这个问题很常见,Hibernate 把这个问题称为 n+1 选择问题。(Martin Fowler 把它称为 波动装载(ripple loading)。) 即时抓取 即时抓取正好与惰性抓取相反。当装载一个对象时,会立即装载标为即时抓取的所有关联。当装载单一对象时,运行的查询并不比使用惰性关联时少。但是,因为查询会立即运行,所以不会出现 LazyInitializationException异常。即时抓取还有助于解决 n+1 选择问题。如果 Employee对象的 Address关联是即时的,并执行查询获取 10 个 Employee实例时,只执行两个查询:一个查询获取 Employee实例的列表,一个查询获取与 Employee对象相关联的 Address实例列表。Hibernate 会无缝地把这些列表合并在一起。 即时抓取可能不适合作为默认策略。如果过分频繁地使用这种策略,很容易导致装载大量不需要的数据。但是,有时候总是需要访问某个关联,在这种情况下使用即时抓取是有意义的。启用即时抓取的过程与启用惰性抓取基本相同。只需在注解中指定即时抓取:
Hibernate 为解决 惰性抓取的问题 提供了一个优雅的解决方案,同时保持惰性抓取的优点。Hibernate 允许查询的定义覆盖关联的默认抓取策略。可以在对对象树进行一般访问期间使用惰性抓取,但是在特殊情况下(在查询时已经知道需要使用关联),可以通过设置查询参数把关联标为即时的。例如,在职员示例程序中,可能需要查询在一段时期内工作了特定小时数的所有职员,以便生成邮件标签。清单 8 中的查询可以完成这个任务: 清单 8. 覆盖默认的抓取策略
setFetchMode的调用。这个调用覆盖职员地址关联默认的惰性抓取策略。 FetchMode.JOIN在搜索查询期间获取 Address实例,使用联结在一个查询中返回所有数据。在许多情况下,这就是我们需要的,它的效果比任何其他策略都要好。 但是,在使用 FetchMode.JOIN时要注意一些问题。在使用基于集合的关联时,查询返回的行数可能与默认抓取策略返回的行数不同。这是因为返回的一个父对象可能有多个子对象会返回。Hibernate 在运行时无缝地隐藏这些细节,它会正确地解析结果集并返回正确的对象列表。但是,如果查询中还使用了 setFirstResult或 setMaxResults,这种行为就会导致问题。在一般情况下,Hibernate 使用与数据库相关的 SQL 语句实现这些特性,但是因为原始的 SQL 查询返回不正确的行数,与数据库相关的技术无法发挥作用。相反,会从数据库获取完整的数据集,然后 Hibernate 提取满足请求所需的数据部分。这样的话,原本用来纠正 n+1 选择问题 的一个简单的性能调整却导致把大量不使用的行装载到应用程序层中。 Hibernate 还提供了第二种可以在查询中使用的抓取模式设置。 FetchMode.SELECT覆盖即时抓取策略,让关联使用惰性抓取。但是,无法在查询时覆盖关联来使用默认的即时抓取技术(第二个查询立即执行)。 批量惰性装载 n+1 选择问题 的另一个解决方案是,通过批量处理惰性装载请求,混合使用惰性抓取和即时抓取。这种方式仍然以惰性方式装载数据,但并不是每次惰性装载一个关联,而是装载多个关联。许多 ORM 框架实现了这种基本思想。Hibernate 把这个设置称为 BatchSize,TopLink 把它称为批量读取(batch reading)。 对于前面的示例,假设一个查询获取 10 个 Employee实例。与 Employee相关联的 Address实例是惰性装载的,每批装载 5 个。最初,使用一个查询获取 Employee实例,但是不执行针对 Address实例的查询。当访问 Address实例之一时,Hibernate 获取这个 Address实例和后面 4 个惰性装载的实例。假设要访问与 Employee实例相关联的所有 Address实例,那么只需要用两个查询获取所有 Address实例,而不是 10 个查询。 为了使用这种策略,需要在 Address类中使用 @BatchSize注解,见清单 9: 清单 9. @BatchSize注解
Address加上注解,而不是 Employee类。加上这个注解之后,每次自动装载 5 个 Address实例(如果可惰性装载的记录不足 5 个,那么装载的实例数量会更少)。还可以给集合加上 @BatchSize注解,从而批量装载实体的集合。
本系列讨论了许多解决常见的持久化层问题的方法。尽管解决方案很简单(把主键重构在基类中以及修改模型的抓取策略),但是能够显著改进 Hibernate 和领域模型的效果,产生更容易维护的应用程序。通过 Hibernate 等框架把继承和多态性等面向对象概念应用于数据库,就能够产生表现力和可重用性更强的领域模型。我们希望这里介绍的最佳实践能够适用于您的环境和领域。
学习 您可以参阅本文在 developerWorks 全球站点上的 英文原文 。 “不要重复 DAO!”(Per Mellqvist,developerWorks,2006 年 5 月):用 Hibernate 和 Spring AOP 构建类型安全的泛型 DAO。 “Generic Data Access Objects”(Hibernate Community Wiki):在不使用 Spring 的情况下创建泛型 DAO。 Hibernate Reference Documentation:学习关于 Hibernate 的全面信息。 Hibernate Annotations Reference Guide:学习关于 Hibernate 注解的更多信息。 “Open Session in View”:处理 LazyInitializationException的策略。 在 技术书店 浏览关于这些主题和其他技术主题的图书。 developerWorks Java 技术专区:数百篇关于 Java 编程各个方面的文章。 获得产品和技术 Hibernate:下载 Hibernate。 讨论 参与 developerWorks blog 并加入 developerWorks 社区。 作者简介
|
相关文章推荐
- 持久化模式,第 1 部分:现代 ORM 工具的策略和最佳实践
- 持久化模式,第 1 部分: 现代 ORM 工具的策略和最佳实践,使用 Hibernate 特性在领域模型上应用面向对象原则
- 现代 ORM 工具的策略和最佳实践
- 现代 ORM 工具的策略和最佳实践
- DB2 最佳实践: 性能调优和问题诊断最佳实践,第 2 部分
- 提高代码重用和改进性能
- 使用 JET 在 Eclipse 中创建更多更好的代码,如何掌握专家的最佳实践并提高您的模型驱动开发进度
- Perf -- Linux下的系统性能调优工具,第 2 部分
- MSDN:Windows SharePoint Services 3.0 中使用代码的开发工具和技术(第 2 部分)
- AWS Lambda最佳实践之利用容器重用来提高函数性能
- 提高 Web 站点性能的最佳实践
- Linux 下的一个全新的性能测量和调式诊断工具 Systemtap, 第 2 部分: DTrace
- 提高网站性能最佳实践(转)
- 《DB2 最佳实践: 性能调优和问题诊断最佳实践,第 1 部分》阅读笔记
- Yahoo提高网站性能34条最佳实践二
- Android最佳实践性能(三)提高性能布局(再利用布局与<include/>')
- Android最佳实践性能(三)提高性能布局(按需加载视图)
- Linux 下的一个全新的性能测量和调式诊断工具 Systemtap, 第 2 部分: DTrace
- .NET性能分析最佳实践之:如何找出使用过多内存的.NET代码(进阶篇)
- localforage 对不同浏览器 使用不同的缓存策略 , 大大提高了性能 ,IndexedDB,WebSQL 和 localStorage 三种存储模式