本次实验环境:spring boot + spring data jpa + hibernate
本次实验用例:
狼:
id
name
bei
狈:
id
name
lang
本次试验使用狼狈为奸的典故。可以从狼找到狈,也可以从狈找到狼。这是一种双向关系。
狼中持有狈的引用,狈中也持有狼的引用。
第一种状况,只用了@OneToOne注解
#Lang类:
@Entity
@Table(name="t_121_lang")
public class Lang {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne
private Bei bei;
//getter与setter省略
}
#Bei类:
@Entity
@Table(name="t_121_bei")
public class Bei {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne
private Lang lang;
//getter与setter省略
}
#生成表
此时各自表中都有外键,都指向对方的id字段。
现在的无论哪一方都没有使用cascade。
此时:
- 添加操作就分为三步:新增lang、新增bei、将两者关连。这种操作是可行的,但是非原子性且麻烦。实际sql语句涉及两个insert和两个update。
- 更新关系的操作得操作双方,即改变了lang中的bei_id,则在相对应的bei中也要修改lang_id,否则关系错乱。且该操作应该是同一事务。
- 删除操作,也要更改双方,删除了lang也要更改对应的bei的lang_id,且该操作应该是同一事务。
假如我们使用了cascade,此时:
- 添加的步骤,new 一个bei,new 一个lang,调用lang.setBei(bei),然后使用langDao添加lang,此时数据库会新增两个数据,lang表中的数据正确,但是bei表中的lang_id并没有指向新增的lang数据,我们必须再调用bei.setLang( )来设置这个方向的关联。
- 更新关系操作,仍然需要执行两个update。
- 删除操作,删除lang会级联删除bei,此结果不正确,所以cascade应该不包含REMOVE。
此种状况最麻烦的地方在于,两个表中的外键是循环外键,所以不管直接删除哪个表都会失败,只能先修改表结构将外键关系去掉才能删表。
第二种状况,使用mappedBy
#Lang类:
@Entity
@Table(name="t_121_lang")
public class Lang {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(cascade=CascadeType.ALL, mappedBy="lang")
private Bei bei;
//getter与setter省略
}
#Bei类:
@Entity
@Table(name="t_121_bei")
public class Bei {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(cascade=CascadeType.ALL)
@JoinColumn(name="lang_id")
private Lang lang;
//getter与setter省略
}
#生成表
此时,lang表中没有外键,bei表中由外键。故总结,如果想在A方的生成的表中有外键,则使用@JoinColumn,则在另一方B中使用mappedBy,其取值为A方中@JoinColumn修饰的成员属性。
因为两张表的结构出现了差异,所以代码上也需要相应的编写技巧。
- 添加操作,因为lang表中没有外键,所以代码中通过langDao来添加,可以添加两条数据,但是这两条数据无关联。
Bei bei = new Bei();
bei.setName("狈A");
Lang lang = new Lang();
lang.setName("狼A");
lang.setBei(bei);
langDao.saveAndFlush(lang);
要添加关系可以通过beiDao来实现。
Lang lang = new Lang();
lang.setName("狼B");
Bei bei = new Bei();
bei.setName("狈B");
bei.setLang(lang);
beiDao.saveAndFlush(bei);
查询,假如我们查询id为2的lang,
Lang lang = langDao.findOne(2L);
System.err.println(lang);
此时会调用lang的toString方法,方法如下:
@Override
public String toString() {
return "Lang [id=" + id + ", name=" + name + ", bei=" + bei + "]";
}
在该方法中又会调用bei的toString,bei的toString如下:
@Override
public String toString() {
return "Bei [id=" + id + ", name=" + name + ", lang=" + lang + "]";
}
这个方法中又调用了lang的toString,形成了循环调用,会报栈溢出的错误,这点一定要小心。更新,实质无论是通过langDao还是beiDao来改变数据库中的数据,真正产生影响的是传入的数据。
比如更新lang,lang类有三个属性:id、name、bei,但是lang表只有两个字段:id、name,那么多出的bei属性实际是对bei表进行操作,如果bei属性为空,则bei表中不做任何操作,反之则做相应的添加或更新操作。
所以在第一种通过langDao来添加时只需加上bei.setLang(lang);
这句代码,则也可以产生关系。为了简便,更新lang时直接不管bei或者将bei置空即可。
更新bei时则需要当心lang_id,如果也置空lang属性表中lang_id也会变成null。删除,同样不能使用CascadeType.REMOVE,否则就级联删除了,如果你的业务需求就是共生关系级联删除,则可以使用。但是在当前的业务背景下,我们删除一方,另一方应该仍然存在只是双方的关系断掉,目前的做法是在同一个事务中先断关系再删除。当如如果是删除bei直接删除即可,因为关系就维护在这条数据中,连带自身字段的值和关系一起消失。