开发人员:Java Java 持久性 API 的基本最佳实践 考虑并采用这些简单的方法,使 Java 持久性 API 应用程序更易于维护。 作者:Dustin Marx 2008 年 5 月发表 Java 持久性 API (JPA) 是 Enterprise JavaBeans (EJB) 3.0 规范的一部分,它现在是实现 Java EE 平台的持久性和对象/关系映射的标准 API,并为 Java 开发人员绑定数据提供了多项优势。首先,它为实现标准 Java 和企业 Java 领域的持久性提供了一个公共 API。其次,JPA 提供了一个可用于多个不同数据存储的标准 API。最后,JPA 还提供了可采用相同方式应用于这些不同上下文环境(标准 Java、企业 Java 和不同数据库)的对象关系映射技术。 本文将简要介绍如何充分利用 Java 持久性 API 的这些优势。 这些建议背后的共同主题 本文列出的大部分建议方法都渗透着以下几个共同的主题: - 此处列出的方法重点关注基于 JPA 的应用程序代码的可读性和可维护性。
- JPA 提供程序实现之间的可移植性通常是期望的理想目标。即使不能维护实现的可移植性,但特定于提供程序的功能应易于识别,并易于与公用功能隔离开来。
- JPA 代码应能跨多个数据源进行移植。必须使用特定于数据库的特性时,应将其隔离到配置文件中,以免损坏代码。
- 基于 JPA 的代码应总能在 Java SE 和 Java EE 上下文环境中运行。
- 可维护的、易于理解的代码的最重要特性就是代码本身的质量。本身一目了然的简洁代码要比使用批注更可取。具有自我说明性的代码和批注要优于解释复杂代码所需的注释。
- 本文探讨的这些实践的实施通常只需额外付出很少的精力,且在开发初期花费很少的成本或无需成本即可提供长期的维护方面的优势。
记住以上列出的共同主题之后,我们将介绍基于 JPA 的高效应用程序的一些建议实践。 支持使用惯例而非特例 在理想情况下,默认的配置设置始终是我们所需的设置。我们使用“按异常进行配置”时,不需要配置任何特例。通过尽量减少与设定配置出现偏差的频率和严重程度,我们可以接近这种理想状况。尽管为默认的配置设置提供特例并无不妥,但这种做法需要我们投入更多的精力来指定和维护用于描述默认配置的特例的元数据。 仔细考虑和选择元数据配置策略 对许多组织而言,最为可行的做法是在开发期间在代码中使用批注,因为代码中配置对开发人员来说更为方便。对于一些这样的组织来说,在部署和生产期间使用外部 XML 文件可能更可取,尤其是在部署小组与开发小组不同时。 JPA 支持将基于 XML 的配置数据用作批注的替代品,但使用 XML 配置方法改写批注的功能更强大。通过使用改写进程,开发人员能够在开发源代码期间使用批注,同时还能在生产时在代码外部改写这些代码内批注。 正如我在 OTN 文章“Java SE 6 — 更好的 JPA、更好的 JAXB 和更好的批注处理”中详细论述的一样,Java SE 6 提供的内置批注处理功能可用于从代码内批注构建映射 XML 文件。此方法适合于开发人员从代码内批注获益,而部署人员从外部配置获益的组织。 对于可能随软件的不同部署而发生变化的配置,外部配置是最有意义的做法。对于在多个部署中相对静态的配置,代码内批注可以只定义一次,而无需经常更改。 有些配置设置必须使用 XML 配置文件来表示,而不能通过代码内批注来表示。此类配置的一个示例是涵盖持续性单元中的所有实体的默认实体监听程序定义。 特定供应商设置是应使用外部配置而非代码内批注的另一种情况。将特定实现设置放在外部配置文件中,可保持代码可移植且整洁。一般来说,应使用名称/值属性在 persistence.xml 文件而不是源代码中声明 JPA 特定供应商的属性。 某个特定数据库专用的 SQL 语句也可以放在源代码外部,并放入 XML 描述符文件中。如果必须使用数据库专用的 SQL 语句,最好将其指定为本地命名查询,并在通用持续性单元的 XML 文件而非特定实体的 Java 源代码文件中对其添加批注。 JPA 1.0 规范的联合制定人 Mike Keith 在 OTN 专栏“是否添加批注”(参见“其他资源”)中介绍了与 XML 元数据策略(XML 策略)和数据源内元数据策略(批注策略)相关的许多折衷办法。 访问字段而不是属性 我更倾向于通过直接对实体字段添加批注,而不是对 get/set 方法(属性)添加批注来指定对象关系映射,这出于多方面的原因。没有一个原因完全偏向于通过字段而非属性来指定持续性,但字段持续性规范的组合优势使其成为一种更具吸引力的方法。 由于持续性的意义在于数据本身的存储、更新和检索,因此直接在数据上而非间接通过获取和设置方法来指定持续性似乎更整洁清晰。此外,也无需记得为持续性标记 getter(而不是 setter)。同样,将字段标记为临时性的以指示其不应持久保存,要比将 get() 方法标记为临时性的更加清晰。通过使用字段而非属性,您不必保证 get 和 set 方法必须遵循与基础字段相关的 JavaBean 命名惯例。我个人更偏爱在同一个位置查看类的数据成员,并且确定每个成员的名称、每个成员的数据类型、与每个数据成员相关的注释以及每个成员的持续性信息的能力。 业务逻辑和持续性的顺序在 get/set 方法中不能保证。开发人员可以从这两种方法中排除业务逻辑,但如果已对字段添加了批注,则以后将业务逻辑添加至 get 方法还是 set 方法都无所谓。 开发人员可能希望拥有一次可操作或检索多个属性的方法,或者名称中不包含“get”或“set”的方法。利用字段批注,开发人员能够按照需要自由编写和命名这些方法,而不必将 @Transient 批注或“transient”关键字放在与持续性不直接关联的方法前面。 支持使用 @EmbeddedId 指定组合键 我个人偏爱使用 @EmbeddedId 指定组合键,主要有以下三方面原因: 1. 使用 @EmbeddedId 与在非主键的嵌入式 Java 类上使用 @Embedded 批注是一致的。 2. @EmbeddedId 使我能在实体中将组合键表示为单键,而不必使用 @Id 批注在实体中为多个数据成员添加批注。 3. @EmbeddedId 方法提供了在单个 Java 类中封装主关键字列上的任何 @Column 或其他列映射的功能。这要优于强制包含实体为组合键中的每个列处理对象关系映射的详细信息。 简而言之,由于可以在单个 @Embeddable 类中将与主键相关的详细信息进行分组,我个人更偏爱使用 @EmbeddedId 方法指定组合主键。这还简化了对作为单个内聚单元(而不是作为实体内的单个片段)的主键类的访问。 理想的方法是使用单值键,因为这种方法通常花费 JPA 开发人员的精力最少。 仅将精确数据类型用于主键 JPA 规范警告不要使用近似类型,尤其是浮点类型(浮点和双精度)。一般说来,对于 JPA,我的首选是在任何可能的情况下使用整数替代主键。 尽量使用标准 JPA 代码 以下维护可移植 JPA 代码的通用指导原则基于 JPA 规范中有关可选或未定义特性的警告。 通常通过引用将表与主键关联 JPA 规范允许实现一个表中的列对另一个表中的非主键列的引用,但 JPA 实现并不要求支持此特性。因此,对于可以跨不同 JPA 实现移植的应用程序来说,最好通过从一个表到另一个表的主键列的引用来关联表。不管怎样,我更倾向于将该方法作为通用的数据库原则。 使用可移植的继承映射策略 即使 JPA 提供程序可实现可选的“每个具体类一个表”继承映射策略,但如果您需要 JPA 提供程序的可移植性,则最好避免使用该策略。 此外,由于不要求 JPA 实现支持在单个类层次结构中混合多个映射继承策略,因此最好在一个给定 Java 实体类层次结构内使用一个继承映射策略。 其他 JPA 实现可移植性问题 除了此处讨论的内容外,JPA 规范还指出了开发基于 JPA 的可移植应用程序时应记住的其他问题。一般而言,除非绝对必要,否则应避免规范中引为可选、未定义或不明确的任何信息,或者特别是在 JPA 实现之间调用为不可移植的任何信息。在许多这些情况下,都不难避免可移植性的例外情况。(有关可移植 JPA 应用程序的一个极好的参考文章是 2007 JavaOne 会议上的演示文稿“Java 持久性 API:可移植性须知”。有关可移植 JPA 应用程序的另一篇极好的参考文章是“使用 EJB 3.0 Java 持久性 API 实现可移植的可持续性”。这两篇文章都在“其他资源”下列出。) 有效利用特定于实现的特性 有时,JPA 实现可能提供极有用的非标准特性(“扩展”)。 尽管通常希望应用程序尽可能基于标准,以提高在各个不同的标准实现之间移植这些应用程序的能力,但这并不意味着始终不应使用特定于实现的特性。相反,应将使用基于所有标准的方法的成本和优点与使用供应商特定特性的成本和优点进行比较。 在决定是否使用特定于某个特定 JPA 实现的特性和扩展时,应考虑以下问题。 - JPA 规范是否列出了允许使用供应商选项而不会损失可移植性的扩展点?
- 特定于实现的特性是否提供了优于使用更为标准的方法的显著优势(更优的性能、更轻松的开发或其他功能)?
- 此应用程序需在未来使用其他 JPA 实现的可能性如何?要考虑的因素包括实现供应商的支持和未来生存能力、许可费用以及使用产品的一般经验。
这里提供了一个通过回答上述问题作出折衷决策的示例:在基于 JPA 的应用程序中使用 Oracle TopLink Essentials 的日志机制。我比较喜欢使用这个特定于提供程序的特性,原因包括以下几点: - 通过将 Oracle TopLink Essentials 的日志级别设置为 FINE,可将 SQL 语句记入日志,这一点对调试非常有价值。
- Oracle TopLink Essentials 是 JPA 参考实现。
- JPA 提供了属性的扩展机制,如日志记录,该机制可以在外部 persistence.xml 文件而非源代码中保存特定于提供程序的设置。此机制还可保证其他提供程序简单地忽略特定于 Oracle TopLink 的属性。
另一个非常有用的特定于提供程序的功能示例是使用二级缓存,这对实现可接受的性能通常至关重要。有关参考实现的扩展的信息,可在“TopLink Essentials JPA 扩展参考”中获得(参见“其他资源”)。 尽量使用标准 SQL 和数据库 使用特定于某个特定数据库的特性比使用特定于某个特定 JPA 提供程序的风险更大,因为通常数据库细节的处理不像 JPA 提供程序细节的处理那样有效。 使用标准 JPA 查询语言语句维护数据库的独立性。代码中特定于数据库的 SQL 语句的红色标志是使用 @NamedNativeQuery 和 @NamedNativeQueries 批注(或其对应的 XML 描述符元素)。同样,EntityManager.createNativeQuery 方法调用也可以指明对特定数据库的依赖性。 有效利用特定于数据库的特性 即使使用特定于供应商的特性是有保证的,您仍可采取措施来清楚地确定并隔离这些供应商细节,以免于标准化的数据库访问。 我更倾向于将特定于数据库的查询放在 XML 部署描述符(一个或多个 named-native-query 元素)中,以尝试将实际代码(包括批注)与特定于供应商的代码尽可能分离。这样,便可将专用数据库代码隔离在外部描述符中,而不是将其与以标准为导向的代码混合。我还喜欢将 named-native-query XML 元素包括为对象关系映射文件的根元素的子元素,而非任何特定实体的子元素。 本地命名查询的范围为整个持续性单元,即使该本地命名查询是在某个特定的实体 Java 类中定义的;因此,将一些其他的唯一标识符包括在本地命名查询的名称中仍被视为一项最佳实践。如果将本地命名查询一起放在外部 XML 映射文件中的根元素下,则更容易直观地发现命名冲突。此方法的缺点是不易观察查询返回了哪个实体的信息,但通过在本地命名查询中包括返回实体的名称可以解决此问题。 为 Java SE 和 Java EE 环境设计 JPA 代码 最终拥有一致的可与标准 Java 和企业 Java 配合使用的 API 的一个好处在于,我们可以编写可与标准应用程序和企业应用程序配合使用的基于 Java 的可持续层。 有效的分层可用于确保基于 JPA 的数据库访问 (DAO) 代码可用于标准 Java 和企业 Java 上下文环境中。要正确完成此工作,实体类和 DAO 层不应执行事务处理,因为这会与企业 Java 应用服务器提供的事务处理发生冲突。需要将事务处理从实体类中向外推送至标准 Java 环境中的客户端。 下图显示了如何将同一实体类用于标准 Java 和企业 Java 环境。通过这种方式构建实体只需花费很少的精力,但却允许高度重用实体类。  将实体管理器访问移回至特定于其托管环境的层,将事务处理移回至特定于该托管环境的同一层,将使 JPA 实体类和数据库访问层能够在标准 Java 环境、Java EE Web 窗口和 Java EE EJB 应用服务器中重用。 尽管上图显示了 Web 层中的 JPA 事务处理和实体管理器处理,但我通常更喜欢将此功能保留在可从 Web 层访问的无状态会话 bean 中。此图所示的 Web 层中的 JPA 仅用于说明如何将事务处理和实体管理器处理与通常的 JPA 和 DAO 代码区分开来。 需要注意的是,不同类型的实体管理器(应用程序管理和容器管理)不应同时或互换使用。此外,单个实体管理器也不能跨并发事务处理使用。 在适当的层中使用 JPA 代码 由于 Java 持久性 API 专用于数据库访问,因此一般来说,不应将其用于除业务层外的应用程序层。通常,将 JPA 代码放在表示层会导致其无法由该层外的任何其他基于 Java 的应用程序使用,从而削弱了 JPA 的一项关键优势。 支持使用自描述代码而不是元数据和注释 尽管注释和批注对于描述代码需要执行或期望执行的功能非常有用,但如果代码自身就能说明一切,则效果更佳。 多年来,“transient”关键字一直是 Java 编程语言的一个内置部分,并提供了一种采用标准 Java 代码的机制来表明某个字段不应保留。我更喜欢使用此关键字来表示这项持续性例外,而不是使用 @Transient 批注或 XML 条目。不过,当实体类需要一个可串行而非可持续的字段时是个例外。在这种情况下,@Transient 批注(或 XML 等效物)是唯一适当的选择。 使用命名惯例获得更易理解的 JPA 代码 命名惯例使开发人员能够更容易地阅读、维护和改进其他开发人员的代码。使用与 JPA 有关的命名惯例可以补充与 JPA 有关的默认值的使用。 命名惯例可以为每个命名查询提供一个唯一的标签,确保给定持续性上下文环境中的所有命名查询都具有唯一的名称。完成此工作的最简便的方法是,将实体类名称用作与该实体最密切关联的每个命名查询的名称的前缀。JPA 蓝图推荐采用相同方式将命名惯例用于命名查询和基于 JPA 的代码的其他方面。 利用 J2SE SE 5(及更高版本)的特性 由于您要将 J2SE 5(或更高版本)与基于 JPA 的应用程序一起使用,因此您可以在您自己的代码中应用 J2SE 5 的特性。 通用项使 JPA 开发人员能够在实体间指定一对多和多对多的关系,而不必表述 targetEntity 属性。这样,便可使用自描述代码而非资源内批注或外部 XML 配置来完成这一相同功能。 不能将枚举用作实体,但可以将其用于持久实体的数据成员,以便为数据成员提供类型安全性和有限范围值控制。 性能是值得重视的一个方面 本文的中心是开发可维护性高的基于 JPA 的应用程序,但 JPA 规范提供了许多有用的“勾子”来优化性能,而不必损失 JPA 代码的可移植性。JPA 提供程序应忽略具有无法识别的属性名称的 persistence.xml 文件中的任何提供程序属性。查询提示会被其不适用的 JPA 提供程序忽略,但我仍倾向于将它们放在外部 XML 而非 Java 代码中。 这些 JPA 提供程序“勾子”应在需要提高性能时使用,但我更倾向于将代码与之分离,并在外部 XML 描述符文件中而不是代码中声明尽可能多的此类“勾子”。 利用最新的工具 主要的集成开发环境 (IDE) 现在都捆绑了几个与 JPA 有关的工具。Oracle JDeveloper 提供了一个向导,该向导使用直接来自指定数据库表的相应批注即可轻松创建基于 JPA 的实体类。仅需几次单击,Oracle JDeveloper 用户即可按同样的方式创建无状态会话 bean,以作为这些新建实体 bean 的外观。NetBeans 6.0 提供了类似的 JPA 向导,而且 Eclipse Dali 项目支持适用于 Eclipse IDE 的 JPA 工具。 可能会不断出现许多与 JPA 有关的非常有用的工具。 将 Spring 添加至 JPA 开发人员可以使用 Spring 编写可在标准 Java 环境、Web 容器和完整的应用服务器 EJB 容器中轻松运行的基于 JPA 的应用程序,而不必对源代码进行任何更改。这可以通过 Spring 容器的注入数据源(在代码外配置)和支持事务处理(通过面向方面的编程实现,同样在代码外配置)的能力来完成。Spring 框架使 JPA 开发人员能够将在不同环境(独立的 Java SE、Java EE Web 容器和 Java EE EJB 容器)中处理 JPA 的具体细节隔离到外部配置文件中,从而保留透明的基于 JPA 的代码。 Spring 2.0 为 JPA 开发人员提供的另一个特性是 @Repository 批注,它有助于评估潜藏在 JPA PersistenceException 下的特定于数据库的问题。 最后,Spring 框架为引用 JPA 提供程序中通用的一些 JPA 提供程序扩展提供了一个便利的机制。 文章“结合使用 Java 持久性 API 和 Spring 2.0”(参见“其他资源)提供了有关将 Spring 框架与 JPA 一起使用的更多信息。 应用 Java EE 5 和 EJB 3.0 最佳实践 通过以下有效的 EJB 实践可以实现在 Java EE 环境中最有效地使用 JPA。正确使用 EJB 3.0 特性(如相关性注入)可确保在 Java EE 环境中最有效地使用 JPA。将 EJB 最佳实践应用于 JPA 的另一个例子是使用无状态会话 bean 作为 Java EE 应用程序中 JPA 实体的外观。 学习和应用相关技术的最佳实践 Java 持久性 API 与许多其他技术密切相关,而且我们可以从这些技术的已知最佳实践中获得许多借鉴。这些相关技术包括关系数据库、SQL、对象关系映射 (ORM) 工具和 JDBC。例如,JPA 中镜像了通过 PreparedStatement 使用命名参数或位置参数的最佳实践。为了安全性和潜在的性能上的好处,JPA 支持在 JPA 查询语言中使用命名参数或位置参数。 熟悉 JPA 标准 Java 持久性 API 规范非常易于理解。它解释了哪些 JPA 特性对实现是可选的,因而也是不可移植的。此外,此规范还讨论了一些可能的趋势或未来可能的发展方向,并警告不要执行可能与预期更改或添加发生冲突的任何操作。 Mike Keith 曾指出“提取和正确解释特性在正确的上下文环境中的用法”不属于此规范的要求的一部分。因此,熟悉 JPA 标准的更有效的方法是阅读明确解释 JPA 正确和适当应用的参考文章。“其他资源”部分中包括了一些这样的参考文章。 熟悉 JPA 蓝图 Java 持久性 API 蓝图文档是有关有效使用 JPA 实践的信息的另一个极好来源。 利用 OTN 上有关 JPA 的资源 Oracle 是开发 Java 持久性 API 的主要参与者,曾共同引领过 JPA 1.0 专家组,并大量参与了 JPA 参考实现。除了提供 JPA 1.0 参考实现 (Oracle TopLink Essentials) 之外,Oracle 正在引领提供 JPA 2.0 参考实现的 EclipseLink 项目。此外,Oracle 还提供 Oracle TopLink 11g 作为 JPA 实现,它具有比参考实现更多的特性。 OTN 还提供了大量的 JPA 资源。其中,我最喜欢的两篇参考文章是“JPA 批注参考”和 OTN 技术文章“试用 JPA”。 学习其他人的经验(与 JPA 有关的网志) 在积累更多 JPA 经验的同时,会不断发现更多更复杂的 JPA 最佳实践和技巧。在线资源尤其具有及时性和相关性。虽然网志的质量良莠不齐,但面向 JPA 的较好的网志提供了在 JPA 开发方面非常有价值的提示和洞察信息。我一直发现有几篇关于 JPA 的网志对我的 JPA 工作非常有帮助。“其他资源”下列出了一些这样的文章。 结论 Java 持久性 API 为 Java SE 和 Java EE 开发人员提供了一个标准化的数据库可持续性机制。本文概述的实践将帮助他们开发基于 JPA 的代码,实现 JPA 规范中提供的优势。 其他资源 Eclipse Dali 项目 www.eclipse.org/webtools/dali/main.php EclipseLink www.eclipse.org/eclipselink/ Java 持久性 API 蓝图 https://blueprints.dev.java.net/bpcatalog/ee5/persistence/ “Java 持久性 API:最佳实践和提示”(2007 JavaOne 会议) http://developers.sun.com/learning/javaoneonline/2007/pdf/TS-4902.pdf “Java 持久性 API:可移植性须知”(2007 JavaOne 会议) http://developers.sun.com/learning/javaoneonline/2007/pdf/TS-4568.pdf “Java Persistence 2.0”(2007 JavaOne 会议) http://developers.sun.com/learning/javaoneonline/2007/pdf/TS-4945.pdf “JPA 批注参考” oracle.com/technetwork/cn/middleware/ias/index-083744-zhs.html Michael Bouschen 的网志 http://weblogs.java.net/blog/mb124283/ “使用 EJB 3.0 Java 持久性 API 实现可移植的可持续性” http://java.sys-con.com/read/286874_2.htm Pro EJB 3:Java 持久性 API (Apress, 2006, ISBN 978-1590596456) http://www.apress.com/book/view/1590596455 Sahoo 的网志 http://weblogs.java.net/blog/ss141213/archive/j2ee/index.html “TopLink Essentials JPA 扩展参考” oracle.com/technetwork/middleware/ias/toplink-jpa-extensions-094393.html “结合使用 Java 持久性 API (JPA) 和 Spring 2.0” http://java.sys-con.com/read/366275.htm Wonseok Kim 的网志 http://weblogs.java.net/blog/guruwons/archive/j2ee/index.html Dustin Marx 是 Raytheon 公司的高级软件工程师。 |