Spring Data JPA/JPA/Hibernate 进阶教程(一)Entity数据加载最佳实践

很多朋友在使用Spring Data JPA/JPA/Hibernate的时候,都会发现Entity加载数据的方式难以控制:有的可以修改为Lazy,有的又不可以;有些时候加一些额外注解可以解决,有些时候又不行,看完本文之后,相信大家可以采取一致的方式解决这些问题。

ORM框架的实现机制非常复杂,很多问题是开发者难以想到的,经常会让大家产生疑问。因为国内JPA实践较少,为了让大家更易于理解Hibernate在Fetch数据时的机制,这篇文章尽可能把话题收缩到目标上,尽可能不过多细化问题,也尽可能不引入其他问题。另外,为了让目标更清晰,本文暂不涉及讨论效率优化问题,也不引入数据表。

看完本文之后,会解决大家的如下问题:
1.为什么@ManyToOne和@OneToOne加上FetchType.Lazy仍然无法实现懒加载?
2.Hibernate是如何考虑读取数据的?
3.如何让Entity的数据读取行为变得统一?
4.如何在保持对象关系的情况下控制数据读取?

一个最简单的例子:

public class OTM{
    //省略其他字段
    @OneToMany(mappedBy = "otm")
    private Set<MTO> mtoSet;
    //省略后面的方法
}
public class MTO{
    //省略其他字段
    @ManyToOne
    @JoinColumn(name="otm_id",referencedColumnName = "id")
    private OTM otm;
    //省略后面的方法
}

备注:上述主键默认为id

@OneToMany和@ManyToOne是JPA里用的最多的两个维护关系的注解,如果你用Repository去分别读取OTM和MTO对象会发现:

情况一:读取OTM对象的时候,mtoSet加载方式是Lazy的。如果把@OneToMany加上fetch=FetchType.Eager可以切换为直接加载(当然这会产生第二个Select,本文暂不讨论这个问题)。
情况二:读取MTO对象的时候,otm的加载方式是Eager的,如果设置fetch=FetchType.Lazy会没有效果,仍然会产生第二条Select。

这是为什么?

要回答这个问题,就必须从Hibernate是如何使用代理去解决懒加载的问题开始。最直观的解决方式是把本来的属性替换为代理,覆盖属性本身的所有可访问方法就可以实现,既然可以,但是为什么上述情况2仍然是Eager?

Hibernate里,如果一个非集合属性对应的表数据不存在,那么这个属性会赋null值,如果一个集合属性对应的表数据不存在,那么这个属性会赋为空的Collection(不为null)。

Hibernate原生处理懒加载的方式是用一个代理去替换需要懒加载的属性(请注意,是替换属性本身),并且重写所有可以访问的方法(即第一次调用的时候Fetch数据写入缓存,后面就直接返回缓存的数据了)。但这会因为属性是不是集合(对应OneToMany和ManyToOne关系)会产生两种完全不同的情况。

OneToMany很简单,因为代理是直接替换的属性,如果有数据就填充集合,没有数据就把集合留空就行了。所以,可以把数据是不是为空后置处理,也就产生了上述情况一的结果。

ManyToOne就麻烦了,因为非集合的属性在对应表没有数据的情况下应赋null值,一旦使用代理后就已经不为null了(变成了属性本身不为null但全部字段为null,到底是数据存在还是不存在?万一恰巧对应这张表所有字段都可以为null呢?),代理不能把自己变为null,所以这个没办法后置处理,必须在一开始的就决定是赋null值还是使用代理。这就使得必须直接产生第二条select去判断如何处理。但是既然第二条select都已经发送了,不如直接把数据也填充了。所以,这就产生了上述情况二的结果。相应的,OneToOne情况类似。

所以,从这个层面总结下来,情况很简单:
1.XToOne的关系,都不能懒加载
2.XToMany的关系,都可以懒加载(当然,实际上还有其他考虑)。

这里顺带解释一个问题,如果开发者细心观察Hibernate产生的表会发现带XToX注解的属性映射出来的表中会存在一张表是有外键约束的(前提是引擎innodb)。因为这是判断是否可以懒加载最佳的方式,如果一个对象中存在另外一个对象的外键约束,不就可以提前判断可不可以使用代理了吗?

这个方法在一些hibernate 3.x和4.x版本里是确实可行的(不稳定,经常变),只要加上fetch=FetchType.Lazy的时候加上optional=false,就可以实现前述无法实现的懒加载。这个方法的逻辑很简单,就是告诉hibernate,这个关系是肯定存在的(optional=false,等同于外键约束存在),那当然就可以使用代理去解决懒加载。但这个方法可靠度非常低,因为在实际中,有可能表的默认创建引擎是Mylsam,也可能外键约束建立失败。实际在历史版本中,各种各样的解决方式非常杂乱,但都逐步被抛弃了。

最统一的实践方式如下:

1.不修改XToX的默认fetch方式,说不定你哪天会发现,新版本又不行了,ORM考虑的问题远远不止你能想到的。

2.如果要改变默认的fetch方式,使用Bytecode Enhancement,有Maven插件可以使用。如下:

    <plugin>
            <groupId>org.hibernate.orm.tooling</groupId>
            <artifactId>hibernate-enhance-maven-plugin</artifactId>
            <version>$currentHibernateVersion</version>
            <executions>
                <execution>
                    <configuration>
                        <failOnError>true</failOnError>
                  <enableLazyInitialization>true</enableLazyInitialization>
                    </configuration>
                    <goals>
                        <goal>enhance</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>

Bytecode Enhancement的功能很强大,暂时这里只说到这儿。加入插件后,在需要改变默认行为的属性上(前述的XToOne属性)加注解:

@LazyToOne(LazyToOneOption.NO_PROXY)

用Maven编译后,Bytecode Enhancement会对所有Entity使用代理去替换,比如前面的情况二,虽然otm代理本身不能把自己设为null,但是MTO对象可以,所以对MTO用代理替换就解决了这个问题,也就解决了XToOne的懒加载问题。

值得,如果一个非集合属性也需要实现懒加载(比如有一个Lob数据),也是可以通过Bytecode Enhancement实现的,这特别适合某个字段数据特别大的情况。你只需要在属性上加上

@Basic(fetch = FetchType.LAZY)

当然,也有一些其他的变通方式,但容易引入不相关的问题。比如,对于OneToOne这种情况,你也可以把映射OneToOne做成OneToMany来解决,但是这不是很好的实践方式。

最后,在实践ORM的时候,要抛弃Table Driven的开发思路(表关系是基础,对象是实现),转变为以对象为中心的思路(对象关系是基础,数据表是实现),这样才会最大的发挥ORM在后期维护上的优势。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容