JPA 之 Hibernate EntityManager 使用指南

Hibernate EntityManager 专题

参考:

基本概念及获得 EntityManager 对象

基本概念

在使用持久化工具的时候,一般都有一个对象来操作数据库,在原生的Hibernate中叫做Session,在 JPA 中叫做EntityManager,在MyBatis中叫做SqlSession,通过这个对象来操作数据库。

EntityManager是 JPA 中用于增删改查的接口,连接内存中的 java 对象和数据库的数据存储。Hibernate EntityManager是围绕提供JPA编程接口实现的Hibernate Core的一个包装,支持JPA实体实例的生命周期,并允许用标准的Java Persistence查询语言编写查询。

EntityManager称为实体管理器,它由EntityManagerFactory所创建。EntityManagerFactory,作为EntityManager的工厂,包含有当前O-R映射的元数据信息,每个EntityManagerFactory,可称为一个持久化单元(PersistenceUnit),每个持久化单元可认为是一个数据源的映射(所谓数据源,可理解为一个数据库,可以在应用服务器中配置多个数据源,同时使用不同的PersistenceUnit来映射这些数据源,从而能够很方便的实现跨越多个数据库之间的事务操作!)

PersistenceContext,称为持久化上下文,它一般包含有当前事务范围内的,被管理的实体对象(Entity)的数据。每个EntityManager,都会跟一个PersistenceContext相关联。PersistenceContext中存储的是实体对象的数据,而关系数据库中存储的是记录,EntityManager正是维护这种OR映射的中间者,它可以把数据从数据库中加载到PersistenceContext中,也可以把数据从PersistenceContext中持久化到数据库,EntityManager通过Persist、merge、remove、refresh、flush等操作来操纵PersistenceContext与数据库数据之间的同步!

EntityManager是应用程序操纵持久化数据的接口。它的作用与hibernate session类似。为了能够在一个请求周期中使用同一个session对象,在hibernate的解决方案中,提出了currentSession的概念,hibernate中的current session,可以跟JTA事务绑定,也可以跟当前线程绑定。在hibernate中,session管理着所有的持久化对象的数据。而在EJB3中,EntityManager管理着PersistenceContext,PersistenceContext正是被管理的持久化对象的集合。

在 Java EE 环境下,一个 JTA 事务通常会横跨多个组件的调用(比如多个 EJB 组件的方法调用)。这些组件需要能够在单个事务范围内访问到同样的PersistenceContext。为了满足这种情况的需要,当EntityManager被注入或通过 JNDI 被查询的时候,它的 PersistenceContext 将会在当前事务范围内自动传播,引用到同一个 Persistence unit 的EntityManager将使用同样的 PersistenceContext。这可以避免在不同的组件之间传递EntityManager引用。

通过容器来传递PersistenceContext,而不是应用程序自己来传递EntityManager。这种方式(由容器管理着PersistenceContext,并负责传递到不同的EntityManager)称为容器管理的实体管理器(Container-Managed EntityManager),它的生命周期由容器负责管理,编程人员不需要考虑EntityManger的连接,释放以及复杂的事务问题等。

有一种不常见的情况是,应用程序自身需要独立访问PersistenceContext。即每次创建一个EntityManager都会迫使创建一个新的PersistenceContext。这些PersistenceContext即使在同一个事务范围内也不会跟其它EntityManager共享!这个创建过程可以由EntityManagerFactory的createEntityManager方法来创建。这被称为应用管理的实体管理器(application-managed entity manager)。

获得EntityManager对象

常用方式:SpringBoot容器托管对象方式:

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

对象注入:

@Autowired
private EntityManager entityManager;

实体状态和转换

在这里插入图片描述


