关于JPA EntityManager、实体生命周期和CascadeType的学习记录

198 阅读7分钟

因为对JPA的EntityManager的用法、实体生命周期以及关联关系里的CascadeType都不熟,所以系统地学习了下,记录一下学习过程。

用到的JPA实现是Hibernate

搭建项目

因为对hibernate不熟,依赖和配置基本是照抄了这里

  1. 依赖
<dependencies>
  <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-core -->
  <dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.1.7.Final</version>
  </dependency>
</dependencies>
  1. 配置文件,位置在src/main/java/META-INF/persistence.xml
<persistence
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
    version="2.1">
  <persistence-unit name="test">
    <description> Hibernate JPA Configuration Example</description>
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>net.javaguides.hibernate.entity.Student</class>
    <properties>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" />
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/dev" />
      <property name="javax.persistence.jdbc.user" value="root" />
      <property name="javax.persistence.jdbc.password" value="" />
      <property name="hibernate.show_sql" value="false" />
      <property name="hibernate.hbm2ddl.auto" value="update" />
    </properties>
  </persistence-unit>
</persistence>
  1. 实体类
@Entity
@Table(name = "person")
public class Person {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String name;

  public Person(String name) {
    this.name = name;
  }

  public Person() {}

  // getters and setters and toString()
}

关于实体生命周期

一个实体对于PersistenceContext来说有这几种状态:

  1. Transient

    对象不存在持久化上下文中时处于transient状态,例如new出来的对象。

  2. Managed

    对一个transient状态的对象调用persistmerge方法,该对象会被加入持久化上下文,处于managed状态。

    find方法返回的对象也是managed状态。

    当对象处于managed状态时,持久化上下文会追踪它的属性变化,当调用flush或者当前事务提交的时候,hibernate会生成update语句更新数据库。

  3. Detached

    对一个处于managed状态的对象调用detach方法,该对象会从持久化上下文中移除,处于detached状态。

    当对象处于detached状态时,持久化上下文不会追踪它的属性变化。

    当出于某种理由需要detach一个对象,建议先flush一下将对象的属性修改同步到数据库(如果有的话),不然对该对象的属性修改都会丢失。

  4. Removed

    对一个处于managed状态的对象调用remove方法,就能使它处于removed状态。

    在flush或事务提交的时候,hibernate会生成delete语句从数据库中删除该对象。

PersistenceContext:持久化上下文,位于应用里,一个对后端数据库的“一级缓存”,用于管理持久化对象,持久化对象就是一些Java对象,一个对象就是后端数据库里一条记录的映射。

EntityManager:用来管理PersistenceContext的,提供对持久化对象的CRUD操作。

persist和merge

persist

public static void main(String[] args) {
  EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
  EntityManager em = emf.createEntityManager();
  EntityTransaction tx = em.getTransaction();
  tx.begin();

  Person person = new Person("a");
  em.persist(person);
  System.out.println("id >>>>> " + person.getId()); // 控制台打印 id >>>>> 1

  tx.commit();
  em.close();
  emf.close();
}

此时查看数据库可以看到添加了一条记录。

+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+

关于实体生命周期:new一个对象出来的时候,它属于transient状态,也就是不存在于持久化上下文中,没有被框架管理,调用persist方法后,它处于managed状态,被加入持久化上下文中。

当一个对象是managed状态时,在事务内部对它的属性做修改,最终修改会被自动同步到数据库里,例如:

tx.begin();

Person person = new Person("a");
em.persist(person); // person此时处于managed状态
person.setName("b");

tx.commit(); // commit后,person的name属性的修改会自动同步到数据库里,数据库里的name字段将会是b
+----+------+
| id | name |
+----+------+
|  1 | b    |
+----+------+

merge

EntityManager.merge(Object entity)

调用merge,hibernate会先执行select查询是否存在主键相同的行,如果存在,则更新该行,否则插入新行。

