springboot jpa 关系注解和mappedBy

jpa系列:
springboot jpa使用
springboot jpa 关系注解和mappedBy
springboot jpa自定义查询


本文需要:
1.了解E-R图转换为关系模式,知道数据库表中外键如何设置
2.了解springboot jpa 注解,知道如何用类成员表示外键部分(这部分我没写),同时为了代码简便省略一些不是重点的注解。一个类对应一个表,叙述上可能混合使用了。
3.这里提到的主键,均是只有一列为主键的情况。联合主键的情况相似,但请自行百度。这里主要是为了整理mappedBy、cascade这些令人困惑的东西,基础部分也还请自行百度。


@ManyToMany

1.表结构:表a、b在E-R图中关系为n:n,表c有两个外键(a_id、b_id)分别指向表a、b。
2.单、双向结构:针对代码而言,通俗来说就是类中是否互有对方的引用。如:

class A {
    int id;
    String name;
    @ManyToMany
    @JoinTable(name = "c",
        joinColumns = @JoinColumn(name = "a_id"),
        inverseJoinColumns = @JoinColumn(name = "b_id"))
    List<B> bList;
}
class B {
    int id;
    String name;
    @ManyToMany
    @JoinTable(name = "c",
        joinColumns = @JoinColumn(name = "b_id"),
        inverseJoinColumns = @JoinColumn(name = "a_id"))
    List<A> aList;
}

A中有B,B中有A,就是双向了。如果只是A中有B或B中有A,都叫单向。

3.增删改查问题
以上面代码为例子,操作为继承jpa接口的DAO层发起的操作,而非Session.persist()这些直接使用hibernate的。假定有对象A a; B b;
查:可以形成循环引用。即a.bList有b,b.aList有a是可行的
改:a.getBList().get(0).setName("bbb"),然后保存a,那么表b对应数据会被更新。
增:a.getBList().add(new B())然后保存a,那么出错,提示表b不存在该数据。此时需要在类A中加上cascade=CascadeType.MERGE。你肯定想问为什么不是CascadeType.PERSIST,参考这里:PERSIST无法级联存储
删:删除a,那么jpa只生成一条"删除a的sql",可能出现的问题就要参考数据库中对外键的约束设置了。为了让开发者不依赖数据库而清晰的知道结果,这里可以加上参数cascade=CascadeType.REMOVE,那么jpa将生成多条sql,把与a关联的表b数据和表c数据删除。

4.上面例子中为“双向结构+双向维护外键”。这个双向维护外键,指无论是对a.bList还是b.aList修改,保存后均会影响表c数据。

5.双向结构+A维护外键

class A {
    int id;
    String name;
    @ManyToMany
    @JoinTable(name = "c",
        joinColumns = @JoinColumn(name = "a_id"),
        inverseJoinColumns = @JoinColumn(name = "b_id"))
    List<B> bList;
}
class B {
    int id;
    String name;
    @ManyToMany(mappedBy="bList")
    List<A> aList;
}

先理清逻辑:
(1)mappedBy与@JoinColumn和@JoinTable互斥,不能同时作用于一个成员
(2)@JoinColumn和@JoinTable描述了数据库表的外键信息
(3)约定优于配置。上面例子看起来似乎缺少外键具体关联哪个表的信息,但事实上jpa会根据所在类和注解的成员自动推断。因此网上各教程中参数的书写情况不完全一致。
(4)类B由于mappedBy的互斥,缺少描述外键信息的注解,查找时jpa似乎无法查找数据并封装到aList,因此我们需要告诉jpa去哪找外键信息。显然外键信息就在A.bList(该成员保存着@JoinTable注解),因此最终写成mappedBy="bList"
(5)类A有@JoinTable注解而能够直接获取外键信息,增/删/改操作时jpa会同步操作外键;类B不直接具有外键信息,级联增/删/改时jpa当外键不存在,但级联操作仍然有效。
(6)混合cascade和mappedBy,以例子举例:
情形1:a.getBList().add(new B())
答:出错。参考cascade提到的CascadeType.MERGE。
情形2:A中增加CascadeType.MERGE,a.getBList().add(new B())
答:表b多了一条数据,表c多了一条数据以表示关联关系
情形3:B中增加CascadeType.MERGE,b.getAList().add(new A())
答:表a多了一条数据,表c不受影响
情形4:A中增加CascadeType.REMOVE,删除对象a
答:与a关联的对应表b表c数据会删除
情形5:B中增加CascadeType.REMOVE,删除对象b
答:表a表c均受影响。原因为删除b时,先级联删除b.aList的所有对象,而这些A对象被删除时,触发情形4。注意,此时情形4中又会级联删除B对象,这些B对象又级联回去删除A对象,造成连环反应。因此mappedBy与cascade的取值一定要谨慎设计,不建议情形5这种设计方式。

总结:

1.继承jpa接口的实现方式中,级联更新无需任何设置便有效。CascadeType.MERGE实际是允许级联更新+级联创建。
2.jpa只根据直接的外键信息进行操作外键。mappedBy这种间接提供外键信息的,级联操作时不会直接增、改、删外键(如情形5的连环反应,这种例外)。
3.在上面例子,A叫关系拥有方,B叫关系被拥有方。


@OneToOne

没啥好说的,此时用@JoinColumn而不是@JoinTable。参数方面、cascade、mappedBy、双向结构这些跟上面一样的。
有点麻烦的就是数据库表实现方式很多样,比如这个1:1关系:
1.你可以只在一个表中放外键。但这样可以实现双向结构吗,或者能够换个方向实现单向结构吗?为了避免麻烦,此时只用单向结构,方向也确定下来,让有外键的一方持有另一方的引用。
2.两个表都放外键。单/双向结构均可,推荐采用双向结构。
3.用中间表实现。那么该用@JoinColumn+table参数还是用@JoinTable?不说了,有兴趣自己尝试。


@OneToMany和@ManyToOne

细节同上。
麻烦之处也类似:
1.用jpa生成表时,默认策略是用中间表还是在多的一方加外键?数据库已有表时,jpa会不会把我的表改成中间表实现/多的一方加外键?
答:生成表时,有mappedBy则采用在多的一方加外键;没有则用中间表。jpa不会乱搞你已有的表,不存在时帮你创建而已。
2.多的一方加外键+双向结构+双向维护外键,那么@OneToMany那边怎么写@JoinColumn注解?它没有外键也能用这个?
答:把@ManyToOne那边的@JoinColumn复制过来。如stackoverflow
3.有多少种可能设计和该设计的bug?
答:(2种方向的单向结构+用不用mappedBy的双向结构)*(数据库表的两种实现)=若干种组合。各个组合都有要点,我踩雷快踩疯了,全写出来又要长篇大论了,不写了。

推荐方式:先用数据库管理工具设计好表,毕竟有ui的直观点。另外代码采用双向结构+mappedBy。(@OneToMany无参数mappedBy)


springboot jpa单元测试的坑

(1)懒加载FetchType.LAZY实际是在session中挂一个代理对象来实现的,测试没有session中将导致报错。
(2)如果用@Transactional来注解测试,问题(1)可以解决但将引入新的问题:事务回滚。非测试情况下@Transactional默认当异常或错误出现时回滚,但在单元测试中@Transactional不管什么情况一律回滚(以免单元测试导致污染数据库)。此时可以搬到实际Controller中进行学习测试,或者单元测试中所有类都调用findAll()再打断点去看是否符合你的预想。

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

推荐阅读更多精彩内容