实体状态详解:

  • 临时状态

    实际上就是new了一个普通的 JavaBean 对象。

  • 托管状态

    临时状态在调用 persist() 后,即可将一般的 JavaBean 做为了托管状态的Bean,该Bean的任何属性改动都会牵涉到数据库记录的改动。

    一旦该记录flush到数据库之后,并且事务提交了,那么此对象不在持久化上下文中,即:变为了游离(没人管的孩子)状态了。

    在游离状态的时候调用更新、刷新方法后,游离状态对象就变为了在持久化上下文的托管状态了。

    通过管理器的find方法,将实体从数据库查询出来后,该实体也就变为了托管形态。

  • 持久化状态

    当处在托管状态的实体Bean被管理器flush了,那么就在极短暂的时间进入了持久化状态,事务提交之后,立刻变为了游离状态。可以把持久化状态当做实实在在的数据库记录。

  • 游离状态

    游离状态就是提交到数据库后,事务commit后实体的状态,因为事务已经提交了,此时实体的属性任你如何改变,也不会同步到数据库,因为游离是没人管的孩子,不在持久化上下文中。

  • 销毁对象

    一般要删除一个持久化对象的时候都是先find出来,之后调用remove方法删之,此时这个对象就是销毁对象,实际上就是瞬时对象的另一种形态罢了。


常用的 API

SELECT、DELETE

SELECT

Ø find() :返回指定的 OID 对应的实体类对象,如果这个实体存在于当前的持久化环境,则返回一个被缓存的对象;否则会创建一个新的 Entity, 并加载数据库中相关信息;若 OID 不存在于数据库中,则返回一个 null。

Ø getReference()

<T> T find(Class<T> entityClass, Object primaryKey);
<T> T find(Class<T> entityClass, Object primaryKey, Map<String, Object> var3);
<T> T find(Class<T> entityClass, Object primaryKey, LockModeType var3);
<T> T find(Class<T> entityClass, Object primaryKey, LockModeType var3, Map<String, Object> var4);

<T> T getReference(Class<T> entityClass, Object primaryKey);

// 参数说明:
    entityClass	// 被查询的实体类类型
    primaryKey	// 待查找实体的主键值

异同:

  1. 当在数据库中没有找到记录时,find()方法会返回null,

    而getReference() 方法会抛出javax.persistence.EntityNotFoundException异常,

  2. 调用getReference()方法,返回的其实并不是实例对象,而是一个代理。当你要使用实体时,才会真正的调用查询语句来查询实例对象

  3. 另外getReference()方法不保证 entity Bean已被初始化。

  4. 如果传递进getReference()或find()方法的参数不是实体Bean,都会引发 IllegalArgumentException


DELETE

Ø Remove()

void remove(Object entity);
  1. 如果级联关系cascade=CascadeType.ALL,在删除person 时候,也会把级联对象删除。把cascade属性设为cascade=CascadeType.REMOVE 有同样的效果。

    Person person = em.find(Person.class, 2);
    em.remove (person);
    
  2. 如果传递进remove ()方法的参数不是实体Bean,会引发一个IllegalArgumentException

    remove()方法不能移除游离对象,只能移除持久化对象

    Order order = new Order();
    order.setId(140);
    entityManager.remove(order);
    

    上面这段代码会抛出异常,因为order是自己创建的对象,也就是游离对象。必须这样写:

    Order order = new Order();
    order = entityManager.find(Order.class140);
    entityManager.remove(order);
    

    这段代码中的order是从数据库中获取的,也就是持久化对象

    hibernate的delete()方法,只要对象有Id,就可以删除


INSERT、UPDATE

INSERT

Ø persist(): 将临时状态的实体持久化到数据库

void persist(Object entity);

persist方法:使对象由临时状态变为托管状态。进而变为持久化状态,就是执行INSERT操作。

  1. 如果传递进persist()方法的参数不是实体Bean,会引发IllegalArgumentException
  2. 和hibernate的save()方法有些不同:当Entity实体类中设置了主键自动生成时,如果传入对象有id值,则会抛出异常

