【SSH進階之路】【十】hibernate5 註解映射【2】 一對多單向關聯

在上一篇文章裏,我們從端方向一端建立關聯關係,完成了從文章到作者的關聯關係建立,但在實際的博客網站中,用戶肯定還需要獲取自己所寫的文章,這時可以建立用戶(一)對文章(多)的單向關聯映射。

先來看我們的一方配置實例

package com.chenhao.hibernate.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;


/**
 * 
 * @author chen_hao
 *
 */

@Entity//聲明當前類爲hibernate映射到數據庫中的實體類
@Table(name = "t_user")//聲明在數據庫中自動生成的表名爲t_user
public class User {


    @Id//聲明此列爲主鍵,作爲映射對象的標識符
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;
    @OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,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.chenhao.hibernate.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")
@Entity
public 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.chenhao.hibernate.Hibernate;

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.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.chenhao.hibernate.model.Article;
import com.chenhao.hibernate.model.User;

public class ArticleTest {


        private static ApplicationContext ac;
        private static SessionFactory sessionFactory;
        private Session session;
        private Transaction transaction;


        @BeforeClass//在測試類初始化時調用此方法,完成靜態對象的初始化
        public static void before(){

            ac = new ClassPathXmlApplicationContext("spring-datasource.xml");
            sessionFactory = (SessionFactory) ac.getBean("sessionFactory");

        }

        @Before//每一個被註解Test方法在調用前都會調用此方法一次
        public void setup(){//建立針對我們當前測試方法的的會話和事務
            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(){

            transaction.commit();//提交事務,主要爲了防止在測試中已提交事務,這裏又重複提交
            session.clear();
            session.close();
            sessionFactory.close();
        }
        @After//在類銷燬時調用一次
        public void after(){

        }

}
執行測試方法,我們會看到控制檯打印下列sql語句: 

Hibernate: create table hibernate_sequence (next_val bigint)
Hibernate: insert into hibernate_sequence values ( 1 )
Hibernate: insert into hibernate_sequence values ( 1 )
Hibernate: create table t_article1 (id integer not null, content varchar(255), primary key (id))
Hibernate: create table t_user (id integer not null, name varchar(255), primary key (id))
Hibernate: create table t_user_t_article1 (User_id integer not null, articles_id integer not null, primary key (User_id, articles_id))
Hibernate: alter table t_user_t_article1 drop constraint UK_f0cdt34bsf0g8q5fuqs8noq7g
Hibernate: alter table t_user_t_article1 add constraint UK_f0cdt34bsf0g8q5fuqs8noq7g unique (articles_id)
Hibernate: alter table t_user_t_article1 add constraint FKl8de4n0kh83owph35kv7ffs3s foreign key (articles_id) references t_article1 (id)
Hibernate: alter table t_user_t_article1 add constraint FKpn1wj1pw945nafx73grpjjt65 foreign key (User_id) references t_user (id)
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: insert into t_user (name, id) values (?, ?)
Hibernate: insert into t_article1 (content, id) values (?, ?)
Hibernate: insert into t_article1 (content, id) values (?, ?)
Hibernate: insert into t_article1 (content, id) values (?, ?)
Hibernate: insert into t_user_t_article1 (User_id, articles_id) values (?, ?)
Hibernate: insert into t_user_t_article1 (User_id, articles_id) values (?, ?)
Hibernate: insert into t_user_t_article1 (User_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: select user0_.id as id1_1_0_, user0_.name as name2_1_0_ from t_user user0_ where user0_.id=?
Hibernate: select articles0_.User_id as User_id1_2_0_, articles0_.articles_id as articles2_2_0_, article1_.id as id1_0_1_, article1_.content as content2_0_1_ from t_user_t_article1 articles0_ inner join t_article1 article1_ on articles0_.articles_id=article1_.id where articles0_.User_id=?
Hibernate: delete from t_user_t_article1 where User_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_user where id=?

可見,它的刪除順序是:先清楚中間表數據->再刪除多方文章4數據->最後清楚一方用戶數據

如果我們不像使用中間表,而想像上一篇配置多對一關聯那樣,在文章表生成user_id,我們就要一方配置@JoinColumn屬性,對應屬性的實例如下:


@OneToMany(cascade = CascadeType.ALL,fetch = FetchType.LAZY,targetEntity = Article.class,orphanRemoval = true)//用戶作爲一方使用OneToMany註解
@JoinColumn(name = "user_id")//添加了這個註解
private Set<Article> articles;//文章作爲多方,我們使用Set集合來存儲,同時還能防止存放相同的文章

修改對應表名,讓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);

會得到如下打印信息:
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集合來存儲,同時還能防止存放相同的文章

先看看我們數據庫的初始記錄信息:

+—-+———–+ 
| 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);//更新用戶

運行測試代碼,我們會看到控制檯僅輸出一條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;

再次運行我們的測試代碼,控制檯輸出:

Hibernate: update t_article2 set user_id=null where user_id=? and id=? 
Hibernate: delete from t_article2 where id=? 
我們的文章記錄就對應被刪除了,這就是去除“孤兒”的含義,在實際開發中,正如我們的身份證總是從屬於某個人的,如果失去這種從屬關係,身份證就沒有意義而可以去除了。

1. @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集合來存儲,同時還能防止存放相同的文章

其中:
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);

會發現我們的用戶、文章對應關係都在中間表中建立起來了:

mysql> select * from t_user_articles;
+———+————+
| user_id | article_id |
+———+————+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
+———+————+
3 rows in set (0.00 sec)
使用中間表的好處是對原來兩張表的結構不對造成任何影響。尤其是在一些老項目中我們可以不修改既定的表結構(事實上在一個項目古老龐大到一定程度就很難去改)、以不侵入原來表的方式構建出一種更清淅更易管理的關係。當然缺點是我們的我們需要維護多一張表,一旦中間表多了,維護起來會愈加麻煩。但綜合來看,我們顯然更推薦用中間表的方式來完成配置。

2.eqauls和hashCode方法

在我們開始配置一對多的一方時,我們通過Set來和多方建立關係,其中提到的一點是可以防止多方相同對象出現。這個相同對應我們數據庫中就是某些屬性列相同,比如:對於Article,如果id和content在兩條記錄中都一樣,我們就可以認爲兩條記錄是一致的,因此會自動去重那麼我們來判斷它們的重複關係呢?這個時候就要通過重寫hashCode和equals方法了。
示例如下:

//重寫equals比較對象相等
@Override
public 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內部,是如何判斷兩個對象是否相等的:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章