Spring 13 Hibernate 一对一关系操作

作者 柚爸

一些重要概念

ForeignKey的理论这里就不再赘述了,总之就是一对一,一对多和多对一,还有多对多关系三种。还有一个Cascade级联操作的概念,都是数据库的传统艺能了。

通过外键取数据有两种风格,一是Eager模式,即一次取出全部数据;二是Lazy模式,即需要用的时候再获取。

还有两种查询关系,之前一般叫正查和反查。A表(外键在A表)查B叫做Uni-Directional,通过B查A叫做Bi-Directional。

剩下来有两个比较大的概念:Entity Class的生命周期,以及Cascade操作的设置

Entity Class的生命周期

在开始继续操作之前,先来看一下Entity Class的生命周期:

Entity生命周期
操作 说明
Detach 分离 如果一个Entity分离的话,就不会和Hibernate的session发生关联
Merge 合并 如果一个对象和session分离,merge就是重新将这个entity对象附加到session上
Persist 持久化 将新的实例准备提交,下一次刷新或者提交的时候会保存到数据库里。这个状态也叫managed state。我自己管这个叫预备持久化状态。
Remove 删除 下一次刷新或者提交的时候会将entity对象从数据库中删除,我自己管这个叫预备移除状态
Refresh 刷新 重新载入或者与数据库同步数据,防止内存中数据对象和数据库中数据不一致。

所谓生命周期,其实就是不同状态的Entity。以Student类为例:

try {
    session.beginTransaction();

    //新建一个Student对象,此时这个对象的状态叫做New/Transient
    Student newS = new Student("New3", "Vegas3", "bestha3.com");

    //此时调用.save()方法,并不是真正保存入数据库,调用save方法只是将这个newS对象附加到了session上,此时状态是Persist或者叫managed state
    session.save(newS);

    //在newS为受控对象的时候,修改newS对象的值,会影响最后写入数据库的值
    newS.setLastName("After save");
    newS.setEmail("after_save@gmail.com");

    //执行commit,会将此时newS实际的值写入数据库,然后解除newS与session的绑定
    session.getTransaction().commit();

    //此时已经解除绑定,再修改Student的值,不影响数据库内的结果
    System.out.println(newS);
    newS.setEmail("after commit");
    System.out.println(newS);
} finally {
    factory.close();
}

获取时候的生命周期:

try{
    session.beginTransaction();

    //从session中获取的对象立刻就会与session绑定,就是Merge状态
    Student student = session.get(Student.class, 8);

    //delete之后,还没有写入数据库,处于预备移除
    session.delete(student);

    //修改值也不影响数据库中的值,因为不再写入
    student.setEmail("after get");
    student.setFirstName("after get");
    student.setLastName("after get");

    //commit之后,student与sesssion解除绑定
    session.getTransaction().commit();


    //这是自行编写的把此时的student再写回数据库的方法,这么操作之后,可以发现原来的对象被删除,而数据库里多了一行修改后的数据。
    tryUpdate(student);

}finally {
    factory.close();
}

Cascade的设置

Cascade就是级联操作,即删除外键所在的行,是否也删掉对应关联的数据。在Hibernate中,Cascade级联操作有如下几种设置:

级联操作类型 解释
PERSIST 如果对象被persisted/saved,级联对象也进入persisted/saved状态
REMOVE 如果对象被removed/deleted,级联对象也被解除与session的关联或者被删除
REFRESH 如果对象更新,级联对象也被更新
DETACH 如果对象被解除与session的关联,级联对象也被解除关联
MERGE 如果对象被恢复关联,级联对象也恢复关联
ALL 包含上述所有级联类型

配置级联关系是在@OneToOne的参数里,比如这样:

@OneToOne(cascade = CascadeType.ALL)

注意,级联操作必须显式指定,如果不加参数,默认是不进行任何级联操作。也可以同时配置多个参数,用一个数组即可,比如:

@OneToOne(cascade = {CascadeType.DETACH,CascadeType.PERSIST})

Uni-Directional的一对一操作

先来看如果通过外键所在的类,操作本类和外键关联的类。

在MySQL创建两个表,一个是instructor,一个是instructor_detail:

DROP SCHEMA IF EXISTS `hb-01-one-to-one-uni`;

CREATE SCHEMA `hb-01-one-to-one-uni`;

use `hb-01-one-to-one-uni`;

SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `instructor_detail`;

CREATE TABLE `instructor_detail` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `youtube_channel` varchar(128) DEFAULT NULL,
  `hobby` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;


DROP TABLE IF EXISTS `instructor`;

CREATE TABLE `instructor` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `instructor_detail_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_DETAIL_idx` (`instructor_detail_id`),
  CONSTRAINT `FK_DETAIL` FOREIGN KEY (`instructor_detail_id`) REFERENCES `instructor_detail` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

SET FOREIGN_KEY_CHECKS = 1;

可以看到,instructor表有一个外键字段叫做instructor_detail_id,这个字段关联到instructor_detail表的id字段。所谓关联,就是指这个键的值只能够是instructor_detail表id字段中存在的值。

