Hibernate多表映射

前面说了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的多对多映射存在一个问题,就是和单向一对多一样,删除一个关联,需要先删除所有关联,然后将其他的重新插入。所以,一般情况下我们不能使用多对多映射,而是建立一个中间类,然后使用双向一对多映射将要关联的类分别和中间类映射。这就比较麻烦了,所以我就不写了。

上一篇:计算机BIOS的简单设置


下一篇:Qt网络编程之二