一些重要概念
ForeignKey的理论这里就不再赘述了,总之就是一对一,一对多和多对一,还有多对多关系三种。还有一个Cascade级联操作的概念,都是数据库的传统艺能了。
通过外键取数据有两种风格,一是Eager模式,即一次取出全部数据;二是Lazy模式,即需要用的时候再获取。
还有两种查询关系,之前一般叫正查和反查。A表(外键在A表)查B叫做Uni-Directional,通过B查A叫做Bi-Directional。
剩下来有两个比较大的概念:Entity Class的生命周期,以及Cascade操作的设置
Entity Class的生命周期
在开始继续操作之前,先来看一下Entity Class的生命周期:
操作 | 说明 |
---|---|
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表中的数据产生错误,但反过来想要不级联删除,就必须解除外键关系。