内容


持久化模式,第 1 部分

现代 ORM 工具的策略和最佳实践

使用 Hibernate 特性在领域模型上应用面向对象原则

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 持久化模式,第 1 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:持久化模式,第 1 部分

敬请期待该系列的后续内容。

简介

在过去 5 到 10 年中,开发人员对企业应用程序中的实体进行持久化的方式发生了根本性变化。早期的企业应用程序使用数据库表和表之间的外键关系进行实体建模。应用程序被看作查看和查询数据库底层模型的方式。近几年,数据库中的实体建模逐渐向应用程序对象模型中的实体建模转变。现在大家已经意识到,数据库仅仅是存储对象结构所定义的持久化信息的一种机制。把建模从数据库转移到对象模型中有许多优点,包括:

  • 持久化实体与对它们执行的操作更紧密地集成
  • 有助于创建松散耦合的应用程序组件
  • 与关系数据库相比,面向对象模型支持更丰富的关系
  • 更加独立于特定的数据库平台

发生这种转变的主要原因是出现了功能强大的对象 - 关系映射(ORM)系统,它们支持按照与目标语言的习惯用法一致的方式访问持久化对象。Hibernate 和 TopLink 等工具大大简化了把对象模型映射到关系数据库模式的过程。

自从这些工具出现以来,使用它们的方法也有所变化。最初,许多开发人员按照使用数据库表的方式使用 ORM 工具。实体一对一地映射到数据库表。对应于主键等字段的变量在各个实体中重复出现。因为数据库不支持与实体相关联的行为,领域模型最终只具有简单的变量以及相关的 getter 和 setter 方法。这些实体的行为最终由服务或视图层实现。

在许多项目中使用 ORM 工具的经验揭示了处理这些问题的更好方法。业务领域各不相同,所以它们的领域模型和持久化方式也可能不同,但也有相同之处。本文讨论应用于不同行业的许多领域模型的最佳实践。这里提供的最佳实践有助于产生更加一致、可重用且可维护的领域模型。我们使用 Hibernate 演示这些最佳实践,但是许多概念可以应用于其他 ORM 工具。

本文分为两部分。第 1 部分讨论以下方面的一些基本概念:

  • 实现领域中的通用功能
  • 减少数据访问层中的代码重复
  • 按照一致的方式处理对实体修改的审计

第 2 部分更深入地讨论这里介绍的一些概念,还要讨论领域模型中的性能调优。

从基础开始:对象模型

定义一个支持持久化对象的对象模型的过程与定义任何对象模型相同。首先,寻找所有对象共享的通用元素。持久化信息中有两个通用元素:惟一地标识持久化对象的方法(应该能够跨应用程序的各次执行标识对象),以及关于对象实例的审计信息。图 1 说明如何用接口和基类定义这两个概念:

图 1. 通用的接口和基类
通用的接口和基类
通用的接口和基类

图 1 引入了 IdentifiableAuditable接口,这些接口定义的 API 用来标识对象实例和设置对象实例的审计信息。还引入了 BaseEntityAuditableEntity基类,可以根据是否需要对象的审计信息,分别从这些基类派生出具体的持久化类。

通过用这些接口定义持久化对象,就可能创建出可以应用于所有具体对象类型的抽象行为。这包括 UI 层(用来标识要执行创建、读取、更新和删除(CRUD)操作的对象)以及服务和数据层。本文的代码示例(在 下载中可以获得完整的代码包)演示如何使用这些接口帮助执行审计和减少数据访问对象(DAO)中的代码重复。

通用的基实体

与对象不同,数据库表没有继承的概念。许多表中都有的字段(比如审计字段)必须为每个表重新定义。请牢记,可以在 Java™代码中使用继承,以避免这种重复出现在代码中。尽管 ORM 工具早就支持这个特性,但是 Java Persistence API 注解使之大大简化了,可以进一步减少代码重复(参见 参考资料)。

