跳到主要内容

原因

和其他ORM不同,Jimmer采用不可变对象作为实体对象

初衷

禁止循环引用

ORM常常伴随一个非常头疼的问题:循环引用。

  • 包含循环引用的数据结构无法被简单地JSON序列化,这对HTTP API的设计是致命缺陷。

  • 其实,这个问题并非无法解决,比如,可以利用JSON序列化库 (如Jackson) 的高级功能序列化包含循环引用的数据结构。但这毫无意义,因为序列化后的结果包含了特殊的扩展内容,并不能为客户端的所有编程语言所理解。

作为一个ORM,Jimmer当然允许用户在实体类型之间定双向关联,这是ORM一个强大的能力。

然而,在实例化对象时,一旦开发人员决定了聚合根的类型,那么被创建的数据结构就只能是以聚合根对象作为起点的单向树。

即,实体类型之间可以定义双向关联,保留两个方向的可能性。待实例化对象时,再按照业务特性选择使用其中一个关联方向。

提示

Jimmer中任何实体相关数据结构绝不包含循环引用,因此,无需做任何转化,就可以直接参与JSON序列化进而被HTTP协议传输,并保证HTTP传递的数据的简单性以保证任何编程语言都可以理解。

这为应用开发带来了极大的简化。

演示

TreeNode aggregateRoot = Objects.createTreeNode(aggregateRootDraft ->
aggregateRootDraft
.setName("Aggregate root")
.addIntoChildNodes(childDraft ->
childDraft
.setName("Child")
// 抛出CircularReferenceException
.setParent(aggregateRootDraft)
)
);
信息

上面代码使用了一个名为TreeNode的类型,该接口是Jimmer根据用户定义的类型TreeNode自动生成的接口类型。

读者可先行忽略这个自动生成的接口,后续文档Draft会对其做出介绍

提示

这段代码会导致异常:org.babyfish.jimmer.CircularReferenceException。

Jimmer对象是不变对象,用户既无法在创建对象时构建循环引用,也无法通过后期修改来形成循环引用,这个品质将会被永远保留。

其他好处

除了保证对象之间绝对没有循环引用外,不可变对象还有很多其他好处。

  • 多线程安全性,这点很好理解,不用解释。

  • 对于Set、Map这类依赖hashCode的集合容器而言,迫切需要对象的不可变性。

    一旦Set或Map (作为Key) 这样的hashCode敏感集合容器持有了可变对象,开发人员就必须小心翼翼地确保不去修改被这些容器共享的数据。如果不慎犯了错误,通常需要debug跟踪来发现问题,这往往会浪费时间影响效率。(其实,hashCode敏感集合容器持有可变对象是一种常见的行为,也可以说大部分Java代码都是不严密的,只是开发人员避开了此问题而已)

  • 实际开发中还有一些长期持有对象的场合,虽然不依赖hashCode,但也会因长期持有对象而导致问题,比如:

    • 利用WebSession中长期持有某些数据

    • 利用一级缓存,即,使用JVM内存的进程内缓存,长期持有某些数据

    细心的开发人员肯定不希望WebSession或Cache中被长期持有数据的引用和泄漏到用户代码中的引用共享数据结构,进而导致不可控的彼此干扰。

    因此,对WebSession或JVM内部Cache这类长期持有数据的结构进行读写操作时,细心的开发人员会将可变数据结构复制一次再进行保存或返回。其中,写入时复制尚可接受,但每次读取都复制一次显得昂贵。可见

    • 使用可变对象,是否需要复制对象以保证必要安全性,依赖于开发人员的风险预知能力,这需要开发人员有一定经验并生性谨慎。然而,即便预知了风险,解决之道也没有客观标准,过于严格会导致过多不必要复制,形成浪费,过于宽松会导致复制不足,出现BUG (团队人数越多,越容易出错)。而且,对于有一定体量的数据而言,团队内部对这种保护机制的严格程度也常有争论,具备很强的主观性。

    • 使用不可变对象,只有数据结构被“修改” (这里的修改是伪修改,并非真正的修改当前数据,后续文档会详细讨论之) 时才复制部分数据 (Jimmer/Immer内部对此有优化,被修改的对象会被复制,从其上级对象开始到根节点为止的所有祖先节点也会被复制,然而,其余所有未变的分支仍然共享重用) 得到新的聚合根引用,其余情况一律简单共享原始引用即可。具备非常严格的无可争议的客观性。

    提示

    毫无疑问,基于客观规律进行开发,必然优于基于主观感觉进行开发,这非常重要。