[译文]JPA的实施模式:映射继承的层次体系

原文:JPA implementation patterns: Mapping inheritance hierarchies

作者:Vincent Partington

出处:http://blog.xebia.com/2009/06/21/jpa-implementation-patterns-mapping-inheritance-hierarchies/

 

上周我在正在进行的JPA实施模式博客系列中讨论比较了域访问与属性访问之间的优缺点,本周我会详细论述在映射继承的层次体系时JPA所提供的选择。

JPA提供了三种把Java的继承层次体系映射到数据库表的方式:

1.         InheritanceType.SINGLE_TABLE—整个继承的层次体系被映射到一张表上,一个对象正好被存放成表中的一行,存放在鉴别器列中的鉴别器值指明了对象的类型,任何在超类中或者是在层次体系的不同分支中未被用到的字段都被设定为NULL,这是JPA默认使用的继承映射策略。

2.         InheritanceType.TABLE_PER_CLASS—层次体系中的每一个具体实体类被映射到一个单独的表上,一个对象正好存放成其类型对应的特定表中的一行,该特定表包含了这一具体类的所有域的列,其中包括了任何继承的域,这意味着继承层次体系中的同层级的每个类自己都会有它们继承自超类的域的副本,在对超类进行查询的时候,会执行对单独表的一个联合(UNION)语句。

3.         InheritanceType.JOINED—层次体系中的每个类都被表示成一个单独的表,不会造成域重复的现象出现,一个对象被分布存储在多个表中,占据每个表的一行,这些表组成了该对象的类继承的层次体系。子类与其超类之间的是(is-a)关系被表示成从“子表”到“超表”的外键关系,通过使用连接(JOIN)语句连接被映射的表来加载实体的所有域。

 

可在DataNucleus的文档中找到一个很好的关于JPA继承映射选项的图形化比较,其中包括了对@MappedSuperclass选项的描述。

现在要关心的问题是:在什么情况下,哪一种方法的效果最好?

 

