persist,save和merge,update之间的不同,我们该怎么使用?

java技术文章

2018-11-20

129

0

JPA和Hibernate提供了不同的方法去持久化新实体和更新存在的实体。你可以选择JPA'S persist,merge方法和Hibernate's的save和update方法。

它似乎是2对两个相同的方法,你也能使用persist和save去存储一个新实体,和使用merge和update去存储改变一个数据库中的实体。那为什么许多开发者想知道该怎么选择使用哪一个。让我们仔细查看这两个方法不同之处吧。

特别感谢Steve Ebersole提供反馈和对Hibernate's隐藏实现独到的见解。

Entity状态-瞬态
在讲解这个4个方法之前,我需要给你快速介绍JPA's实体生命周期状态。

 

如果一个实体存在(attached)当前的持久上下文中,它有着被管理的生命周期状态。

那就意味着它有一条记录映射到数据库中。你的持久化生成器生成需要的插入和更新sql语句去执行所有的改变。一个管理实体也存储到一级缓存里面。

当你创建了一个新实体,它表示瞬时状态,它保持这个状态直到你从当前持久上下文中分离实体。在下个章节,我将显示你怎样能使用JPA's persist和Hibernate’s保存方法。同样的一个实体处在瞬时状态,它没有映射到一条数据库记录和没有被任何持久化上下文管理。

在分离生命周期状态下的实体不再被持久化上下文管理。那样的情况是你关闭了持久化上下文或者你明确的从持久化上下文中分离了实体,在最后一个章节,我将更详细的说明你怎样使用JPA's merge和hibernate's update方法重新关联这些实体。

最后一个生命周期状态是removed.这些实体在预先删除之前是前面状态管理的。删除实体在这次提交范围之外,因此我没有太多关于它的讲解。你能通过调用EntityManager接口上的remove方法删除一个实体。

持久化一个新实体使用persist或者save
当你创建一个新实体对象,它是处在瞬时状态下。它没有映射到任何数据库记录。

Author a = new Author();
a.setFirstName("Thorben");
a.setLastName("Janssen");

你需要关联实体到持久化上下文,所以它变成可管理的和持久化到数据库中,你能选择使用JPA‘s persist或者Hibernate's save方法。两个方法看起来似乎是一样的,但是它们有一些不同。

规则说明书和所有权API

最明显的地方是JPA规则说明书定义的是persist方法。你能用所有的JPA实现使用它。save方法,换句话说,是hibernate指定的。因此它不能使用其它JPA实现。

但是这个只是相关的,如果你想使用其它的JPA实现替换hibernate,可以使用Eclipse Line或者OpenJPA

返回类型和执行SQL语句
另一个不同之处是这两个方法他们的返回类型。JPA's persist方法返回void和Hibernate's save方法返回一个带有主键key的实体。

这个看起来像是有很大的不同,尤其当你更仔细看看Hibernate's javadoc和JPA说明文档。
1、hibernate's save方法的帮助文档的状态先生成一个主键值:
Persist获取瞬时实例,先设定生成一个ID标识。
javadoc Session.save(entity);
2、你没有找到任何信息关于这个JPA说明文档。当主键值被设定时它没有被定义。因此在调用persist方法和flush持久化上下文之间的时候,持久化提供者能在任何时间内实例化。
在大多情况下,如果你在调用save或者persist它没有任何不同.Hibernate使用实体类名称和主键值存储实体在一级缓存里面。因此,在执行persist方法时需要一个主键值。

在大多情况下,当你调用persist或者save方法时,Hiberante在必要的时候会立即生成主键值和触发一个SQL语句
但是除了一种情况之外,如果你使用IDENTITY 策略和在没有一个活动的事务或者有FlushMode.MANUAL的情况下试图持久化一个实体,在这种情况下,Hibernate推迟执行SQL插入语句和创建一个缓存主键值.但是如果你调用save方法,Hiberante立即执行插入语句并且从数据库中返回一个主键值。

你能接收到save方法返回的值

Author a = new Author();
a.setFirstName("Thorben");
a.setLastName("Janssen"); 
Long id = (Long) em.unwrap(Session.class).save(a);

或者如果你使用JPA's persist方法时,你能通过getter方法获取管理实体的主键值

Author a = new Author();
a.setFirstName("Torben");
a.setLastName("Janssen");
 
em.persist(a);
 
