Hibernate框架学习之注解配置关系映射

时间:2020-12-15 20:35:06

     上篇文章我们通过注解对映射了单个实体类,但是具体项目中往往实体类之间又是相互关联的,本篇文章就是从实体类之间存在的不同关联角度,具体学习下如何映射他们之间的关联,主要涉及内容如下:

  • 单向的一对一关联关系映射
  • 单向的多对一的关联关系映射
  • 单向的一对多的关联关系映射
  • 单向的多对多的关联关系映射
  • 双向的一对一关联关系映射
  • 双向的一对多关联关系映射
  • 双向的多对多关联关系映射

一、单向的一对一关联关系映射
首先,我们需要知道什么样的两张表具有一对一的关联关系。

Hibernate框架学习之注解配置关系映射

这就是一个典型的单向的一对一的关联关系,所谓的一对一其实就是指,主表中的一条记录唯一的对应于从表中的一条记录。但具体到我们的实体类中又该如何来写呢?我们先看一个完整映射代码,然后逐渐解释其中的相关注解。

//定义实体类userinfo
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int user_id;
    private String name;
    private int age;
    
    @OneToOne(targetEntity = UserCode.class)
    @JoinColumn(name = "code",referencedColumnName = "code_id",unique = true)
    private UserCode userCode;
    //省略getter,setter方法
}
//定义实体类usercode
@Entity
@Table(name = "code")
public class UserCode {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int code_id;
    private String code;
    //省略getter,setter方法
}

因为是单向的一对一,所以我们的usercode表并不存在外键列可以直接访问到userinfo表,所以它的实体类配置没什么特殊的地方。而userinfo实体类定义了一个UserCode 类型的属性,当我们使用hibernate进行插入或者返回数据时候,usercode表中对应的记录则会被装在在这个属性中,当然,我们也通过它配置外键关联关系。

@OneToOne注解指定这是一个一对一的关联关系,targetEntity 指定了被关联的实体类类型。@JoinColumn用于配置外键列,name属性用于指定外键列的列名,Hibernate将会在userinfo表中增加一个字段用做外键列。referencedColumnName 属性用于指定该外键列用于参照的表字段,这里我们参照的是usercode表的主键。由于是一对一,所以要求外键列不能重复,指定unique唯一约束即可。

对比着表中的各个字段,再次体会下上述注解中的属性的各个值的意义。
Hibernate框架学习之注解配置关系映射

二、单向的多对一的关联关系映射
依然,在详细学习之前,先看看什么样的两张表构成多对一的关系。

Hibernate框架学习之注解配置关系映射

像这种,userinfo表中多条不同的记录对应于usersex表中的一条记录的情况,我们称作多对一的关联关系。其中,多的一方设有外键列,掌控着关系的维护。看代码:

//定义usersex实体类
@Entity
@Table(name = "userSex")
public class UserSex {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int sex_id;
    private String sex;
    //省略getter,setter方法
}
//定义userinfo实体类
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int uid;
    private String name;
    private int age;

    @ManyToOne(targetEntity = UserSex.class)
    @JoinColumn(name = "sex",referencedColumnName = "sex_id")
    private UserSex userSex;
    //省略getter,setter方法
}

同样,@ManyToOne指定这是个多对一关系,并通过targetEntity 属性指定被关联的实体类型。@JoinColumn依然用于配置外键列。

对比着表中的各个字段,再次体会下上述注解中的属性的各个值的意义。
Hibernate框架学习之注解配置关系映射

三、单向的一对多的关联关系映射
单向的一对多和单向的多对一是完全不同的两种表间关系。虽然两张表看起来是没什么太大差别,但是关系的维护方确实截然相反的。这种情况下,两张表的关系则由一的一方进行维护,所以在一的一端需要定义一个集合属性用于映射多的一端的记录集合,看代码:

//定义一的一端的实体类
@Entity
@Table(name = "userSex")
public class UserSex {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int sex_id;
    private String sex;

    @OneToMany(targetEntity = UserInfo.class)
    @JoinColumn(name = "sex",referencedColumnName = "sex_id")
    private Set users;
    //省略getter,setter方法
}
//定义多的一端的实体类
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int uid;
    private String name;
    private int age;
    //省略getter,setter方法
}

其中,@OneToMany指定了两个表之间的是一种一对多的关联关系,targetEntity 属性指定被关联的实体类类型。这里的@JoinColumn是不一样的,它将生成一个外键字段,但不是生成在本实体类所代表的数据表中,而是生成在被关联的数据表中。name属性指定了外键字段的字段名称,referencedColumnName属性指定了该外键字段的值依赖于本表的那个字段(我们这里让他依赖于userSex的主键)。

