hibernate5(10)注解映射[2]一对多单向关联

来源:互联网 发布:教师网络研修平台登录 编辑:程序博客网 时间:2024/06/01 13:12

在上一篇文章里,我们从端方向一端建立关联关系,完成了从文章到作者的关联关系建立,但在实际的博客网站中,用户肯定还需要获取自己所写的文章,这时可以建立用户(一)对文章(多)的单向关联映射。
先来看我们的一方配置实例

package com.zeng.model;import java.util.Set;import javax.persistence.CascadeType;import javax.persistence.Entity;import javax.persistence.FetchType;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.OneToMany;import javax.persistence.Table;@Entity//声明当前类为hibernate映射到数据库中的实体类@Table(name = "t_user")//声明在数据库中自动生成的表名为t_userpublic class User {    @Id//声明此列为主键,作为映射对象的标识符    @GeneratedValue(strategy = GenerationType.AUTO)    private Integer id;    private String name;    @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,mappedBy = "user",targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解    private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public Set<Article> getArticles() {        return articles;    }    public void setArticles(Set<Article> articles) {        this.articles = articles;    }    //重写hashcode方法提高比较效率    @Override    public int hashCode() {        final int prime = 31;        int result = 1;        result = prime * result + ((id == null) ? 0 : id.hashCode());        result = prime * result + ((name == null) ? 0 : name.hashCode());        return result;    }    //重写equals比较对象相等    @Override    public boolean equals(Object obj) {        if (this == obj)            return true;        if (obj == null)            return false;        if (getClass() != obj.getClass())            return false;        User other = (User) obj;        if (id == null) {            if (other.id != null)                return false;        } else if (!id.equals(other.id))            return false;        if (name == null) {            if (other.name != null)                return false;        } else if (!name.equals(other.name))            return false;        return true;    }}

下面是我们对应的多方配置

package com.zeng.model;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.Table;@Table(name = "t_article1")@Entitypublic class Article {    @Id    @GeneratedValue(strategy = GenerationType.AUTO)    private Integer id;    private String content;    public Integer getId() {        return id;    }    public void setId(Integer id) {        this.id = id;    }    public String getContent() {        return content;    }    public void setContent(String content) {        this.content = content;    }}

根据这些配置,我们来编写测试方法:

package com.zeng.test;import java.util.HashSet;import java.util.Set;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.Transaction;import org.junit.After;import org.junit.AfterClass;import org.junit.Before;import org.junit.BeforeClass;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;import com.zeng.model.Article;import com.zeng.model.User;public class Test2 {    private ApplicationContext ac;    private SessionFactory sessionFactory;    private Session session;    private Transaction transaction;    @BeforeClass//在测试类初始化时调用此方法,完成静态对象的初始化    public static void before(){    }    @Before//每一个被注解Test方法在调用前都会调用此方法一次    public void setup(){//建立针对我们当前测试方法的的会话和事务        ac = new ClassPathXmlApplicationContext("spring-datasource.xml");        sessionFactory = (SessionFactory) ac.getBean("sessionFactory");        session = sessionFactory.openSession();        transaction = session.beginTransaction();    }    //测试一对多单向关联    @Test    public void test2(){        User user = new User();        user.setName("oneObject");        Set<Article> articles = new HashSet<Article>();        for(int i = 0 ; i < 3;i ++){//添加三篇文章            Article article = new Article();            article.setContent("moreContent" + i) ;            articles.add(article);        }        user.setArticles(articles);//建立关联关系        session.save(user);//仅保存用户    }    @After//每一个被注解Test方法在调用后都会调用此方法一次    public void teardown(){        if(transaction.isActive()){//如果当前事务尚未提交,则            transaction.commit();//提交事务,主要为了防止在测试中已提交事务,这里又重复提交        }        session.clear();        session.close();        sessionFactory.close();    }    @After//在类销毁时调用一次    public void after(){    }}

执行测试方法,我们会看到控制台打印下列sql语句:

Hibernate: insert into t_user1 (name) values (?)
Hibernate: insert into t_article1 (content) values (?)
Hibernate: insert into t_article1 (content) values (?)
Hibernate: insert into t_article1 (content) values (?)
Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?)
Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?)
Hibernate: insert into t_user1_t_article1 (t_user1_id, articles_id) values (?, ?)
在前四句,我们看到在保存user对象时,级联保存了我们的文章对象,最后面三条信息又是什么?原来在我们没有设置@JoinColumn(具体使用方法请参考我的上篇文章)。那么在一对多的关联配置中,hibernate会默认帮我们生成中间表来完成两者的映射关系,查询数据库,我们会发现
mysql> select * from t_user1_t_article1;
+————+————-+
| t_user1_id | articles_id |
+————+————-+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
+————+————-+
3 rows in set (0.00 sec)