对一个全新的对象调用merge时,效果与persist相同,都会在数据库插入一条新行,并使它进入managed状态。

tx.begin();

Person person = new Person("b");
em.merge(person);

tx.commit();
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    | 新插入的行
+----+------+

如果数据库中已存在主键与该对象相同的行,则merge会更新该行。

tx.begin();

Person person = new Person("b");
person.setId(1L);
em.merge(person); // merge会将该对象的属性覆盖掉数据库里那一行的属性
// 调用persist的话会报错 detached entity passed to persist

tx.commit();
执行前
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
+----+------+
执行后
+----+------+
| id | name |
+----+------+
|  1 | b    | merge修改了id为1的行
|  2 | b    |
+----+------+

merge还可以将detached的对象重新attach到持久化上下文中

tx.begin();

Person b = em.find(Person.class, 1L);
em.detach(b);
b.setName("c");
em.merge(b); // 将b重新attach到持久化上下文中,最后事务commit的时候,b的name属性修改会被同步到数据库

tx.commit();
执行前
+----+------+
| id | name |
+----+------+
|  1 | b    |
|  2 | b    |
+----+------+
执行后
+----+------+
| id | name |
+----+------+
|  1 | c    |
|  2 | b    |
+----+------+

另外如果持久化上下文中已存在一个主键相同的对象,调用merge返回的对象将会是上下文里的那个对象。

tx.begin();

Person person = em.find(Person.class, 1L);
System.out.println("found person: " + person.hashCode());

Person another = new Person("b");
another.setId(1L);
System.out.println("another person: " + another.hashCode());

Person merged = em.merge(another);
System.out.println("merged person: " + merged.hashCode());

tx.commit();
/* 输出结果
  found person: 35419258
  another person: 802628402
  merged person: 35419258 与find到的对象是同一个对象
*/

find

public static void main(String[] args) {
  EntityManagerFactory emf = Persistence.createEntityManagerFactory("test");
  EntityManager em = emf.createEntityManager();
  EntityTransaction tx = em.getTransaction();
  tx.begin();

  Person person = em.find(Person.class, 1L); // 查询id为1的Person记录
  System.out.println(person); // Person{id=1, name='a'}

  tx.commit();
  em.close();
  emf.close();
}

调用find方法查询出来的对象已经是处于managed状态,在事务内部修改它的属性也会自动同步到数据库里。

tx.begin();

Person person = em.find(Person.class, 1L); // 查询id为1的Person记录
System.out.println(person); // Person{id=1, name='a'}
person.setName("b"); // 修改person的name属性

tx.commit(); // commit后,person的name属性的修改会自动同步到数据库里,数据库里的name字段将会是b
执行前
+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+
执行后
+----+------+
| id | name |
+----+------+
|  1 | b    |
+----+------+

detach

EntityManager.detach(Object entity)

detached状态下,对该对象的属性的修改不会被同步到数据库。

如果查询出对象后不希望对它的属性的修改被同步到数据库,可以调用detach方法

tx.begin();

Person person = em.find(Person.class, 1L); // 查询id为1的Person记录
System.out.println(person); // Person{id=1, name='a'}
person.setName("b"); // 修改person的name属性
em.detach(person);

tx.commit(); // commit后,person的name属性的修改不会同步到数据库里,数据库里的name字段仍然是a

如果在detach之前调用了EntityManager.flush()方法,那么flush之前对该对象做的修改也会被同步到数据库:

tx.begin();

Person person = em.find(Person.class, 1L);
person.setName("b");
em.flush(); // flush会立即将持久化上下文中所有实体的状态同步到数据库
em.detach(person);
person.setName("c"); // 这一次修改不会影响到数据库

tx.commit();
执行前
+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+
执行后
+----+------+
| id | name |
+----+------+
|  1 | b    |
+----+------+

refresh

EntityManager.refresh(Object entity)

再次查询数据库,更新对象的状态。