单表(SINGLE_TABLE每个类层次体系一张表

 

单表策略的优点是简单,加载实体的时候只需要查询一张表,使用鉴别器列来确定实体的类型。在手工检查或者修改存储在数据库中的实体的时候,简单化也是有帮助的。

这一策略的缺点是当层次体系中有很多的类时,单表会变得非常大,此外,映射到层次体系中的子类的列应该是可为空的,对于大型的继承层次体系来说,这尤其令人讨厌。最后一点是,层次体系中的任何一个类的变化都需要改变单表,这使得单表策略只适用于小型的继承层次体系。

 

每个类一张表(TABLE_PER_CLASS每个具体类一张表

 

每个类一张表策略并不要求列是可设为空的,从而数据库模式(database schema)相对易于理解,因此也容易手工检查或者修改。

缺点是以多态方式加载实体需要所有被映射表的一个联合(UNION)语句,这可能会影响性能。最后一点是,对应于超类域的列的重复导致了数据库的设计是非规范化的,这使得很难针对重复的列执行聚合(SQL)查询。因此,这一策略最适合于宽但不深的继承层次体系,在这样的继承体系中,超类的域不会是你想要针对之进行查询的。

 

连接(JOINED每个类一张表

 

连接策略提供了漂亮的规范化数据库模式,不存在任何的重复列或是不必要的可空列,因此,其最适合大型的继承层次体系,无论是有深度的或者有宽度的都可以。

这一策略确实使得更难以手动地检查或者修改数据,此外,需要用来加载实体的连接(JOIN)操作可能会成为性能问题或是继承策略在规模方面的一个彻头彻尾的障碍。还需要注意的是,Hibernate在使用连接策略时并未正确地处理鉴别器列

顺便说一下,在使用Hibernate代理的时候,一定要注意,延迟加载使用上述三种策略中的任一种映射的类时,总是返回一个代理,而该代理是超类的一个实例

 

这些就是所有可用的选项了吗?

 

那么,总结一下,在从JPA的标准继承映射选项中进行选择时,以下规则适用:

Ÿ           小型的继承层次体系->单表。

Ÿ           宽度的继承层次体系->每个类一张表。

Ÿ           深度的继承层次体系->连接。

 

但如果继承的层次体系有相当的宽度或者非常深的话则会怎样呢?以及如果系统中的类经常要修改的话又会怎样呢?如同我们在为我们的Java EE部署自动化产品Deployit创建一个持久命令框架一个灵活CMDB时所发现的那样,大型继承层次体系底部的具体类可能会经常改变,因此,这两个问题往往会一起得到一个肯定的答案,很幸运一个方案就可以解决这两个问题!

 

使用二进制大对象(blob

 

首先需要注意的是,继承占据了对象关系阻抗失配中的很大分量,然后我们应该问自己这样的一个问题:为什么我们甚至会把所有这些经常改变的具体类映射到数据库的表上?如果对象数据已有真正的突破的话,那么把这些类存储在这样的数据库中可能情况会更好一些,而实际的情况是,这个世界是属于关系数据库的,所以这是不可能的。也可能存在这样的情况,对于对象模型的某一部分来说,关系模型实际上是有意义的,因为你希望执行查询以及让数据库来管理(外键)关系,但就一些地方来说,你实际上只是对简单的对象持久感兴趣。

一个很好的例子就是我在前面提到过的“持久命令框架”,该框架需要存储关于每个命令的一般信息,例如到其所属的“变更计划”(一种执行上下文)的引用、开始和结束时间、日志输出等等,但是它还需要存储代表了真正要完成的工作(对wsadimin或者wlst或者用例中类似工具的调用)的命令对象。

对于第一部分来说,继承模型是最适合的,对于第二部分来说,简单的序列化就可以实现,因此,我们先定义一个简单的接口,我们系统中的不同命令对象会实现这一接口:

 

public interface Command {

void execute();

}

 

然后我们创建一个存储了元数据(我们想存储在关系模型中的数据)和已序列化的命令对象这两者的实体:

 

@Entity

public class CommandMetaData {

@Id

@GeneratedValue(strategy = GenerationType.AUTO)

private int id;

 

@ManyToOne

private ChangePlan changePlan;

 

private Date startOfExecution;

 

private Date endOfExecution;

 

@Lob

private String log;

 

@Lob

@Column(name = "COMMAND", updatable = false)

private byte[] serializedCommand;

 

@Transient

private Command command;

 

public CommandMetaData(Command details) {

           serializedCommand = serializeCommand(details);

}

 

public Command getCommand() {

           if (command != null) {

                    command = deserializeCommand(serializedCommand);

           }

           return command;

}

 

[... rest omitted ...]

}

 

因为@Lob这一注解的缘故,serializedCommand域是一个在数据库中被当成blob存储的字节数组,列名被显式地设为“COMMAND”,以防止“SERIALIZEDCOMMAND”这一缺省列名在数据库模式中出现。

Command域标记为@Transient以防止其被存储到数据库中。

CommandMetaData对象创建的时候,一个Command对象被传入,构造函数序列化该命令对象并把结果存放在serializedCommand域中。在那之后,命令就不能够被修改了(不存在setCommand()方法),因此,serializedCommand能够被标记为不可更新的,这可以防止每次CommandMetaData的其他域(例如log域)被更新时,这一相当大的blob域被写入数据库中。

每次getCommand方法被调用时,如果需要的话命令会被反序列化,然后被返回。如果该对象在多个并发线程中使用的话,可以把getCommand标记为同步的(synchronized)。

关于这一做法需要注意一些事情:

Ÿ           序列化方法的使用影响了这一做法的灵活性,标准的Java序列化很简单,但是不能很好地处理变更的类,XML可以是一种选择,但是会带来其本身的版本问题,选择合适的序列化机制这一问题就作为一个留给读者的练习。

Ÿ           虽然blob已经存在了一段时间,一些数据库仍对它们有所争执,例如,HibernateOracle中使用blob可能就会非常棘手。

Ÿ           在上面介绍的方法中,在对象已被序列化之后,任何对该Command对象的更改都不会被存储,巧妙地利用@PrePersist@PreUpdate这些生命周期钩子就可以解决该问题。

 

这个半对象数据库/半关系数据库的持久方法对我们来说效果相当好,我很想听听其他人是否尝试过同样的做法,以及进展如何。或者,你考虑过这些问题的其他解决方案吗?

 

你可能感兴趣的:(设计模式,oracle,Hibernate,框架,jpa)