特殊场景及处理方案:

  • 特殊场景:Entity上使用自动生成ID值,但有些又需要插入的主键值是指定的ID值,而非自动生成的ID值

  • 处理方案1:Bean主键为Null,persist()后自动生成ID值,然后再使用QueryDsl-Jpa的update()方法,根据自动生成的ID值为条件,更新新增的实体的ID值为指定值

    @Test
    @Transactional
    @Rollback(false)
    public void addByEntityManager(){
          
          
        User bean = User.builder()
            .addressee("孙六")
            .build();
        
        entityManager.persist(u1);
    
        QUser entity = QUser.user;
        long execute = queryFactory.update(entity)
            .set(entity.id, "11111")
            .where(entity.id.eq(bean.getId()))
            .execute();
        
        entityManager.flush();
        entityManager.clear();
    }
    
  • 处理方案2:使用Querydsl-SQL中,SqlQueryFactory.insert()方法。


UPDATE

Ø 当实体正在被容器管理,即托管状态,你可以调用实体的set方法对数据进行修改,在容器决定flush时(这个由Container自行判断),更新的数据才会同步到数据库,而不是在调用了set方法对数据进行修改后马上同步到数据库。如果希望修改后的数据马上同步到数据库,可以调用 EntityManager.flush() 方法。

// 使用示例
@tranational
publicvoid updatePerson() {
    
    
    Person person = entityManager.find(Person.class, 1);
    person.setName("lihuoming");	//方法执行完后即可更新数据
    entityManager.merge(person);
}

Ø Merge

<T> T merge(T entity);
  1. 传入的对象没有id

    在这种情况下,调用merge方法,将返回一个新的对象(有id),并对这个新的对象执行insert操作。

  2. 传入的对象有id,entityManager的缓存中没有该对象,数据库中没有该记录:

    在这种情况下,调用merge方法,将返回一个新的对象,并对该对象执行insert操作。

    注意:如果Entity的主键设置的是自动生成,则新对象的id并不是原传入对象的id,而是自动生成的(比如自增长的id)。(其实和情况1的结果是一样的)

  3. 传入的对象有id,entityManager的缓存没有该对象,数据库中有该记录

    在这种情况下,调用merge方法,将会从数据库中查询对应的记录,生成新的对象,然后将传入的对象复制到新的对象,最后执行update操作。

    简单来说,就是更新操作。

  4. 传入的对象有id,entityManager的缓存有该对象

    在这种情况下,调用merge方法,JPA会把传入的对象赋值到entityManager的缓存中的对象,然后对entityManager缓存中的对象执行update操作。(和情况3的结果一样)

总结:执行merge时,如果实体ID为空,则进行insert操作。 如果有ID则进行update操作。


flush()、clear()

flush()

将实体的改变立刻刷新到数据库中

当EntityManager对象在一个session bean 中使用时,它是和服务器的事务上下文绑定的。EntityManager 在服务器的事务提交时提交并且同步它的内容。

在一个session bean 中,服务器的事务默认地会在调用堆栈的最后提交(如:方法的返回)。

// 例子1:在方法返回时才提交事务
public void updatePerson(Person person) {
    
    
    try {
    
    
        Person person = em.find(Person.class, 2);
        person.setName("lihuoming");
        em.merge(person);
        //后面还有众多修改操作
    } catch (Exception e) {
    
    
    	e.printStackTrace();
    }
	//更新将会在这个方法的末尾被提交和刷新到数据库中
}

默认只在当事务提交时才将改变更新到数据库中,容器将所有数据库操作集中到一个批处理中,这样就减少了代价昂贵的与数据库的交互。

当调用 persist( ),merge( )或 remove( ) 这些方法时,更新并不会立刻同步到数据库中,直到容器决定刷新到数据库中时才会执行,默认情况下,容器决定刷新是在 “相关查询” 执行前或事务提交时发生。

当然 “相关查询” 除 find() 和 getreference() 之外,这两个方法是不会引起容器触发刷新动作的,默认的刷新模式是可以改变的。


如果你需要在事务提交之前将更新刷新到数据库中,你可以直接地调用EntityManager.flush()方法。