之后来创建两个Entity Class,对于instructor_detail表来说,就是一个普通的表:

import javax.persistence.*;

@Entity
@Table(name = "instructor_detail")
public class InstructorDetail {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "youtube_channel")
    private String youtubeChannel;

    @Column(name = "hobby")
    private String hobby;

    public InstructorDetail() {

    }

    public InstructorDetail(String youtubeChannel, String hobby) {
        this.youtubeChannel = youtubeChannel;
        this.hobby = hobby;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getYoutubeChannel() {
        return youtubeChannel;
    }

    public void setYoutubeChannel(String youtubeChannel) {
        this.youtubeChannel = youtubeChannel;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    @Override
    public String toString() {
        return "InstructorDetail{" +
                "id=" + id +
                ", youtubeChannel='" + youtubeChannel + '\'' +
                ", hobby='" + hobby + '\'' +
                '}';
    }
}

但是对于instructor表来说,外键那一列就有需要注意的地方:

import javax.persistence.*;

@Entity
@Table(name = "instructor")
public class Instructor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @OneToOne
    @JoinColumn(name = "instructor_detail_id")
    private InstructorDetail instructorDetail;

    public Instructor() {

    }

    public Instructor(String firstName, String lastName, String email, InstructorDetail instructorDetail) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.instructorDetail = instructorDetail;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public InstructorDetail getInstructorDetail() {
        return instructorDetail;
    }

    public void setInstructorDetail(InstructorDetail instructorDetail) {
        this.instructorDetail = instructorDetail;
    }

    @Override
    public String toString() {
        return "Instructor{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", email='" + email + '\'' +
                ", instructorDetailId='" + instructorDetail + '\'' +
                '}';
    }
}

首先需要注意的是,虽然知道外键是一个int类型的列,但是这里不能够将变量设置为int类型,因为在ORM里,通过外键取到的是一个数据对象,是一个关联的表的一行或者多行数据(这里我们一对一,就是一行,所以用的直接就是InstructorDetail类)。

然后通过@OneToOne注解,告诉Hibernate这个对应关系,然后通过@JoinColumn(name = "instructor_detail_id")注解告诉Hibernate这不是一个普通的列,而是有关联关系的列。

(如果在数据库中建立强的一对一关系,那么需要将外键字段设置为unique,这里我们没有这么做,就是让Hibernate去管理这个关系。)这样Hibernate在操作数据的时候就知道这是一个一对一的外键关系。

由于新创建了两个表和数据库hb-01-one-to-one-uni,因此必须修改一下Hibernate的配置:

<property name="connection.url">jdbc:mysql://localhost:3306/hb-01-one-to-one-uni?useSSL=false&serverTimezone=UTC</property>

修改好之后,来编写向数据库中添加记录的MainApp.java:

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class MainApp {
    public static void main(String[] args) {
        SessionFactory factory = new Configuration().configure("hibernate.cfg.xml").addAnnotatedClass(Instructor.class).addAnnotatedClass(InstructorDetail.class).buildSessionFactory();
        Session session = factory.getCurrentSession();

        //创建新的InstructorDetai和Instructor对象,并把外键字段设置好关联
        InstructorDetail instructorDetail = new InstructorDetail("youtube.com/minkolee", "coding and game");
        //Entity Class里的外键关联是另外一张表的一个对象,切记,这是ORM的核心映射关系
        Instructor instructor = new Instructor("Minko", "Lee", "minkolee@gmail.com", instructorDetail);

        try {
            //写入数据库
            session.beginTransaction();

            //由于设置过级联类型是ALL,这里会将instructor对象和instructorDetail对象一并设置为persist/save状态
            session.save(instructor);

            //提交事务,级联的两个对象会被一并写入
            session.getTransaction().commit();
        }finally {
            factory.close();
        }
    }
}

这样就完成了同时写入关联对象的操作。如果数据库中外键被设置为不能是null,则在保存每个Instructor对象的时候,必须同时保存或者已经存在对应的InstructorDetail对象。

级联删除的代码只需要获取有外键的对象,然后删除即可,对应的级联对象会一起删除:

try {
    session.beginTransaction();
    Instructor instructor = session.get(Instructor.class, 2);
    session.delete(instructor);
    session.getTransaction().commit();
}finally {
    factory.close();
}

注意,如果删除没有外键的InstructorDetail类,由于数据库的约束限制,是无法删除的。所以要获取有外键的对象来删除。

至于修改和查询就和单独查询一个对象一样,先获得Instructor对象,然后通过属性就可以获得对应的InstructorDetail对象,然后进行操作。

Bi-Directional的一对一操作

对这个例子来说,Bi-Directional就是指先获取InstructorDetail对象,然后获取对应的Instructor对象。由于InstructorDetail对象中没有直接指向Instructor对象的属性,所以用上一种方式直接设置属性是不行的,必须采取另外一种方式。