确实是通过中间表,将用户和文章关联起来了。
这时进行级联删除测试:

“`java
User user = (User) session.get(User.class, 1);
session.delete(user);

>我们会得到打印信息:>Hibernate: delete from t_user1_t_article1 where t_user1_id=?Hibernate: delete from t_article1 where id=?Hibernate: delete from t_article1 where id=?Hibernate: delete from t_article1 where id=?Hibernate: delete from t_user1 where id=?</font>可见,它的删除顺序是<font color=red>先清楚中间表数据->再删除多方文章4数据->最后清楚一方用户数据</font>如果我们不像使用中间表,而想像上一篇配置多对一关联那样,在文章表生成user_id,我们就要一方配置@JoinColumn属性,对应属性的实例如下:```java@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解@JoinColumn(name = "user_id")//添加了这个注解private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章<div class="se-preview-section-delimiter"></div>

修改对应表名,让hibernate重新在数据库中生成表,此时再运行我们的测试方法,会看到:

Hibernate: insert into t_user2 (name) values (?)
Hibernate: insert into t_article2 (content) values (?)
Hibernate: insert into t_article2 (content) values (?)
Hibernate: insert into t_article2 (content) values
Hibernate: update t_article2 set user_id=? where id=?
Hibernate: update t_article2 set user_id=? where id=?
Hibernate: update t_article2 set user_id=? where id=?

此时我们会看到,最后三行换成了更新我们的表属性值。从这里我们看出为了更新aritlce表中user_id对应值,我们额外使用多了三条数据,这是很不值的,会额外消耗数据库的性能,有没方法使得在插入文章表的同时插入user_id的值呢?查看hibernate源码,我们会发现这是因为user作为主动方,它处理关联对象时必须通过update来完成,如果我们想取消update,应该将user放弃主动,让另一方(多方)去维护,这又涉及到我们的一对多、多对一双向关联了,我们在下一篇文章再具体解决这一问题。

这个时候我们来测试级联删除:

User user = (User) session.get(User.class, 1);session.delete(user);<div class="se-preview-section-delimiter"></div>

会得到如下打印信息:
Hibernate: update t_article2 set user_id=null where user_id=?
Hibernate: delete from t_article2 where id=?
Hibernate: delete from t_article2 where id=?
Hibernate: delete from t_article2 where id=?
Hibernate: delete from t_user2 where id=?
注意到,它的删除顺序是:清除用户表和文章表的关联关系(这又是因为用户表作为主动方,它必须通过此方法来维护关联关系->然后清除多方文章信息->最后才删除我们的一方用户

上面我们基本完成了我们的测试工作,下面我们对配置属性加以分析:
1. 相对于上一篇我们提到的ManyToOne属性,OneToMany独有的属性有:mapperBy和orphanRemoval,mpperBy是指放弃维护级联关系,具体我们在双向关联中再详细分析,这里比较独特的属性是orphanRemoval
表面意思是去除孤儿,当一方不再关联多方某一实体A时,自动从数据库中删除A。下面来看实例测试,假如我们先将其设为false

@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = false)//用户作为一方使用OneToMany注解@JoinColumn(name = "user_id")private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章<div class="se-preview-section-delimiter"></div>

先看看我们数据库的初始记录信息:

+—-+———–+
| id | name |
+—-+———–+
| 2 | oneObject |
+—-+———–+
1 row in set (0.00 sec)

mysql> select * from t_article2;
+—-+————–+———+
| id | content | user_id |
+—-+————–+———+
| 6 | moreContent0 | 2 |
| 5 | moreContent1 | 2 |
| 4 | moreContent2 | 2 |
+—-+————–+———+
3 rows in set (0.00 sec)

接着开始我们的测试:

User user = (User) session.get(User.class,2);Article article = user.getArticles().iterator().next();//获取与用户有对应关系的一篇文章user.getArticles().remove(article);//从用户对应关系中清除出来session.update(user);//更新用户<div class="se-preview-section-delimiter"></div>

运行测试代码,我们会看到控制台仅输出一条sql语句:
Hibernate: update t_article2 set user_id=null where user_id=? and id=?

查询数据库,此时变成:

