提到树状映射,许多人肯定会头疼死了,因为看"树状"这俩字就肯定想到会跟数据结构打交道,而数据结构是本科阶段最重要也是最难学的一门专业课。说实话摩罗我《数据结构》这门课学得也不咋滴,花了点时间终于完成了hibernate之树状映射这个小程序。
首先我先定义了个公司组织类(Org),一个组织下面可以有多个子组织,但每个组织都只有一个上级组织,也就是传说中的一对多的关系,但这个关系是双向的。提到双向关系,我们第一件事就是在双向关系的两边都要设双向关联,另外为了防止产生冗余,我们要在其中的一方设mappedBy,至于设在哪方,等下在下面的程序中解释。
既然确定了这个组织类(Org)是双向关系的,那么该如何定义这个类呢?首先从数据库表设计的角度来考虑一下,每个组织下面都可能有多个子组织,而每个子组织对应的只有一个上级组织,这是典型的一对多关系,一个组织拥有它自己的id以及组织名字(name),这俩已经可以描述这个组织了,但怎么描述各个组织之间的关系呢?也就是说在表里我们还必须定义字段去提现关系。因为是一对多的关系,一对多映射关系有一个规律很重要,那就是必须是在“多”的这方设置“一”的这方的外键,在这里就是在下级组织中设置上级组织的外键,这样通过这个外键我们就可以找到这个组织对应的上级组织是谁。有的同学可能会疑惑了:为什么非要在“多”的这方设置设置“一”的这方的外键呢?而不是反过来呢?来看看下面这个例子就会明白为什么要这样设置了。
注:以下是数据库表里的错误设计方法:
GroupId |
GroupName |
UserId |
1 |
Group_1 |
1 |
1 |
Group_1 |
2 |
UserId |
UserName |
1 |
moluo |
2 |
xingzhe |
一个组(Group)里可以有多个用户(User),这是典型的一对多关系,如果在Group(也就是在“一”的这方)设置User(“多“的这方)的外键,那么表中红色字体部分的信息是不是产生了冗余?这只产生了两条记录的冗余,还可以接受。但如果Group表有很多个字段,并且有很多条记录呢?这样设计产生的冗余就大得夸张了,所以必须在“多”的这方设置“一”的这方的外键。
解释完数据库里表的设计方法,再来讲讲程序里Org这个类该如何定义。
首先Org这个类是一个组织,从面向对象的角度来考虑,它既有上级组织,也有下级组织,当然id和name这俩属性是必不可少的。另外更重要的是它的上级组织以及下级组织实际上也是一个组织,于是我就定义出了下面这个Org类:
package com.hibernate.model; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @Table(name="_Organization") public class Org { private int id; private String name; private Set<Org> children = new HashSet<Org>(); private Org parent; @Id @GeneratedValue public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @OneToMany(mappedBy="parent",cascade=CascadeType.ALL,fetch=FetchType.EAGER) public Set<Org> getChildren() { return children; } public void setChildren(Set<Org> children) { this.children = children; } @ManyToOne @JoinColumn(name="parent_id") public Org getParent() { return parent; } public void setParent(Org parent) { this.parent = parent; } }
简要解释一下,首先id自增就不解释了,解释一下mappedBy,mappedBy是用来定义类与类之间的关系的,如果类与类是单向关系,那么可以不用定义;如果是双向关系,那么就必须定义mappedBy,mappedBy表示外键在”对方“已经设置过了,这里不用再设置了,如果没有设置mappedBy,就很可能会产生数据的不一致性。另外mappedBy的值应该设成"多"方对应的class里”一“方的变量名,实际上更确切的讲是应该设置成"一"方变量名对应的getXXX方法里那个XXX,因为极少数情况下getXXX()和对应的变量名不一致(当然这是一种极不提倡的做法)。
再来解释一下cascade,该属性定义的是类与类之间的级联关系,定义的级联关系将被容器视为将当前类对象以及与之关联的类对象采取相同的操作,而且这种级联关系是递归的。cascade属性值有:ALL:表示所有情况都进行相同操作,即save、update、delete;PERSIST:这个表示在保存时采取相同的操作,MERGE是JPA的官方叫法,实际上就跟sava()一样;REMOVE:这个表示级联删除,实际上跟delete()方法一样;REFRESH:级联刷新。
接着解释一下fetch:fetch是读操作,它有俩属性,LAZY:表示只读取当前对象,关联对象不读;EAGER:当前对象被读取的时候,关联对象也会被读取,而且这种级联关系是递归的。
@OneToMany:表示当前这个类对象(实体)对应getXXX()方法表示的属性XXX对应的对象(实体)是一对多的关系。
@ManyToOne:表示当前这个类对象(实体)对应getXXX()方法表示的属性XXX对应的对象(实体)是多对一的关系。
@JoinColumn:用来注释表中的字段名字。
最后再来看看JUnit Test Case:
package com.hibernate.model; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.tool.hbm2ddl.SchemaExport; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; public class TreeTest { public static SessionFactory sf = null; @BeforeClass public static void beforeClass(){ try{ sf = new AnnotationConfiguration().configure().buildSessionFactory(); } catch(Exception e) { e.printStackTrace(); } finally{ } } @Test public void testSave() { Org o = new Org(); o.setName("总公司"); Org o1 = new Org(); o1.setName("分公司1"); Org o2 = new Org(); o2.setName("分公司2"); Org o11 = new Org(); o11.setName("分公司1下的部门1"); Org o12 = new Org(); o12.setName("分公司1下的部门2"); Org o21 = new Org(); o21.setName("分公司2下的部门1"); o.getChildren().add(o1); o.getChildren().add(o2); o1.getChildren().add(o11); o1.getChildren().add(o12); o12.getChildren().add(o21); o1.setParent(o); o2.setParent(o); o11.setParent(o1); o12.setParent(o1); o21.setParent(o2); Session session = sf.getCurrentSession(); session.beginTransaction(); session.save(o); session.getTransaction().commit(); } @Test public void testLoad() { testSave(); Session session = sf.getCurrentSession(); session.beginTransaction(); Org o = (Org)session.load(Org.class, 1); print(o,0); session.getTransaction().commit(); } private void print(Org o,int level) { String preStr = ""; for (int i=0; i<level; i++) { preStr += "_"; } System.out.println(preStr + o.getName()); for (Org child:o.getChildren()) { print(child,level+1); } } @Test public void testSchemaExport(){ new SchemaExport(new AnnotationConfiguration().configure()).create(false, true); } @AfterClass public static void afterClass(){ sf.close(); } }
里面定义了个print()方法,里面那个level是为了使打印结果呈现树状结构
打印结果:
总公司 _分公司2 __分公司2下的部门1 _分公司1 __分公司1下的部门2 __分公司1下的部门1
后记:写这篇文章花的时间比我写这个程序花的时间要多许多,希望各位看客如果要转载的时候尊重一下摩罗我的劳动成果,注明原文链接并且不要删除我的二维码,谢谢!
欢迎关注行者摩罗微信公众号(xingzhemoluo),共同交流编程经验,扫描下方二维码即可;