由于Entity类依然是数据表的映射,因此无需更改数据表,而是通过更改Java代码,修改InstructorDetail类来体现关系。

具体的做法是:

  1. 给InstructorDetail添加一个新的成员变量,用于引用对应的Instructor类。
  2. 为这个变量添加getter和setter方法
  3. 添加@OneToOne注解

来修改InstructorDetail类,添加新成员变量和getter及setter方法:

//mappedBy的值instructorDetail指的是Instructor类中的属性instructorDetail
@OneToOne(mappedBy = "instructorDetail", cascade = CascadeType.ALL)
private Instructor instructor;

public Instructor getInstructor() {
    return instructor;
}

public void setInstructor(Instructor instructor) {
    this.instructor = instructor;
}

这里最关键的是@OneToOne注解的mappedby属性,通过注解加在Instructor类型的变量上,然后指定mappedBy的值为instructorDetail,实际上是告诉Hibernate这里需要通过Instructor类的instructorDetail属性来获取对应的Instructor对象。

在查询的时候,Hibernate就会先定位到Instructor类的instructorDetail变量,然后看到上边有注解@JoinColumn,就会知道外键关系,进而通过InstructorDetail的id去寻找对应的Instructor对象。

这里也将级联操作设置为ALL。

来写一些代码获取对象,这里新建就没有什么意义了,因为要把两个对象互相设为引用对方,一般新建对象都是添加有外键的一方,这里先获取一下:

public class MainApp2 {
    public static void main(String[] args) {
        SessionFactory factory = new Configuration().configure("hibernate.cfg.xml").addAnnotatedClass(Instructor.class).addAnnotatedClass(InstructorDetail.class).buildSessionFactory();
        Session session = factory.getCurrentSession();

        Random rand = new Random();
        int pk = rand.nextInt(100) + 1;

        try {
            session.beginTransaction();
            //从主键获取一个InstructorDetail对象
            InstructorDetail instructorDetail = session.get(InstructorDetail.class, pk);
            //通过刚才自定义的键获取对应的Instructor对象
            Instructor instructor = instructorDetail.getInstructor();
            //显示两个对象
            System.out.println(instructorDetail);
            System.out.println(instructor);

            session.getTransaction().commit();
        }finally {
            factory.close();
        }
    }
}

再试验一下级联删除对象:

try {
    session.beginTransaction();

    InstructorDetail instructorDetail = session.get(InstructorDetail.class, pk);
    Instructor instructor = instructorDetail.getInstructor();

    System.out.println("要删除的对象是:" + instructor);

    session.delete(instructorDetail);

    session.getTransaction().commit();
} catch (Exception ex) {
    ex.printStackTrace();

} finally {
    session.close();
    factory.close();
}

可见删除InstructorDetail的同时也删除了对应的Instructor对象。这里修改了一下try-catch语句,由于没有关闭session和处理错误,所以会造成泄露,这样调整了一下就好了。

不级联操作

之前的操作都设置了全部级联操作,如果想仅删除InstructorDetail对象,但保留Instructor对象,该怎么做呢?这里需要了解一下不级联操作的设置。

先来修改一下InstructorDetail类里的instructors属性的级联设置:

@OneToOne(mappedBy = "instructorDetail", cascade = {CascadeType.DETACH,CascadeType.PERSIST,CascadeType.MERGE,CascadeType.REFRESH})
private Instructor instructor;

现在再获取一个InstructorDetail对象并且进行删除,可以看到对应的Instructor对象还在。

不使用级联删除通常用于只删除一个基础索引表的数据,而把相关数据保留之用,和级联操作一样,非级联操作也使用很广泛。

发现直接删除InstructorDetail对象报错,由于Instructor对象还指向InstructorDetail对象,所以必须在删除前解除对应关系。完整代码如下:

Random rand = new Random();
int pk = rand.nextInt(100) + 1;
//不进行级联删除
try {
    session.beginTransaction();

    InstructorDetail instructorDetail = session.get(InstructorDetail.class, pk);
    Instructor instructor = instructorDetail.getInstructor();

    System.out.println("要删除的对象是:" + instructor);

    //解除与要删除的InstructorDetail对象关联的Instructor对象指向这个InstructorDetail对象的外键引用。
    //有点绕,实际上就是将关联的对象的外键设置为空,这样等于只有ID指向I对象的一个变量,而I对象没有外键关联到ID对象了。
    instructorDetail.getInstructor().setInstructorDetail(null);

    session.delete(instructorDetail);

    session.getTransaction().commit();
} catch (Exception ex) {
    ex.printStackTrace();

} finally {
    session.close();
    factory.close();
}

这里如果不加上解除关系的那一行,就会报错,因为外键还关联着删除的对象,所以不能直接删除,否则外键里边就保留了一个错误的数值。

在Uni-Directional下删除Instructor对象就不用解除关系,因为删除Instructor对象不会导致InstructorDetail表中的数据产生错误,但反过来想要不级联删除,就必须解除外键关系。