前面说了Hibernate的单表映射,由于是实体类和数据表之间一对一的映射,所以比较简单。现在就来说说多表映射,这需要涉及到多个实体类和数据表之间的关系。因此稍微复杂一点。
建立实体类
我建立了两个实体类,一个作者类,一个文章类,其他方法都忽略了,就留下了注解。作者类如下:
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NaturalId()
private String username;
@Column(nullable = false)
private String password;
@Column
private String nickname;
@Column(name = "register_time")
@Temporal(TemporalType.DATE)
private Date registerTime;
}
文章类如下:
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column
private String title;
@Column
@Lob
private String content;
@ManyToOne(targetEntity = Author.class)
@JoinColumn(foreignKey = @ForeignKey(name = "FK_AUTHOR_ID"))
private Author author;
@Column(name = "create_time")
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
@Column(name = "modify_time")
@Temporal(TemporalType.TIMESTAMP)
private Date modifyTime;
}
文章实体类用到了另一个注解Lob
,表示SQL中的大对象。Hibernate会自动根据所注解的对象生成合适的SQL语句,如果Lob
注解到了字符串上,Hibernate会生成CLOB类型对象;如果注解到了byte[]数组之类的上面,就会生成BLOB类型的对象。
ManyToOne
上面的Article类中应用了一个ManyToOne注解。一个作者可以写很多篇文章,所以文章和作者的关系正是多对一。这个注解表示的也正是这种外键关系。如果我们查看一下MySQL的表生成语句,会发现Article表是这个样子的:
CREATE TABLE `article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`content` longtext,
`create_time` datetime DEFAULT NULL,
`modify_time` datetime DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`author_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_AUTHOR_ID` (`author_id`),
CONSTRAINT `FK_AUTHOR_ID` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
上面的文章实体类还应用了另一个注解JoinColumn
,这个注解用来控制数据库外键的行为。我这里是用来修改外键约束的名称。其他的使用方法需要查看官方文档。
@JoinColumn(foreignKey = @ForeignKey(name = "FK_AUTHOR_ID"))
这样,一个基本的外键映射就建立好了。但是有时候还不能满足需求,这样的话就需要双向的映射了。
单向的OneToMany
在介绍这种映射之前,我们先建立一个评论实体类,多余的内容省略了。
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@ManyToOne
private Author author;
@Lob
private String content;
@Column(name = "create_time")
@Temporal(TemporalType.TIMESTAMP)
private Date createTime;
}
建立好评论类之后,我们就可以建立文章类和评论类之间的多对一关系了。可以注意到我在author字段上应用了ManyToOne注解。本来也应该有一个应用ManyToOne注解的article字段来表示评论所属的文章,但是为了演示单向的OneToMany映射,所以我故意不添加这个文章属性。
有的同学可能想到了,多对一注解应用到字段上没有问题。但是一对多注解,如何应用到普通字段上呢。所以,这里需要一个集合。我们在文章实体类中添加如下一段,对应的Getter省略了:
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
这样就建立了评论集合和评论实体类的单向一对多映射。对于单向一对多映射,Hibernate会建立一个映射表,比如这里就会建立一个article_comment表,表的内容就是两张表的主键。orphanRemoval指定当出现孤立数据时是否删除孤立数据。cascade指定了级联操作的类型,这里使用ALL允许所有操作。指定了ALL之后,我们就可以通过直接在Article类中添加评论,级联地更新comment表。CascadeType还有另外几个值,这里就不再细述了。
单向的一对多映射并不高效,如果删除了某文章的某评论,Hibernate进行的操作是这样:首先删除关联表中该文章关联的所有评论,然后再将其他评论添加回关联表中,最后,根据orphanRemoval决定是否删除评论表中孤立的评论。
双向的OneToMany
理解了单向OneToMany之后,很容易就能理解双向OneToMany了。两个实体类一边需要使用ManyToOne注解,另外一边的集合类使用OneToMany注解。需要注意在双向注解中,OneToMany需要额外一个参数,mappedBy,指定ManyToOne注解那一边的属性名,这样Hibernate才会明白这是一个双向注解。
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
定义了双向注解之后,Hibernate不会再生成一个映射表,而是直接控制外键。因此比单向映射更高效。
OneToOne
一对一映射也是一种常用的映射关系。比方说我们要实现用户头像的功能。由于用户上传的头像文件大小可大可小,因此不能放在用户表中。这时候就需要一个头像表,这个表中每个头像和用户表中的每个用户就是一一对应的关系。
一对一关系也存在单向和双向的。首先我们看看单向映射。首先需要一个头像实体类:
@Entity
public class Avatar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Lob
private byte[] avatar;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public byte[] getAvatar() {
return avatar;
}
public void setAvatar(byte[] avatar) {
this.avatar = avatar;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Avatar avatar1 = (Avatar) o;
return id == avatar1.id &&
Arrays.equals(avatar, avatar1.avatar);
}
@Override
public int hashCode() {
return Objects.hash(id, avatar);
}
@Override
public String toString() {
return "Avatar{" +
"id=" + id +
'}';
}
}
然后需要更新Author类:
@OneToOne
private Avatar avatar;
这样单向一对一映射就完成了。使用这种方法建立的底层数据库,和使用ManyToOne是一样的。看一下数据表,就会发现这样建立出来的用户表存在一个外键,指向头像表。但是仔细考虑一下两张表的关系,头像是依附于用户存在的,所以外键应该是头像表的,指向用户表。这样就需要使用双向一对一映射。
首先需要更新头像类,添加一对一映射。
@OneToOne
private Author author;
作者类同样需要更新,一旦使用双向映射,就需要添加mappedBy属性。这里添加cascade以便可以级联更新头像表。
@OneToOne(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private Avatar avatar;
如果查看生成的数据表的话,就会发现,这次外键生成在了头像表一边。
ManyToMany
有了一对一、一对多、多对一映射的概念之后,多对多就很容易理解了。以上面我们建立的作者、文章、评论实体类为例,我们如果添加一个标签类,一个标签下可以存在多篇文章;一篇文章也可以有多个标签,这样就实现了一个多对多映射。要实现多对多映射,必须要有一个关联表。另外需要注意的是,使用多对多映射时,不能把级联属性指定为CascadeType.DELETE或者CascadeType.ALL,我们应该不希望在删除一篇文章的标签时,同时将该标签下的所有文章都删除吧?
另外Hibernate的多对多映射存在一个问题,就是和单向一对多一样,删除一个关联,需要先删除所有关联,然后将其他的重新插入。所以,一般情况下我们不能使用多对多映射,而是建立一个中间类,然后使用双向一对多映射将要关联的类分别和中间类映射。这就比较麻烦了,所以我就不写了。