因为对JPA的EntityManager的用法、实体生命周期以及关联关系里的CascadeType都不熟,所以系统地学习了下,记录一下学习过程。
用到的JPA实现是Hibernate
搭建项目
因为对hibernate不熟,依赖和配置基本是照抄了这里。
- 依赖
<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>
- 配置文件,位置在
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>
- 实体类
@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来说有这几种状态:
-
Transient
对象不存在持久化上下文中时处于
transient状态,例如new出来的对象。 -
Managed
对一个
transient状态的对象调用persist或merge方法,该对象会被加入持久化上下文,处于managed状态。由
find方法返回的对象也是managed状态。当对象处于
managed状态时,持久化上下文会追踪它的属性变化,当调用flush或者当前事务提交的时候,hibernate会生成update语句更新数据库。 -
Detached
对一个处于
managed状态的对象调用detach方法,该对象会从持久化上下文中移除,处于detached状态。当对象处于
detached状态时,持久化上下文不会追踪它的属性变化。当出于某种理由需要detach一个对象,建议先flush一下将对象的属性修改同步到数据库(如果有的话),不然对该对象的属性修改都会丢失。
-
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操作,当一个对象被从数据库移除时,其关联对象也会被从数据库移除。