通过使用 Java 5 注解,可以用类级注解在类源代码中直接嵌入数据库映射。Java Persistence API 为此定义了一套标准注解。Hibernate 和其他工具现在支持这些注解。可以通过 @MappedSuperclass注解使用在基类中定义的映射。只要所有数据库表对通用字段采用相同的列类型和列名,那么只需在基类中编写映射一次,就可以在所有子类中重用。清单 1 是 BaseEntity类的一个示例:

清单 1. 用 @MappedSuperclass定义 BaseEntity
@MappedSuperclass 
 public class BaseEntity implements Identifiable{ 
    private Long id; 

    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO) 
    public Long getId() { 
        return id; 
    } 
    public void setId(Long id) { 
        this.id = id; 
    } 
 }

清单 1 中的映射把 ID 字段映射到默认列名(id),并指定自动生成 ID(实现方式与数据库相关)。

注意,即使每个表使用不同的列名,仍然可能使用这些通用基字段。请考虑一种典型情况:所有数据库表的主键都是 Long类型的,但是列名可能不一样。通过重新定义分配给特定子类的一个属性的列,仍然可以重用与 id属性相关的代码。清单 2 演示如何重新定义与 id属性相关联的列:

清单 2. 重新定义与 id属性相关联的列
@Entity 
 @AttributeOverride( name="id", column = @Column(name="EMPLOYEE_ID") ) 
 public class Employee extends BaseEntity {//...}

如果不使用 Hibernate 注解,也可以重用这些基类中的代码。但是,必须映射每个具体类的字段。Hibernate 会在 Java 代码中自动使用继承的字段。

数据访问核心

顾名思义,DAO 模式封装了访问一个对象或相关对象集中的数据的逻辑。Hibernate 中的 DAO 包含 Criteria查询和 Hibernate Query Language 查询,还有一个 Hibernate SessionFactory。所有面向数据库的逻辑都应该包含在 DAO 中,这意味着应该通过 DAO 存储和读取普通 Java 对象(POJO)和其他基本数据类型值。DAO 是企业 Java 分层体系结构中一种很典型的模式,通常通过一个服务访问 DAO。仔细研究 DAO 就会发现,它们的操作往往很相似。

为了了解 DAO 之间的共性,我们来看一些示例。清单 3 给出的两个方法根据 图 1中定义的 Identifiable接口中的 ID 查询 AddressEmployee

清单 3. 数据访问对象中的典型方法
 public Address findById(Long id){ 
    return (Address) getSession().get(Address.class, id); 
 } 

 public Employee findById(Long id){ 
    return (Employee) getSession().get(Employee.class, id); 
 }

EmployeeAddress上的这些操作之间,主要的差异仅仅是操作中使用的类。查询是相同的,只是把结果转换为不同的类。其他操作(比如删除模型中的给定实体或者从模型中获取某一实体的所有实例)在 DAO 中是相似的,在不同实体之间也是相似的。因此,可以利用 Java 1.5 的泛型功能创建一个可重用的 DAO,从而构成数据访问层的核心。

泛型 DAO

泛型 DAO 模式(也称为类型安全的 DAO)对于减少数据访问层中的代码重复非常重要。如果使用 Java 1.4,也可以因通用基 DAO 而受益;这种实现不是类型安全的,也不是很简洁,但是仍然能够减少代码重复。

有几种实现泛型 DAO 的方式,其中一些方式取决于您的环境。这个示例使用依赖项注入风格,因此实现不需要为如何配置自身操心,而是假设在使用它之前它所需要的所有东西都已经注入了。其他一些方式不使用依赖项注入(更多信息参见 参考资料中 Hibernate wiki 的链接)。依赖项注入方式的关键是,在查询数据时,要注入 DAO 将查询的实体的 Class并定义泛型类型。

创建泛型 DAO 的第一步是定义它的一些通用操作。图 2 给出一个泛型 DAO 的接口:

图 2. 基 DAO 接口和实现
基 DAO 接口和实现
基 DAO 接口和实现

清单 4 给出这个泛型 DAO 的一些示例代码:

清单 4. 泛型 DAO 模式的示例代码
public interface BaseDao<B extends BaseEntity> { 
    B getById(Long id); 
    // ...other methods 
 } 
   
 public class BaseDaoHibernate<B extends BaseEntity> 
        implements BaseDao<B extends BaseEntity> { 
        
    private Class<B> queryClass; 
    
    public B getById(Long id) { 
        return (B) getSession().get(getQueryClass(), id); 
    } 
    // ...other methods 
 }

清单 4 中的方法构成了数据访问层的核心。可以直接使用这个泛型 DAO,也可以根据查询的实体的需要,从它派生出子类。可以通过一个服务直接使用这个泛型 DAO,比如清单 5 中的服务:

清单 5. 通过服务直接使用泛型 DAO
 BaseDao<Employee> dao = new BaseDao<Employee>(); 
 dao.setQueryClass(Employee.class); 
 dao.setSessionFactory(sessionFactory); 
 ... 
 dao.getById(-1L);

如果查询需要更复杂的数据集,那么可以从泛型 DAO 派生出子类。例如,假设希望找到在 Iowa 生活的所有全职职员。为此,需要定义一个与 Employee相关的 DAO 方法,findIowaEmployees。如果创建一个扩展 BaseDAO的新的 EmployeeDAOEmployeeDAO可以执行特定的全职职员查询,还可以执行泛型 DAO 提供的所有基本查询,见清单 6:

清单 6. 从泛型 DAO 派生出子类
 public class EmployeeDao extends BaseDaoImpl<Employee> { 
    public EmployeeDao() { 
        setQueryClass(Employee.class); 
    } 
    
    List<Employee> findIowaEmployees() { 
        Criteria crit = getCurrentSession().createCriteria(getQueryClass()); 
        crit.createCriteria("address").add(Restrictions.eq("state", "IA")); 
        return crit.list(); 
    } 
 }

注意,清单 6 使用 createCriteria()方法。它是另一个在企业应用程序的 DAO 中常常重复出现的方法。在使用泛型 DAO 的过程中,您会发现新的通用操作,可以把它们添加到泛型 DAO 中,从而增强可重用性并减少重复。第 2 部分将详细描述其他一些通用方法,比如启用真正的分页和处理搜索参数。

审计

基本审计是以数据库为中心的应用程序中的常见特性之一。大多数应用程序都需要记录审计信息,比如何时创建了对象、谁创建了对象、何时修改了对象以及谁修改了对象。这些特性的建模并不困难。要求审计的任何对象只需要增加 4 个字段,每个字段存储一种审计信息。图 1为包含审计字段的持久化实体提供了一个基类。这个特性比较难的部分是,决定在代码中的什么地方设置审计信息。有几个选择:

  • 生硬的方式:在需要修改审计信息的对象的任何地方,都确保用正确的值填充审计信息。这种方式有许多明显的缺陷。最主要的缺陷是,审计逻辑会在应用程序中的许多地方重复出现。即使把这一逻辑集中在一个实用程序类中,仍然必须记住在修改对象的每个地方使用这个实用程序 —不只是在最初开发系统时,在维护系统时也必须牢记这一点。但是,在任何规模的系统中,开发人员或早或晚都会忘记这项要求。
  • 把审计逻辑放在 DAO 中:另一种方式是把审计逻辑集中在一个通用 DAO 中。通过在保存方法中添加审计逻辑,使用这个 DAO 保存的任何对象都会自动填充审计字段。这种方式在许多情况下效果很好,但是仍然有一些缺陷。一个缺陷是,这种方式假设应用程序总是使用通用 DAO 的保存方法来保存数据。但是,实际情况不一定总是这样的,那么前面的问题就又出现了:必须记住添加审计逻辑。另一个问题更严重:这个解决方案忽略了 ORM 工具最有用的特性之一,过渡持久化(transitive persistence)。可以使用 DAO 显式地保存 Employee对象,但是 Hibernate 也会自动地对与它相关联的 Address的任何修改进行持久化。在这种情况下,Employee会填充它的审计字段,但是 Address不会。
  • Hibernate 的 Interceptor:为了解决这个问题,需要在 Hibernate 中建立一个扩展点。每当框架保存对象时,需要在一个地方填充这些审计字段。Hibernate 通过它的 Interceptor接口提供了这个特性。这个接口为许多 Hibernate 事件提供回调方法,包括创建、修改和删除对象。把审计逻辑放在 Hibernate 的 Interceptor中,就可以消除重复的逻辑,而且不再需要为确保执行逻辑操心。只要由 Hibernate 负责保存数据,就一定会执行审计逻辑。

实现的细节

Hibernate 有一个 EmptyInterceptor类,它为 Interceptor接口中的十几个回调方法提供了空的实现。通过这个类添加审计信息是非常好的方式。在清单 7 中的实现中,只有两个方法与审计相关:onSave(当把新对象刷新到数据库时调用这个方法)和 onFlushDirty(当 Hibernate 把更新过的(脏)对象刷新到数据库时调用这个方法):

清单 7. 扩展 EmptyInterceptor
public class AuditInterceptor extends EmptyInterceptor {    

    @Override 
    public boolean onSave(Object entity, Serializable id, Object[] currentState, 
            String[] propertyNames, Type[] types) { 
        ... 
    } 
                
    @Override 
    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, 
                Object[] previousState, String[] propertyNames, Type[] types) { 
        ... 
    } 
 }