Long id = a.getId();

Hibernate在调用persist或者save方法时执行相同的sql语句,选择那个和什么时候怎么做依赖于你的主键生成策略。

如果你通过代码设置了一个主键,例如一个自然数ID标识,当你刷新持久化上下文时,Hiberante执行一个SQL插入语句。

14:08:34,979  INFO TestPersistSaveMerge:237 - Save entity
14:08:35,052  INFO TestPersistSaveMerge:240 - Commit transaction
14:08:35,123 DEBUG SQL:92 - 
    insert
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)

用IDENTITY生成主键策略

如果你使用IDENTITY策略去生成主键值,当你调用save或persist方法从数据库获取主键值,Hibernate需要执行插入INSERT语句.

14:09:28,264  INFO TestPersistSaveMerge:237 - Save entity
14:09:28,336 DEBUG SQL:92 - 
    insert
    into
        Author
        (firstName, lastName, version) 
    values
        (?, ?, ?)
14:09:28,354  INFO TestPersistSaveMerge:240 - Commit transaction

使用SEQUENCE策略生成主键

如果你使用SEQUENCE策略,Hibernate执行一个select SQL语句从数据库序列获取下一个值,Hibernate接着延迟执行INSERT语句,直到刷新持久化上下文。在这个例子中,当事务提交的时候执行刷新操作。

14:10:27,994  INFO TestPersistSaveMerge:237 - Save entity
14:10:28,002 DEBUG SQL:92 - 
    select
        nextval ('hibernate_sequence')
14:10:28,042  INFO TestPersistSaveMerge:240 - Commit transaction
14:10:28,096 DEBUG SQL:92 - 
    insert
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)

使用table生成策略

你不应该使用TABLE策略,因为在主键table上它需要低级别的行锁,并且扫描不是很好。无论如何如果你使用这个策略,Hibernate执行一个select SQL查询语句从数据库中获取下一个主键值,并且写入一个新主键到数据库table中。对于一个新实体它延迟执行一个INSERT插入语句直到刷新持久化上下文中。

14:11:17,368  INFO TestPersistSaveMerge:237 - Save entity
14:11:17,482 DEBUG SQL:92 - 
    select
        tbl.next_val 
    from
        hibernate_sequences tbl 
    where
        tbl.sequence_name=? for update
            of tbl
14:11:17,531 DEBUG SQL:92 - 
    insert
    into
        hibernate_sequences
        (sequence_name, next_val)  
    values
        (?,?)
14:11:17,534 DEBUG SQL:92 - 
    update
        hibernate_sequences 
    set
        next_val=?  
    where
        next_val=? 
        and sequence_name=?
14:11:17,584  INFO TestPersistSaveMerge:240 - Commit transaction
14:11:17,655 DEBUG SQL:92 - 
    insert
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)


那该选择那一个呢?

你可能认为save和persist方法表现上有一些不同,因为在这里JPA说明文档和hibernate方法说明文档之间有一些不同。

但是大多数情况下,当你查看内部实现的时候,所有这些不同之处可以忽略。这里仅仅存在2个的个别情况,一个是hibernate可能延迟获取主键值,一个是方法返回类型和支持其他JPA的实现。

对于大多数应用程序,如果你在Hibernate save方法返回类型获取生成主键值或者从你的主键值属性获取值,它们并没有什么不同。同样的,如果你不需要继承持久化上下文和在一个活动的事务中执行所有数据库操作,我推介你使用JPA's的persist方法。

更新一个游离实体

当你关闭当前持久化上下文或者通过调用在接口EntityManager接口上的clear或者detach方法明确的删除一个实体。那就意味着它不在存储在一级缓存中并且Hibernate将不在复制任何发生的改变到数据中。

你可以使用Hibernate's update或者JPA's merge方法去用持久化上下文关联一个游离实体。在这样做之后,Hibernate基于实体属性值更新数据库。

UPDATE和MERGE方法的效果似乎是一样的,但是在下一章节,你将看到它们之间有一些重要的不同。

JPA's merge方法
JPA’s merge方法复制一个游离实体到同一个管理的实体。因此,Hibernate执行一个SELECT查询语句从数据库获取一个管理实体。如果持久化上下文已经存在一个管理的实体实例,Hibernate将替换已经存在的实例。它将负责所有的属性值到管理实体并返回给调用者。