ORM框架执行的一些更新数据库的方法,其实质是在更新缓存,只有调用了 flush() 后才会将缓存同步到数据库,即真正执行SQL语句,但是这时并没有真正将数据保存进数据库,需要事务commit后才能全部保存。一般 flush 后立刻就会进行事务的提交。

public void updatePerson(Person person) {
    
    
    try {
    
    
        Person person = em.find(Person.class, 2);
        person.setName("lihuoming");
        em.merge(person);
        em.flush();//手动将更新立刻刷新进数据库

    //后面还有众多修改操作
    } catch (Exception e) {
    
    
    	e.printStackTrace();
    }
}

clear()

分离所有当前正在被管理的实体

1.清除持久上下文环境,断开所有关联的实体。如果这时还有未提交的更新则会被撤消。

2.在处理大量实体的时候,如果你不把已经处理过的实体从EntityManager中分离出来,将会消耗你大量的内存。

3.调用EntityManager 的clear()方法后,所有正在被管理的实体将会从持久化内容中分离出来。

4.有一点需要说明下,在事务没有提交前(事务默认在调用堆栈的最后提交,如:方法的返回),如果调用clear()方法,之前对实体所作的任何改变将会丢失,所以建议在调用clear()方法之前先调用flush()方法保存更改。


JcreateQuery() – PQL

创建查询对象

除了使用 find() 或 getReference() 方法来获得Entity Bean之外,你还可以通过 JPQL 得到实体Bean。要执行 JPQL 语句,你必须通过 EntityManager 的 createQuery() 或 createNamedQuery() 方法创建一个Query 对象。

注:JPQL 没有插入语句。即不能执行insert语句。


Ø getResultList()

Query query = em.createQuery("select p from Person p where p.name=’黎明’");
List result = query.getResultList();

Iterator iterator = result.iterator();
while( iterator.hasNext() ){
    
    
//处理Person
}

Ø getSingleResult()

返回查询的第一条数据,可以进行强转,如:

// 查询数目:
Query query = em.createQuery("select count(1) from Person p");
Long num = (Long)query. getSingleResult ();

// 强转为实体:
Query query = em.createQuery("select p from Person p");
User user = (User)query. getSingleResult ();

Ø executeUpdate()

// 执行更新和删除操作,返回受影响的记录数。
Query query = em.createQuery("delete from Person");
int result =query.executeUpdate();	 //影响的记录数

Ø 关于 **JPQL 和SQL **中参数的问题:

  1.  使用标识符
    
Query query =em.createQuery("delete from Person p where p.name := name");
query.setParameter("name","张三");
int result =query.executeUpdate(); //影响的记录数
  1.  使用索引下标
    
Query query =em.createQuery("delete from Person p where p.id = ?1 ");
query.setParameter(1, "张三");
int result =query.executeUpdate(); //影响的记录数  

createNaiveQuery() – SQL

用法基本同createQuery(),只不过这里使用的不是 JPQL 而是SQL

Ø 将查询到的数据映射成实体:

Query query = em.createNativeQuery("select * from person", Person.class);
List result = query.getResultList();

if (result != null){
    
    
	Iterator iterator = result.iterator();
	while( iterator.hasNext() ){
    
    
		Person person= (Person)iterator.next();}
}

refresh()

如果怀疑当前被管理的实体已经不是数据库中最新的数据,则可以通过 refresh() 方法刷新实体,容器会把数据库中的新值重写进实体。这种情况一般发生在获取了实体之后,有人更新了数据库中的记录,这时需要得到最新的数据。当然再次调用 find() 或 getReference() 方法也可以得到最新数据,但这种做法并不优雅。

User user = em.find(User.class, 1);

//第二次同样的查询不会访问数据库
user = em.find(User.class, 1);

// 运行以上代码,发现调用了两次find,但是只执行了一次select语句,这是缓存导致的。

// 执行refresh()方法刷新缓存,容器会把数据库中的新值重写进实体。
User user = em.find(User.class, 1);
em.refresh(user);

其他方法

contains()

判断实体是否还在EntityManage的管理下,或者说是否属于当前持久上下文环境。