数据库:

+----+------+
| id | name |
+----+------+
|  1 | a    |
+----+------+
Person person = em.find(Person.class, 1L);
System.out.println("name: " + person.getName());
System.out.println("waiting...");
Thread.sleep(10000); // 在等待期间去手动修改数据库的数据 update person set name = 'b' where id = 1;
em.refresh(person);
System.out.println("after refresh: " + person.getName());

打印结果

name: a
waiting...
after refresh: b

上面代码的执行并没有开启事务,如果开启了事务的话,由于MySQL默认的隔离级别为可重复读,看不到其他事务提交的更改,所以再次refresh到的数据跟一开始find到的数据是一样的。

remove

这个用法很简单,就是从数据库中删除数据

tx.begin();

Person person = em.find(Person.class, 1L);
em.remove(person); // 传入的对象不可以是transient或者detached状态

tx.commit();

关于CascadeType

新建类

@Entity
@Table(name = "address")
public class Address {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String address;

  @OneToOne(mappedBy = "address")
  private Person person;

  public Address() {}

  public Address(String address) {
    this.address = address;
  }
}

CascadeType.PERSIST

当一个对象被持久化时,其关联对象也会被持久化。

Person类

@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
tx.begin();

Person person = new Person("老麦");
person.setAddress(new Address("洛圣都"));
em.persist(person);

tx.commit();

此时person和address都会被持久化。

+----+------+------------+
| id | name | address_id |
+----+------+------------+
|  1 | 老麦 |          1 |
+----+------+------------+

+----+---------+
| id | address |
+----+---------+
|  1 | 洛圣都  |
+----+---------+

CascadeType.MERGE

级联传递merge操作

Person类

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;

测试代码

tx.begin();

Person person = new Person("Tommy");
person.setId(1L);
Address address = new Address("Vice city");
address.setId(1L);
person.setAddress(address);
em.merge(person); // 调用merge方法,address也会被merge

tx.commit();

执行前

+----+------+------------+
| id | name | address_id |
+----+------+------------+
|  1 | 老麦 |          1 |
+----+------+------------+

+----+---------+
| id | address |
+----+---------+
|  1 | 洛圣都  |
+----+---------+

执行后

+----+-------+------------+
| id | name  | address_id |
+----+-------+------------+
|  1 | Tommy |          1 |
+----+-------+------------+

+----+-----------+
| id | address   |
+----+-----------+
|  1 | Vice city |
+----+-----------+

如果不设置CascadeType.MERGE,那么上面的代码执行结果是只有person表的数据被改变,address表的数据保持原样。

CascadeType.DETACH

级联传递detach操作

Person类

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH})
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;

测试代码

tx.begin();

Person person = em.find(Person.class, 1L); // managed状态
Address address = person.getAddress(); // managed状态
person.setName("Niko");
address.setAddress("Liberty City");
em.detach(person);
// detach之后,person和address都处于detached状态

tx.commit();

由于设置了CascadeType.DETACH,以上代码执行后数据库里的记录不会被更改,因为person以及它的address都被detached了。

CascadeType.REFRESH

级联传递refresh操作

测试代码

Person person = em.find(Person.class, 1L);
Address address = person.getAddress();
System.out.println("Person: " + person.getName() + ", Address: " + address.getAddress());
System.out.println("waiting...");
Thread.sleep(20000);
/*
  在等待期间,手动修改数据库的数据
  update person set name = 'Niko' where id = 1;
  update address set address = 'Liberty City' where id = 1;
*/
em.refresh(person); // refresh person的同时,address也被refresh了
System.out.println("Person: " + person.getName() + ", Address: " + address.getAddress());

打印结果

Person: Tommy, Address: Vice City
waiting...
Person: Niko, Address: Liberty City

CascadeType.REMOVE

与前面的同理,级联传递remove操作,当一个对象被从数据库移除时,其关联对象也会被从数据库移除。