只应合并时,JPA会持续多次

时间:2017-04-25 10:21:05

标签: java hibernate jpa

我有一种情况需要坚持物品的层次结构:

一个Call有一个Quote和一个Quote有几个QuoteFragments。 QuoteFragments拥有'由父母报价。

Quote已经存在于数据库中,但是Call和一个QuoteFrament是新的(在Quote中可能还有其他的QuoteFragments已经存在)

class Call {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @ManyToOne(fetch=FetchType.LAZY, cascade=CascadeType.ALL)
  @JoinColumn(name="contract_quote_id", nullable=true)
  protected Quote quote;

  @OneToOne(fetch=FetchType.LAZY, cascade={CascadeType.REFRESH})
  @JoinColumn(name="fragment_id", nullable=true)
  protected QuoteFragment fragment;
}

class Quote {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @OneToOne(fetch=FetchType.LAZY)
  @JoinColumn(name="customer_call_id", nullable=false)
  protected Call customerCall;

  @OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, mappedBy="parent")
  protected Set<QuoteFragment> fragments = new HashSet<>();
}

class QuoteFragment {
  @Id
  @Column(name="id")
  @GeneratedValue(strategy=GenerationType.AUTO)
  protected Long id;

  @ManyToOne(fetch=FetchType.EAGER)
  @JoinColumn(name="parent_id", nullable=false)
  protected Quote parent;

  @Column(name="valid_until", nullable=false)
  @Temporal(TemporalType.DATE)
  protected Date validUntil;
}

报价/调用关系中的onetoone / manytoone是因为当报价首次持久化时,它与第一次调用相关联。后续调用可能引用相同的引用,但引用将始终引用第一个调用。

我的问题:我找不到使用级联或单独的持久/合并操作的方法来阻止额外的QuoteFragment副本被持久化到数据库。

这是我目前的代码:

  Quote quote = getExistingQuote( ... );
  QuoteFragment fragment = new QuoteFragment();
  fragment.setParent( quote );
  quote.addFragment( fragment );

  Call call = new Call();
  call.setQuote( quote );
  call.setQuoteFragment( fragment );

  // doing other config stuff

  // later on at the persist stage..
  Quote existingQuote = call.getQuote();
  call.setQuote( null );

  // if attaching a fragment, remove before inserting or it will be inserted twice (once by the call and once by the parent)
  QuoteFragment newfrag = call.getQuoteFragment();
  call.setQuoteFragment( null );

  // This bit should insert the Call record but not the quote/fragment
  call = entityManager.persist( call );

  call.setQuote( existingQuote );
  // This should update any quote details, including persisting the fragment
  entityManager.merge( call );

  if( newfrag != null ) {
    // a new fragment was created - need to set this back to the call and update it (the first update will have created it within the parent quote)
    call.setQuoteFragment( newfrag );
    entityManager.merge( call );
  }

我认为通过这样做,它只会持续片段一次,但它会创建3条记录!

任何人都可以帮助我正确地连接所有这些(理想情况下使用单个数据库写入而不是3)而不创建重复项吗?

我使用JPA 2.1和Hibernate 4.3.11

提前致谢。

1 个答案:

答案 0 :(得分:1)

这里的混淆是关于持久化和合并的角色。看看this excellent answer for more info。我将此处的重要内容复制给用户Josep Pandero,以防万一它应该消失:

  

坚持:

     
      
  • 将新注册表插入数据库
  •   
  • 将对象附加到实体管理器。
  •   
     

合并:

     
      
  • 找到具有相同ID的附加对象并进行更新。
  •   
  • 如果存在则更新并返回已附加的对象。
  •   
  • 如果不存在,请将新注册表插入数据库。
  •   

以下是您期望行为的时间表。我将QuoteFragment缩写为QF,假设现有Quote的quoteFragments集合首先为空(标记为{}),通过颜色跟踪尚未保留的实体,并为持久化的实体分配虚构的id

expected situation

然而,实际情况正在发生变化。持久化方法采用您提供的实例并进行管理。跟踪对托管实体的更改,并在事务提交(容器管理或显式)上执行更新。实际上,您的merge(call)调用实际上并不需要,无论如何都会这样做。另一方面,merge方法接受您提供的实例,通过主键(从存储中获取,如果尚未在EntityManager中缓存)找到相应的管理实体,并将参数的状态合并到该管理实体。如果您在尚未管理的对象上调用merge并且没有相应的数据库条目,它将首先对您提供的对象进行复制并保留该文件。

我不太清楚为什么这个位适合你

  

call = entityManager.persist(call);

因为persist具有void返回类型。也许您使用特定于Hibernate的类?它没有返回任何东西以避免这种混乱。您作为参数传递的对象将成为管理对象。

但是,当您调用merge ...

  

合并(someObject);

此合并的返回值不一定与您传入的参数的实例相同。如果您首先显式保留它,它将是,但正如所述,您仍然不需要显式合并调用,因为更改是跟踪。更具体地说,如果someObject与主键的任何管理实体(缓存或数据库中)不对应,它实际上将返回一个不同的实例, 的实例管理。

所以实际发生的是:

actual situation

我不确定第三个条目的来源(也许您明确地将QuoteFragment保留在其他位置)但是此时应该清楚问题所在。最好的方法是首先显式地保持QuoteFragment,然后依赖于它来保持对象。不需要在存储或从存储中提取的某个实体上调用合并。

// later on at the persist stage..
// assuming all relations between the entities are still filled in (not set to null)

// persisting the new fragment
entityManager.persist(fragment);

// persisting the new call
entityManager.persist(call);

您可能需要更改quoteCall字段和fragments中字段Quote中的级联设置,否则可能会导致JPA抛出错误关于试图坚持和已经管理的实体。我知道这可能正是您试图避免的(手动必须管理持久性命令)但级联持久性/合并主要适用于您一次创建整个实体树的情况。在您的情况下,您有一个以前创建和检索的实体(Quote)和一个在多个地方引用的东西(QuoteFragment)。在这种情况下,最好不要依赖级联并明确地做事。