一对多关系:使用JPA 2.0更新已删除的子项

3
我有一个双向一对多关系。
0或1个客户端<->0或多个产品订单列表。
该关系应在两个实体上设置或取消: 在客户端方面,我想设置分配给客户端的产品订单列表;然后应自动选择设置/取消选择的订单。 在产品订单方面,我想将客户端设置为所分配的客户端;然后应从其先前分配的客户端列表中删除该产品订单并添加到新分配客户端的列表中。
我想使用纯JPA 2.0注释和一个“合并”调用到实体管理器(具有级联选项)。我已尝试使用以下代码片段,但它不起作用(我使用EclipseLink 2.2.0作为持久性提供程序)。
@Entity
public class Client implements Serializable {
    @OneToMany(mappedBy = "client", cascade= CascadeType.ALL)
    private List<ProductOrder> orders = new ArrayList<>();

    public void setOrders(List<ProductOrder> orders) {
        for (ProductOrder order : this.orders) {
            order.unsetClient();
            // don't use order.setClient(null);
            // (ConcurrentModificationEx on array)
            // TODO doesn't work!
        }
        for (ProductOrder order : orders) {
            order.setClient(this);
        }
        this.orders = orders;
    }

    // other fields / getters / setters
}

@Entity
public class ProductOrder implements Serializable {
    @ManyToOne(cascade= CascadeType.ALL)
    private Client client;

    public void setClient(Client client) {
        // remove from previous client
        if (this.client != null) {
            this.client.getOrders().remove(this);
        }

        this.client = client;

        // add to new client
        if (client != null && !client.getOrders().contains(this)) {
            client.getOrders().add(this);
        }
    }

    public void unsetClient() {
        client = null;
    }

    // other fields / getters / setters
}

客户端持久化的外观模式代码:

// call setters on entity by JSF frontend...
getEntityManager().merge(client)

用于持久化产品订单的门面模式代码:

// call setters on entity by JSF frontend...
getEntityManager().merge(productOrder)

在订单端更改客户分配时,它能够正常工作:在客户端上,订单将从先前客户的列表中移除,并添加到新客户的列表中(如果重新分配)。
但是,在客户端更改时,我只能添加订单(在订单端,分配给新客户),但当我从客户列表中删除订单时,它会被忽略(保存并刷新后,订单仍然在客户端的列表中,在订单端它们仍然分配给先前的客户)。
为了澄清,我不想使用“删除孤儿”选项:从列表中删除订单时,它不应从数据库中删除,而其客户分配应更新(即为空),如Client#setOrders方法中所定义。怎样才能实现这一点?
编辑:由于我在这里得到的帮助,我成功解决了这个问题。请看下面的解决方案:
客户端(“One” /“ Owned”端)在一个临时字段中存储已修改的订单。
@Entity
public class Client implements Serializable, EntityContainer {

    @OneToMany(mappedBy = "client", cascade= CascadeType.ALL)
    private List<ProductOrder> orders = new ArrayList<>();

    @Transient
    private List<ProductOrder> modifiedOrders = new ArrayList<>();

    public void setOrders(List<ProductOrder> orders) {
    if (orders == null) {
        orders = new ArrayList<>();
    }

    modifiedOrders = new ArrayList<>();
    for (ProductOrder order : this.orders) {
        order.unsetClient();
        modifiedOrders.add(order);
        // don't use order.setClient(null);
        // (ConcurrentModificationEx on array)
    }

    for (ProductOrder order : orders) {
        order.setClient(this);
        modifiedOrders.add(order);
    }

    this.orders = orders;
    }

    @Override // defined by my EntityContainer interface
    public List getContainedEntities() {
        return modifiedOrders;
}

在外观模式中,在持久化时,会检查是否还有其他需要持久化的实体。需要注意的是,我使用一个接口来封装这个逻辑,因为我的外观模式实际上是通用的。

// call setters on entity by JSF frontend...
getEntityManager().merge(entity);

if (entity instanceof EntityContainer) {
    EntityContainer entityContainer = (EntityContainer) entity;
    for (Object childEntity : entityContainer.getContainedEntities()) {
        getEntityManager().merge(childEntity);
    }
}
3个回答

5
JPA不支持自动维护双向关系,据我所知也没有JPA实现支持这一点。JPA要求您管理关系的两个方向。当只更新关系中的一个方向时,有时会被称为“对象损坏”。
在双向关系中,JPA定义了一个“拥有”(owning)方向(对于OneToMany来说,这是不带有mappedBy注解的那一方),用于在向数据库中持久化时解决冲突(与内存中的两个表示相比,数据库中只有一个表示,因此必须进行冲突解决)。这就是为什么ProductOrder类的更改能够生效而Client类的更改却不能。
即使有“拥有”关系,您也应该始终更新双向关系的两个方向。这经常导致人们只更新一侧,当他们打开二级缓存时就会遇到麻烦。在JPA中,上述冲突只有在将对象持久化并重新从数据库加载时才得以解决。一旦启用了二级缓存,这可能会发生在数个事务之后,在此期间您将处理一个已损坏的对象。

谢谢你对“拥有方”如何影响冲突解决的解释。现在我明白了这种行为。 - SputNick

0
另一个可能的解决方案是在您的ProductOrder上添加新属性,我在下面的示例中将其命名为detached
当您想要将订单与客户端分离时,可以在订单本身上使用回调函数:
@Entity public class ProductOrder implements Serializable { 
  /*...*/

  //in your case this could probably be @Transient
  private boolean detached;  

  @PreUpdate
  public void detachFromClient() {
    if(this.detached){
        client.getOrders().remove(this);
        client=null;
    }
  }
}

不要直接删除你想删除的订单,而是将detached属性设置为true。当你合并和刷新客户端时,实体管理器会检测到修改的订单,并执行@PreUpdate回调,从而有效地将订单与客户端分离。


好主意。因为我实际上想要删除订单,所以我使用了类似的机制,使用了@PreRemove注解。 - SputNick

0

你还需要合并你删除的订单,仅合并客户是不够的。

问题在于,虽然你正在更改已删除的订单,但你从未将这些订单发送到服务器,并且从未调用它们的合并操作,因此无法反映你的更改。

你需要对每个删除的订单调用合并操作。或者在本地处理你的更改,这样你就不需要序列化或合并任何对象。

EclipseLink确实具有双向关系维护功能,在这种情况下可能适用于你,但它不是JPA的一部分。


在这种情况下(或者通常情况下),在订单上添加级联是否有帮助(这样当客户合并时,订单会自动合并)? - iCrus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接