我有一种情况需要坚持物品的层次结构:
一个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
提前致谢。
答案 0 :(得分:1)
这里的混淆是关于持久化和合并的角色。看看this excellent answer for more info。我将此处的重要内容复制给用户Josep Pandero,以防万一它应该消失:
坚持:
- 将新注册表插入数据库
- 将对象附加到实体管理器。
合并:
- 找到具有相同ID的附加对象并进行更新。
- 如果存在则更新并返回已附加的对象。
- 如果不存在,请将新注册表插入数据库。
以下是您期望行为的时间表。我将QuoteFragment缩写为QF,假设现有Quote的quoteFragments集合首先为空(标记为{}),通过颜色跟踪尚未保留的实体,并为持久化的实体分配虚构的id
然而,实际情况正在发生变化。持久化方法采用您提供的实例并进行管理。跟踪对托管实体的更改,并在事务提交(容器管理或显式)上执行更新。实际上,您的merge(call)
调用实际上并不需要,无论如何都会这样做。另一方面,merge方法接受您提供的实例,通过主键(从存储中获取,如果尚未在EntityManager中缓存)找到相应的管理实体,并将参数的状态合并到该管理实体。如果您在尚未管理的对象上调用merge
并且没有相应的数据库条目,它将首先对您提供的对象进行复制并保留该文件。
我不太清楚为什么这个位适合你
call = entityManager.persist(call);
因为persist具有void返回类型。也许您使用特定于Hibernate的类?它没有返回任何东西以避免这种混乱。您作为参数传递的对象将成为管理对象。
但是,当您调用merge ...
时合并(someObject);
此合并的返回值不一定与您传入的参数的实例相同。如果您首先显式保留它,它将是,但正如所述,您仍然不需要显式合并调用,因为更改是跟踪。更具体地说,如果someObject
与主键的任何管理实体(缓存或数据库中)不对应,它实际上将返回一个不同的实例, 的实例管理。
所以实际发生的是:
我不确定第三个条目的来源(也许您明确地将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);
您可能需要更改quote
中Call
字段和fragments
中字段Quote
中的级联设置,否则可能会导致JPA抛出错误关于试图坚持和已经管理的实体。我知道这可能正是您试图避免的(手动必须管理持久性命令)但级联持久性/合并主要适用于您一次创建整个实体树的情况。在您的情况下,您有一个以前创建和检索的实体(Quote)和一个在多个地方引用的东西(QuoteFragment)。在这种情况下,最好不要依赖级联并明确地做事。