实际上一对多就是多对一的一个逆向的关联关系,但是两张表依然是通过一个外键列来维系,只不过这个外键列由谁生成的有点不同。具体的表结构此处不再贴出,我们通过插入数据来感受下一对多的关联关系表。

UserInfo user1 = new UserInfo("a",1);
UserInfo user2 = new UserInfo("b",55);
UserInfo user3 = new UserInfo("c",4);
UserInfo user4 = new UserInfo("d",3);

Set<UserInfo> sets = new HashSet<UserInfo>();
sets.add(user1);
sets.add(user2);
sets.add(user3);
sets.add(user4);

UserSex userSex = new UserSex();
userSex.setSex("男");
userSex.setUsers(sets);

session.save(user1);
session.save(user2);
session.save(user3);
session.save(user4);

session.save(userSex);

当我们执行上述程序的时候,hibernate首先会为我们插入四条userinfo记录到userinfo表中(其中的外键字段为空),然后插入一条记录到usersex表中,在这之后,hibernate将根据set集合中的元素依次执行这么一条SQL语句:

update userinfo set sex=? where uid=?

显然,根据集合中每个元素的id值定位userinfo表,并将这些元素的外键字段同一赋值为当前usersex实例的主键值。这样两张表就形成了对应的关系了。当然,当我们想要取出一条usersex实例时候,hibernate也会拿该实例的主键值去搜索userinfo表,并将匹配的记录装载到set集合中。

不过这种由一的一端管理关联关系的情况有点反常规逻辑,因此不建议用一的一端管理整个关联关系。

四、单向的多对多的关联关系映射
对于单向的多对多关联关系,我们无法使用外键列进行管理。所以,一般会增设一张辅助表来维系两张表之间的关联关系,举个例子:一个人可以有多个兴趣爱好,一个兴趣爱好也可以对应多个人,我可以获取到某个人所有兴趣爱好,也可以获取具有相同兴趣爱好的所有人。如果仅仅使用两张表来描述这种关联关系的话,根本就无法描述,不信你可以试试,即便可以实现,那种表结构也是极其复杂冗余的。目前最好的策略是引入第三方表来维系两张表之间的多对多关联。

Hibernate框架学习之注解配置关系映射

这样的表结构是清晰的,也是易于维护的。下面看看代码实现:

//定义hobby实体类
@Entity
@Table(name = "hobby")
public class Hobby {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int hobby_id;
    private String hobby;
    //省略getter,setter方法
}
//定义实体类userinfo
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int user_id;
    private String name;
    private int age;

    @ManyToMany(targetEntity = Hobby.class)
    @JoinTable(name = "connectTable",
            joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "hobbyid",referencedColumnName = "hobbyid")
    )
    private Set sets;
    //省略getter,setter方法
}

多对多的关联映射需要使用到一个注解@JoinTable,该注解用于指定新生成的连接表的相关信息。name 属性指定表名,joinColumns 配置外键列及其依赖的属性字段,我们这里在新表中指定一列名为user_id并且依赖于userinfo实体的主键字段的值,inverseJoinColumns 用于指定关联的实体类的外键列,我们这里在新表中会生成一列名hobbyid并依赖Hobby实体类的主键值。

当我们插入数据的时候,会首先分别插入两张表的记录,然后会根据userinfo表中的集合属性中的元素向连接表中进行插入。返回数据也是类似的。

五、双向的一对一的关联关系映射
其实本质上看,单向的关联关系和双向的关联关系的区别在于,单向的关系中,只有一方存在对另一方的引用,也就是可以通过外键列指向另一方,而被引用的一方并不具备指向别人的外键列,所以整个关系都只有一方在维护。而双向的关系则是两方都具备关系维护的能力,能够相互访问。我们看看实体类的映射:

//定义userinfo实体类
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int user_id;
    private String name;
    private int age;

    @OneToOne(targetEntity = UserCode.class)
    @JoinColumn(name = "code",referencedColumnName = "code_id",unique = true)
    private UserCode userCode;
    //省略getter,setter方法
}
//定义usercode实体类
@Entity
@Table(name = "code")
public class UserCode {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int code_id;
    private String code;

    @OneToOne(targetEntity = UserInfo.class,mappedBy = "userCode")
    private UserInfo userInfo;
    //省略getter,setter方法
}

