JPA 中级联之OneToMany
在JPA中@OneToMany注解一对多的关系,而@ManyToOne注解多对一的关系。
-
说配置
按不同的业务需求可分为单项关联和双向关联。
单项关联
单项关联即单独使用@ManyToOne,或者@OneToMany
例如:有dialog对话表和person人员表,一个person有多个dialog,实体关联关系显然是一对多
- 使用ManyToOne,只需要配置many方,dialog表即可
// 这里的personId属性是不必要的,仅仅只是示例 // 因为@ManyToOne默认是EAGER的,所以person属性自然可以拿到对应personId // 但是呢,双向关联时,千万不能这么配置!必须干掉这样的配置 @Column(name = "person_id") public void getPersonId() { return personId; } // optional默认是true,表示left join,而false表示inner join @ManyToOne(optional=false) // referencedColumnName是被关联表的字段名称,一般不需要配置,除非person表的主键名称不是id @JoinColumn(name = "person_id",referencedColumnName="id") public Person getPerson() { return person; }
这样配置的意义是侧重dialog表的数据保存,存储person的每个dialog,而不关心每个person具体有多少dialog记录。
- 使用OneToMany,只需要配置one方,person表即可
@OneToMany(fetch=FetchType.LAZY) @JoinColumn(name="person_id") public Set<Dialog> getDialogs() { return dialogs; }
这样配置的意义是侧重每个person具体有多少dialog记录。
注意:insertable,updatable只有双向关联才有作用,所以在@OneToMany和@ManyToOne无需配置。
双向关联
双向关联时,many方使用@ManyToOne注解,one方使用@OneToMany注解
继续使用上述的例子。
- 此时的many方dialog表配置
@ManyToOne @JoinColumn(name = "person_id",insertable=true,updatable=false) public Person getPerson() { return person; }
insertable表示当dialog记录insert的时候,与person表关联的字段person_id是否需要插入,默认true,是需要插入的。
updatable表示当dialog记录update的时候,与person表关联的字段person_id是否需要更新,默认是true,是需要更新的。
具体的配置看业务需求。我这里的updatable配置为false,是因为在业务中dialog记录不可能去更新(说过的话怎么可能更改呢)。 - 此时的one方person表配置
@OneToMany(fetch=FetchType.LAZY,,cascade=CascadeType.ALL,mappedBy="person") @JoinColumn(name="person_id") @Fetch(FetchMode.SELECT) public Set<Dialog> getDialogs() { return dialogs; }
mappedBy用来指定关联关系的被维护方。这句话有点抽象,我详细解释下。
此处的关联关系是person表的主键id去映射dialog表的外键person_id。
而person是被维护方,dialog是维护方,说的是dialog表中外键的person_id的增删改查,由dialog表来负责,person表你一点也不需要操心。
cascade表示级联权限,分为CascadeType.ALL,CascadeType.PERSIST,CascadeType.MERGE,CascadeType.REMOVE,CascadeType.REFRESH,CascadeType.DETACH六种。
此时的配置表示Person对Dialog有CascadeType.ALL权限,即所有的权限。
- 使用ManyToOne,只需要配置many方,dialog表即可
-
说保存
单向关联时,不管是mang方还是one方的数据CRUD都与另一方无关,即不受另一方的约束,只是能保存其映射属性,而非实体。
这里重点说说双向关联的保存。one方保存many方
虽说是保存,还是分两种情况。
1.one方新建,many方新建,双方需要相互set
示例代码:
Person person = new Person(); person.setName("kevin"); person.setAge(11); Set<Dialog> dialogs = new HashSet<Dialog>(); for(int i = 0; i < 2; i++) { Dialog dialog = new Dialog(); dialog.setContent(content);
// dialog中set person属性 dialog.setPerson(person); dialogs.add(dialog); } // person中set dialogs集合 person.setDialogs(dialogs); this.getEntityDao.save(dialogs);
2.one方持久态,many方新建
通常这种情况,需要先delete之前的many方数据,再insert新的数据到many方。
示例代码:Person person = this.findById(id); Set<Dialog> dialogs = person.getDialogs(); // 先删除之前的老数据 if(dialogs != null && examinees.size() > 0) { // 如果是双向级联必须解除双方级联关系后删除,必须是双方 for(Dialog d : dialogs) { d.setPerson(null); } person.setDialogs(null);
this.getEntityDao().deleteAll(dialogs); } // 这一步非常非常关键! dialogs = person.getDialogs(); if(dialogs == null) { dialogs = new HashSet<Dialog>(); } for(int i = 0; i < 2; i++) { Dialog dialog = new Dialog(); dialog.setContent(content); dialog.setPerson(person); dialogs.add(dialog); } person.setDialogs(dialogs); // 此处需要使用save方法 this.getEntityDao.save(person);仍然需要相互set来维持彼此的关联关系,但不同的是dialogs这次是从person中get出来的。
中间dialogs = person.getDialogs这句是再次获取dialogs,非常非常关键!不然会抛出异常
org.springframework.orm.hibernate3.HibernateSystemException: Found two representations of same collection。
原因分析:
第一次Set<Dialog> dialogs = person.getDialogs()获取到了dialogs集合,
person.setDialogs(null)后dialogs为null,紧接着又重新dialogs = new HashSet<Dialog>(),产生新的集合dialogs。
所以commit的时候,内存中两种dialog集合,一个是delete状态,另一个是new状态。所以才导致这种异常。
dialogs = person.getDialogs()再次获取dialogs维持其dialogs最新的状态。 -
说删除
如上,这里的删除讲述双向关联的删除,分三种情况。
1.one方删除时,同时删除many方
one方中@OneToMany中cascade配置包含CascadeType.REMOVE或CascadeType.ALL
示例代码:
Person person = this.findById(id); this.getEntityDao().delete(paper);
注意:CascadeType.REMOVE威力强大,请慎用。
2.只删除many方
双向关联时,删除任意一方都必先解除双发级联关系,即set对方为null。
示例代码:Person person = this.findById(id); Set<Dialog> dialogs = person.getDialogs(); // 先删除之前的老数据 if(dialogs != null && examinees.size() > 0) { // 如果是双向级联必须解除双方级联关系后删除,必须是双方 for(Dialog d : dialogs) { d.setPerson(null); } person.setDialogs(null);
this.getEntityDao.deleteAll(dialogs); }
3.只删除one方
如上所说,必须先解除双方级联关系
示例代码:Person person = this.findById(id); Set<Dialog> dialogs = person.getDialogs(); // 先删除之前的老数据 if(dialogs != null && examinees.size() > 0) { // 如果是双向级联必须解除双方级联关系后删除,必须是双方 for(Dialog d : dialogs) { d.setPerson(null); } person.setDialogs(null); this.getEntityDao.delete(person); }
-
总结
1.按照自己业务需求,确定是单向关联还是双向关联,并合理的进行注解
2.单向关联的增删改查都相对简单,一般不会出什么错。
3.双向关联时级联保存需要相互set,删除时按照不同的需求进行删除。