Author managedAuthor = em.merge(a);

在活动的SQL语句日志之后,在日志中你能看到执行的SELECT和UPDATE语句。

11:37:21,172 DEBUG SQL:92 - 
    select
        books0_.bookId as bookId1_2_0_,
        books0_.authorId as authorId2_2_0_,
        book1_.id as id1_1_1_,
        book1_.fk_author as fk_autho6_1_1_,
        book1_.format as format2_1_1_,
        book1_.publishingDate as publishi3_1_1_,
        book1_.title as title4_1_1_,
        book1_.version as version5_1_1_,
        author2_.id as id1_0_2_,
        author2_.firstName as firstNam2_0_2_,
        author2_.lastName as lastName3_0_2_,
        author2_.version as version4_0_2_ 
    from
        BookAuthor books0_ 
    inner join
        Book book1_ 
            on books0_.authorId=book1_.id 
    left outer join
        Author author2_ 
            on book1_.fk_author=author2_.id 
    where
        books0_.bookId=?
11:37:21,180  INFO TestPersistSaveMerge:82 - Before commit
11:37:21,182 DEBUG SQL:92 - 
    update
        Author 
    set
        firstName=?,
        lastName=?,
        version=? 
    where
        id=? 
        and version=?

当Hibernate下一时刻刷新持久化上下文,它的脏检查机制将检查所有的管理实体。如果它发现合并操作改变了实体的属性值,它将触发需要的UPDATE sql语句。

当你使用JPA‘s merge方法时,这里有一个重要的细节。Hibernate复制游离实体的属性值到管理实体。在当前session中的实体上,将覆盖你执行的任何改变。

Hibernate 更新方法

Hibernate 更新方法不触发一个select 查询语句,它仅仅关联实体到当前持久化上下文中。对比JPA's merge方法,通过调用update方法,你不能错过任何改变。如果持久化上下文已经包含了一个管理实体的实例,并且你想去执行UPDATE方法,它将抛出一个异常。

em.unwrap(Session.class).update(a);

当hibernate执行下一步刷新时,它不执行任何脏检查,因为这个是不可能的,因为hibernate没有从数据中读取最后一个版本的实体。它仅仅执行了一个UPDATE SQL语句关联实体。

11:38:28,151  INFO TestPersistSaveMerge:121 - Before commit
11:38:28,153 DEBUG SQL:92 - 
    update
        Author 
    set
        firstName=?,
        lastName=?,
        version=? 
    where
        id=? 
        and version=?


当实体和对应的数据库记录保持同一条数据时,缺少脏检查会造成一个不必要的UPDATE更新语句。如果你的DBA注册一个update触发时这个可能造成一些问题。在这个章节,你能使用注解@SelectBeforeUpdate注解你的实体。

@Entity
@SelectBeforeUpdate
public class Author { ... }

它告诉Hibernate选择实体,并且在生成一个UPDATE更新语句时执行一个脏检查,同样你能看到日志输出,UPDATE方法的行为现在类似于JPA’s的merge方法。

19:08:16,530  INFO TestPersistSaveMerge:121 - Before commit
19:08:16,531 DEBUG SQL:92 - 
    select
        author_.id,
        author_.firstName as firstNam2_0_,
        author_.lastName as lastName3_0_,
        author_.version as version4_0_ 
    from
        Author author_ 
    where
        author_.id=?
19:08:16,592 DEBUG SQL:92 - 
    update
        Author 
    set
        firstName=?,
        lastName=?,
        version=? 
    where
        id=? 
        and version=?

但是在这两个方法之间有重要的不同。当你调用update方法,Hibernate将根据你提供的方法参数选择一个实体。但是当你调用JPA‘s方法时,Hibernate将选择所有带有CascadeType.MERGE注解的实体。因此,如果你关联多条实体数据时,你应该选择JPA’s的merge方法。

那该选择哪一个呢?

对于这个问题没有一般的回答。如你所见,两个方法有它的优势或者劣势,你需要根据你自己的情况来决定使用哪一个方法,比如说:Hibernate在触发执行UPDATE语句之前你需要选择一个实体。并且在这样的情况下,你也需要考虑你的实体结构的深度和提供feching行为潜在的性能。

欢迎访问:www.hongfu951.com博客,查看更多文章

发表评论

全部评论:0条

鸿福951

努力打造一个好用的webui

热评文章

推荐文章