这两个事件都在 Hibernate 已经决定要保存对象的哪些属性以及那些属性的值之后调用。传递给这些方法的参数包括属性名的 String数组和属性值的 Object数组。因为 Hibernate 已经决定了要保存的值,所以直接更新对象不会取得想要的效果。实际上,更新对象不会对最终发送到数据库的值产生影响。所以,实际上需要更新属性值数组中的元素。尽管这种更新数组的方式有点儿笨拙,但是实现仍然相当简单明了。只需循环遍历属性名数组,寻找审计字段。找到这些字段之后,用对应的索引更新值数组。还有最后一点细节需要注意:这些回调方法返回一个布尔值。如果修改了对象的状态,方法就需要返回 true。如果没有进行修改,就应该返回 false。清单 8 中的示例代码显示这一逻辑:

清单 8. 更新审计字段的 Interceptor回调方法
for(int i = 0; i < propertyNames.length; i++) { 
    if(propertyNames[i].equals("createdOn")) { 
        currentState[i] = new Date(); 
        updated = true; 
    } 

    if(propertyNames[i].equals("createdBy")) { 
        currentState[i] = username; 
        updated = true; 
    } 
 }

最后一步是帮助强制使用统一的审计字段名称,并确保数据库实体具有这些字段。Auditable接口是实现这个目标的最容易、最好的方法,但是它看上去有点儿奇怪。尽管为这些字段提供了 getter 和 setter 方法,但是审计代码中实际上不使用这些方法。但是,通过让实体实现 Auditable接口,可以显著减少在开发持久化类时需要的代码量。

第 1 部分结束语

本文主要关注使用 Hibernate 特性在领域模型上应用基本的面向对象原则。与所有模式和最佳实践一样,您应该根据自己的环境评估这里描述的解决方案,并相应地进行调整。泛型 DAO 能够提供很强的灵活性。不同的数据库结构、技术和业务需求具有不同的通用功能,可以把这些功能转移到泛型 DAO 的通用代码中。不同的应用程序可能需要不同的审计信息(ATM 机领域实体事务的审计信息显然与职员地址的审计信息不一样)。无论处于什么上下文,最佳实践是相同的。

本系列的 第 2 部分进一步讨论构造数据模型方面的最佳实践,包括利用 Hibernate 的多态性、泛型 DAO 的其他有用特性以及数据模型的性能调优。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=308723
ArticleTitle=持久化模式,第 1 部分: 现代 ORM 工具的策略和最佳实践
publish-date=05222008