映射双向一对一关系的时候,需要在两端都使用@OneToOne修饰,我们在userinfo端增加了一个外键列并指向usercode的主键。往往两张表只要有一方维护着关系就行了,不建议两方同时维护着关系,那样会造成性能上的损失,我们指定mappedBy 属性的值来告诉Hibernate,usercode端不打算维护关系。当我们指定了双向的关联关系之后,两方都存在对方的引用了,实现了互访的能力。

有人可能会有疑问,usercode一端放弃对关系的管理没有设置外键列,那么我们是如何通过usercode获得userinfo的引用呢?答案是:

//从usercode查到userinfo表的引用
UserCode userCode = (UserCode) session.get(UserCode.class,1);

Hibernate框架学习之注解配置关系映射

hibernate通过左连接将根据外键列的值和usercode表的主键值连接了两张表,于是我们可以通过usercode的主键一次性查到两张表对应的记录,最后为我们返回相应的实例。

而如果想要通过userinfo表查询到usercode表的引用相对容易些,因为userinfo表中有一个外键列可以使用。查两次表即可。

六、双向的一对多的关联关系映射
其实双向的一对多和双向的多对一是同一种关联关系,只是主导关系的人不一样而已。看代码:

//定义userinfo实体类
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int uid;
    private String name;
    private int age;

    @ManyToOne(targetEntity = UserSex.class)
    @JoinColumn(name = "sex_id",referencedColumnName = "sex_id",nullable = false)
    private UserSex userSex;
    //省略getter,setter方法
}
//定义usersex实体类
@Entity
@Table(name = "userSex")
public class UserSex {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int sex_id;
    private String sex;

    @OneToMany(targetEntity = UserInfo.class,mappedBy = "userSex")
    private Set users;
    //省略getter,setter方法
}

一的一端使用@OneToMany修饰并放弃对关系的维护多的一端使用@ManyToOne修饰,并增加外键列指向usersex表的主键列。其实和我们介绍的单向多对一基本一样,只是此处的一的一端增加了一个一对多的映射,增加了对userinfo表的一个引用而已。

对于我们从多的一端访问一的一端直接利用的外键列进行访问,从一的一端对多的一端的访问具体会生成以下两条SQL语句:

Hibernate框架学习之注解配置关系映射

先根据usersex的主键值查一次usersex表,再通过usersex的主键值去查一次userinfo表,获取的所有的userinfo记录都会被注入到usersex的集合属性中。

七、双向的多对多的关联关系映射
双向的多对多关系关联的映射依然需要通过第三张辅助表来进行连接。依然使用我们上述的userinfo和hobby举例:

//定义实体类userinfo
@Entity
@Table(name = "userinfo")
public class UserInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int user_id;
    private String name;
    private int age;

    @ManyToMany(targetEntity = Hobby.class)
    @JoinTable(name = "connectTable",
            joinColumns = @JoinColumn(name = "user_id",referencedColumnName = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "hobby_id",referencedColumnName = "hobby_id")
    )
    private Set sets = new HashSet();
    //省略getter,setter方法
}
//定义实体类hobby
@Entity
@Table(name = "hobby")
public class Hobby {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int hobby_id;
    private String hobby;

    @ManyToMany(targetEntity = UserInfo.class)
    @JoinTable(name = "connectTable",
        joinColumns = @JoinColumn(name = "hobby_id",referencedColumnName = "hobby_id"),
        inverseJoinColumns = @JoinColumn(name = "user_id",referencedColumnName = "user_id")
    )
    private Set sets = new HashSet();
    //省略getter,setter方法
}

多对多的两端都需要指定连接表的信息,但配置的是同一张表的信息,基本没什么变化。这些注解也是我们介绍过的,此处不再赘述。比如我们想要获取一个userinfo实例,那么hibernate会先根据指定的主键值查一次userinfo表,然后当需要用到usersex表的相关信息的时候,hibernate会拿userinfo的主键值再去查一次connect连接表,并将查到的usersex实例集注入userinfo的集合属性中。

综上,我们介绍了关系型数据库中常见的几种关联关系,并介绍了Hibernate是如何利用注解对实体类进行映射的。总的来说,单向的关联关系和双向的关联关系有一个最本质的区别,具有双向关联关系的两张表,各自都存在对对方的引用,也就是说可以互相访问的。而单向的关联关系则永远只有一方可以访问到另一方。

当读者在实际的项目开发中使用到这些关联关系的时候,想必对于Hibernate的映射操作会有更加深刻的认识。总结不到之处,望指出!