contains() 方法使用一个实体作为参数,如果这个实体对象当前正被持久化内容管理,返回值为true,否则为false。

如果传递的参数不是实体 Bean,将会引发一个IllegalArgumentException.

User user = em.find(User.class, 1);

if (em.contains(user)){
    
    
	//正在被持久化内容管理
}else{
    
    
	//已经不受持久化内容管理
}

getFlushMode ():

获取持久上下文环境的Flush模式。返回FlushModeType类的枚举值。


setFlushMode()

改变实体管理器的Flush模式

setFlushMode()的Flush模式有2种类型:AUTO 和 COMMIT。AUTO为默认模式。

// 改变实体管理器的Flush模式
em.setFlushMode(FlushModeType.COMMIT);
  • FlushModeType.AUTO

    默认情况下除了在事务提交时 flush,在进行查询时(除了 find() 和 getreference() 查询)也会进行一次 flush ,比如使用JPQL查询前会进行一次flush。

    使用场合:在大量更新数据的过程中没有任何查询语句(除了 find() 和 getreference() 查询)的执行。

  • FlushModeType.COMMIT

    刷新只有在事务提交时才发生,查询不触发。

    使用场合:在大量更新数据的过程中存在查询语句(除了find() 和 getreference() 查询)的执行。

其实上面两种模式最终反映的结果是:JDBC 驱动跟数据库交互的次数。

JDBC 性能最大的增进是减少JDBC 驱动与数据库之间的网络通讯。

FlushModeType.COMMIT 模式使更新只在一次的网络交互中完成,而 FlushModeType.AUTO 模式可能需要多次交互(触发了多少次Flush 就产生了多少次网络交互)


isOpen()

判断当前的实体管理器是否是打开状态


close()

关闭实体管理器。

之后若调用实体管理器实例的方法或其派生的查询对象的方法都将抛出 IllegalstateException 异常,除了 getTransaction 和 isOpen方法(返回 false)。

不过,当与实体管理器关联的事务处于活动状态时,调用 close() 方法后持久上下文将仍处于被管理状态,直到事务完成。


getTransaction()

返回资源层的事务对象。EntityTransaction实例可以用于开始和提交多个事务


EntityTransaction 接口用来管理资源层实体管理器的事务操作

通过调用实体管理器的getTransaction方法 获得其实例。

① begin ()

用于启动一个事务,此后的多个数据库操作将作为整体被提交或撤消。若这时事务已启动则会抛出 IllegalStateException 异常。

② commit ()

用于提交当前事务。即将事务启动以后的所有数据库更新操作持久化至数据库中。

③ rollback ()

撤消(回滚)当前事务。即撤消事务启动后的所有数据库更新操作,从而不对数据库产生影响。

④ setRollbackOnly ()

使当前事务只能被撤消。

⑤ getRollbackOnly ()

查看当前事务是否设置了只能撤消标志。

⑥ isActive ()

查看当前事务是否是活动的。如果返回true则不能调用begin方法,否则将抛出 IllegalStateException 异常;如果返回 false 则不能调用 commit、rollback、setRollbackOnly 及 getRollbackOnly 方法,否则将抛出 IllegalStateException 异常。

需要注意的是:

// 在JPA里面,先需要 getTransaction,再 begin 
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();

// 在 hibernate 里面呢,直接 begin,然后进行 commit 
EntityTransaction transaction = session.beginTransaction();
transaction.commit();

JPA 调用存储过程

参考:https://www.cnblogs.com/zhuang229/p/12227938.html

定义存储过程及调用方式

定义一个简单的存储过程

传入一个int参数,返回这个参数+1

CREATE DEFINER=`root`@`localhost` PROCEDURE `plus1inout`(IN ARG INT, OUT res INT)
BEGIN 
    SET res = ARG + 1;
END