mysql> select * from t_article2;
+—-+————–+———+
| id | content | user_id |
+—-+————–+———+
| 6 | moreContent0 | 2 |
| 5 | moreContent1 | 2 |
| 4 | moreContent2 | NULL |
+—-+————–+———+
3 rows in set (0.00 sec)

现在我们先恢复测试前的数据:
mysql> update t_article2 set user_id = 2 where id = 4;
然后再将对应注解里的orphanRemoval设为true

@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)@JoinColumn(name = "user_id")private Set<Article> articles;<div class="se-preview-section-delimiter"></div>

再次运行我们的测试代码,控制台输出:

Hibernate: update t_article2 set user_id=null where user_id=? and id=?
Hibernate: delete from t_article2 where id=?
我们的文章记录就对应被删除了,这就是去除“孤儿”的含义,在实际开发中,正如我们的身份证总是从属于某个人的,如果失去这种从属关系,身份证就没有意义而可以去除了。

2. @JoinTable

在默认不使用@JoinColumn时,多对一关联中hibernate会为我们自动生成中间表,但如果我们像自己来配置中间表,就可以使用@JoinTable注解。它的实例配置如下:

@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)//用户作为一方使用OneToMany注解@JoinTable(name = "t_user_articles",inverseJoinColumns = {@JoinColumn(name = "article_id")},joinColumns = {@JoinColumn(name = "article_id")})private Set<Article> articles;//文章作为多方,我们使用Set集合来存储,同时还能防止存放相同的文章<div class="se-preview-section-delimiter"></div>

其中:
1. name为创建的中间表名称。
2. inverseJoinColumns指向对方的表,在这里指多方的表。
3. joinColumns指向自己的表,即一方的表,这些指向都是通过主键映射来完成的。

运行我们的测试代码:

User user = new User();user.setName("oneObject");Set<Article> articles = new HashSet<Article>();for(int i = 0 ; i < 3;i ++){    Article article = new Article();    article.setContent("moreContent" + i) ;    articles.add(article);}user.setArticles(articles);//建立关联关系session.save(user);<div class="se-preview-section-delimiter"></div>

会发现我们的用户、文章对应关系都在中间表中建立起来了:


mysql> select * from t_user_articles;
+———+————+
| user_id | article_id |
+———+————+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
+———+————+
3 rows in set (0.00 sec)

使用中间表的好处是对原来两张表的结构不对造成任何影响。尤其是在一些老项目中我们可以不修改既定的表结构(事实上在一个项目古老庞大到一定程度就很难去改)、以不侵入原来表的方式构建出一种更清淅更易管理的关系。当然缺点是我们的我们需要维护多一张表,一旦中间表多了,维护起来会愈加麻烦。但综合来看,我们显然更推荐用中间表的方式来完成配置

3. eqauls和hashCode方法

在我们开始配置一对多的一方时,我们通过Set来和多方建立关系,其中提到的一点是可以防止多方相同对象出现。这个相同对应我们数据库中就是某些属性列相同,比如:对于Article,如果id和content在两条记录中都一样,我们就可以认为两条记录是一致的,因此会自动去重那么我们来判断它们的重复关系呢?这个时候就要通过重写hashCode和equals方法了。
示例如下:

//重写equals比较对象相等@Overridepublic boolean equals(Object obj) {    if (this == obj)//如果地址引用相同,直接判断为相等        return true;    if (obj == null)//如果目标对象为null,直接判断不等        return false;    if (getClass() != obj.getClass())//两者类不一致        return false;    User other = (User) obj;    if (id == null) {        if (other.id != null)            return false;    } else if (!id.equals(other.id))//判断两者id是否都存在且相等        return false;    if (name == null) {        if (other.name != null)            return false;    } else if (!name.equals(other.name))//判断两者名字是否都存在且相等        return false;    return true;}

一般来说,我们重写equal方法就能判断两个对象是否相等了,为什么还要重写hashCode方法呢?主要是考虑到效率的问题,对于equals方法,当比较规则比较复杂的话就会比较耗时了,而hashCode为每一个对象生成一个散列码(通过一种神秘的算法,一般为关键属性乘以一个质数),避免了比较慢的运算。不过我们不能因为快就单凭hash码来判断两个对象是否相等,因为hashCode并不能保证能为每一个不同的对象生成唯一的散列码,所以可能会有两个hash码相同,但对象确实不一致的情况。不过我们知道的是如果连hash码都不一致,那两个对象肯定是不一致的。根据此思路,我们可以很好地理解在java内部,是如何判断两个对象是否相等的:
这里写图片描述

1 0
原创粉丝点击