一些重要概念
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类来体现关系。
具体的做法是:
- 给InstructorDetail添加一个新的成员变量,用于引用对应的Instructor类。
- 为这个变量添加getter和setter方法
- 添加@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表中的数据产生错误,但反过来想要不级联删除,就必须解除外键关系。