注意:

  1. IN参数个数没有限制。

  2. 如果out参数类型为sys_refcursor,那么最好只定义这 一个out参数(JPA API限制);

    sys_refcursor 类型的 out 参数,在 JPA 中统一注册为 Void.class 类型,参数模式定义为 ParameterMode.REF_CURSOR;

    使用getResultList()方法获取游标fetch到的多行数据,返回结果为List<Object[]>,一个Object[]对应一行数据。

  3. 如果使用Oracle 存储包,只需在定义存储过程名字时加个对应的package名前缀即可(例如:包名.存储过程名)。


JPA调用存储过程的两种方式

  • 基于Entity实体类,在实体类上使用@NamedStoredProcedureQuery注解(需要数据库中有对应的表,可自动映射结果集)
    • EntityManager创建createNamedStoredProcedureQuery,传参调用
    • SpringDataJpa中Repository自定义方法,传参调用
  • 直接使用EntityManager进行自定义(不需要数据库中有对应的表,需要自己处理结果集)
    • EntityManager创建StoredProcedureQuery对象,注册 IN/OUT 参数模式

实体类(使用注解声明存储过程)

@NamedStoredProcedureQuery注解(解析详见注解目录)

  • name为唯一名称,调用时使用;procedureName 为存储过程名;

  • @Entity注解必须有

  • @Entity要求必须有主键id属性(存储过程可返回id任意值即可)

  • @Entity要求必须对应数据库表必须存在(JPA表检查用)(若用EntityManager调用存储过程,此条存疑)

import lombok.Data;
import javax.persistence.*;

@Data
@Entity
// 存储过程使用了注解@NamedStoredProcedureQuery,并绑定到一个随意一个JPA表
@NamedStoredProcedureQueries({
    
    
        @NamedStoredProcedureQuery(
            name = "User.plus1", 
            procedureName = "plus1inout", 
            parameters = {
    
    
                @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class),
                @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) 
            }),
        @NamedStoredProcedureQuery(
            name = "User.mytest", 
            procedureName = "mytest") })
class user{
    
    
    @Id
    private Integer id;
    private String name;
    private String age;
}

EntityManager直接调用存储过程

@Autowired
private EntityManager em;

@Test
public void test01(){
    
    
    StoredProcedureQuery query = em
        .createStoredProcedureQuery("plus1inout")	// 创建StoredProcedureQuery对象,传入被调用的存储过程名称
        .registerStoredProcedureParameter("ARG", Integer.class, ParameterMode.IN)	// 注册参数
        .registerStoredProcedureParameter("res", Integer.class, ParameterMode.OUT)
        .setParameter("ARG", 20);	// 传参

    query.execute();	// 执行存储过程调用

    String result = query.getOutputParameterValue("res").toString();	// 获取存储过程中的返回值
    System.out.println(result);
}

调用基于实体类注解的存储过程

使用EntityManager调用基于Entity实体类注解的存储过程。实体类详见 JPA调用存储过程

@Autowired
private EntityManager em;

@Test
public void test02(){
    
    
    StoredProcedureQuery query = em
        // 创建NamedStoredProcedureQuery对象,传入实体类上@NamedStoredProcedureQuery注解中name的值
        .createNamedStoredProcedureQuery("proKQAttendanceRecord")
        
        // IN模式的参数可以在实体类上注解,此处相应注释掉
        // .registerStoredProcedureParameter("PRM_ID", Integer.class, ParameterMode.IN)
        
        // 使用EntityManager调用基于Entity实体类注解的存储过程时,OUT或INOUT模式的参数,必须要在此处注册,并删掉原实体类上相应的参数注解。不然运行报错。
        .registerStoredProcedureParameter("PRM_APPCODE", Integer.class, ParameterMode.OUT)
        .registerStoredProcedureParameter("PRM_ERRMSGE", Integer.class, ParameterMode.OUT)
        .setParameter("PRM_ID", attId);		// 传参

    query.execute();

    List resultList = query.getResultList();
    Object code = query.getOutputParameterValue("PRM_APPCODE");
    Object msg = query.getOutputParameterValue("PRM_ERRMSGE");
}

猜你喜欢

转载自blog.csdn.net/footless_bird/article/details/129294444