转载请注明出处,保持署名
1. 简介
实体之间的关联关系是刚入门的同学比较头疼的问题,但是在日常开发中又是比较重要的技巧,熟练的使用实体关联,能够使代码清晰易懂,并且节省宝贵的开发时间。通常情况下,对于存在关联关系的两个实体,我们会明确指定其中一个实体为关系的维护端,而另一个实体为关系的查询端(反向端)。对于OneToMany或ManyToOne关系,JPA规范明确指出以Many一端为关系的维护端,One一端为关系的查询端;而对于ManyToMany的关联关系,双方都可以作为关系的维护端,因此在日常的开发工作中会出现一些误用。 本文主要从两个方面谈一下@ManyToMany关联,一方面是@ManyToMany的常用场景,另一方面是@ManyToMany的使用误区。
2. @ManyToMany的常用场景
1)单向@ManyToMany关联
举例来说,User和Role是ManyToMany关联,假如我们只关心一个User有多少个roles,而不关心一个Role下有多少个users,这种情况就属于单向关联。在这种情况下,@ManyToMany注解只出现一次,并且是在关系的维护方,即User这一边,代码如下:
@Entity public class User extends Model { @Id public long id; public String name; @ManyToMany(cascade=CascadeType.ALL) public List<Role> roles = new ArrayList<Role>(); public static Finder<Long,User> find = new Finder<Long,User>(Long.class, User.class); }
@Entity public class Role extends Model { @Id public long id; public String name; public static Finder<Long,Role> find = new Finder<Long,Role>(Long.class, Role.class); }
注意,因为不需要根据Role查询User,所以Role实体没有users属性。
Code-1:
User u1=new User(); u1.name="u1"; Role r1=new Role(); r1.name="r1"; u1.roles.add(r1); //r1,u1和r1之间的关系都会被保存进数据库,插入到数据库后,r1和u1的id都为1 u1.save();
2)双向@ManyToMany关联
还是上面的User和Role的例子,这时不仅要从User查询关联的roles,而且要从Role查询关联的users,这种情况属于双向关联。在这种情况下,@ManyToMany注解在实体双方对会出现,但是需要注意的是,关系的查询端需要指定mappedBy属性(该属性在下文会有详细介绍)。理论上来讲,User和Role都可以作为关系的维护端,但是通常情况下,我们指定User为关系的维护端,Role为关系的查询端。代码如下:
@Entity public class User extends Model { @Id public long id; public String name; @ManyToMany(cascade=CascadeType.ALL) public List<Role> roles = new ArrayList<Role>(); public static Finder<Long,User> find = new Finder<Long,User>(Long.class, User.class); }
@Entity public class Role extends Model { @Id public long id; public String name; @ManyToMany(mappedBy="roles") public List<User> users = new ArrayList<User>(); public static Finder<Long,Role> find = new Finder<Long,Role>(Long.class, Role.class); }Code-2:在code-1的基础上,测试如下代码,
//通过Role查询关联的users,users列表中包含上面保存的u1,大小为1 Role.find.byId(1L).users.size();
3. @ManyToMany的使用误区
1)双向@ManyToMany关联时,实体双方都未指定mappedBy属性
如果两边都未指定mappedBy属性,则EBean会认为双方都可以作为关系的维护端,为两个实体各自生成一张关系表,即USER_ROLE为User的关系维护表,ROLE_USER为Role的关系维护表。这样导致的结果是,双方的关系信息分别存在各自的关系表里,导致通过User保存的关系,Role实体无法查询到,反之亦然。测试代码如下:
Code-3:首先建立关系,
User u1=new User(); u1.name="u1"; Role r1=new Role(); r1.name="r1"; u1.roles.add(r1); //r1,u1和r1之间的关系都会被保存进数据库,插入到数据库后,r1和u1的id都为1 u1.save();Code-4:查询关系:
//输出 users.size()为0 System.out.println(Role.find.setId(id).fetch("users").findUnique().users.size());
2)使用关系的查询端保存关系
你觉得下面的代码可以正确的保存关系吗?答案是否定的,有兴趣的同学可以自己试一下!
User u1=new User(); u1.name="u1"; Role r1=new Role(); r1.name="r1"; r1.users.add(u1); r1.save(); //r1和u1间的关系并没有被保存,因为Role是关系的查询端,而不是维护端
3)mappedBy属性的使用
mappedBy属性用于明确的指定该实体为关系的查询端,而另一个实体为关系的维护端。属性的值可以理解为另一个实体的外键,我们再来看一下Role实体的定义:
@Entity public class Role extends Model { @Id public long id; public String name; @ManyToMany(mappedBy="roles") public List<User> users = new ArrayList<User>(); public static Finder<Long,Role> find = new Finder<Long,Role>(Long.class, Role.class); }
mappedBy的属性值为"roles",表示的意思是:根据关系表中的role.id来查询所有的roles。实际上在@OneToMany的注解中看起来更加直接一点。
4) cascade注解属性的使用
cascade注解属性一般用在关系的维护端,在上面的例子里就是User这一端。在ManyToMany关联中,级联删除只会删除关联关系,而不会删除关联实体。
//在删除u1时,u1和r1的关联关系会被删除,但是r1实体不会被删除 User.find.byId(1L).delete();
4. 小结
1)在双向关联关系中,明确使用mappedBy属性指定关系的查询端(反向端),另一端为关系的维护端;
2)关系的查询端(反向端)只能查询关系,而不能保存关系;
3)ManyToMany关系中的级联删除,只会删除关联关系,而不会删除关联实体。