错误:传递给持久化的分离实体 - 尝试持久化复杂数据(Play-Framework)

时间:2011-11-02 20:34:38

标签: java hibernate playframework

我遇到了通过play-framework持久保存数据的问题。也许不可能取得这样的结果,但如果它能起作用那将是非常好的。

简单:我有一个复杂的模型(Shop with Addresses),我想立即用地址更改商店并以相同的方式存储它们(shop.save())。但是错误detached entity passed to persist发生了。

Udate History 05.11

  • 05.11

    • 使用属性mappedBy="shop"
    • 更新模型商店
    • 更新指向Google用户组的链接
  • 09.11

    • 找到解决方法,但它不是通用的
  • 16.11

    • 更新示例html表单,感谢@Pavel
    • 将解决方法(更新09.11)更新为通用方法,感谢@ mericano1
  • 21.11
    • 我放弃了试图寻找解决方案并等待播放2.0 ......

Dateil : 我尝试将问题减少到最低限度:
型号

@Entity
public class Shop extends Model {

    @Required(message = "Shopname is required")
    public String shopname;

    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="shop")
    public List<Address> addresses;

}


@Entity
public class Address extends Model {

    @Required
    public String location;

    @ManyToOne
    public Shop shop;
}

现在我的前端代码

#{extends 'main.html' /}

#{form @save(shop?.id)}

    <input type="hidden" name="shop.id" value="${shop?.id}"/>

    #{field 'shop.shopname'}
        <label for="shopName">Shop name:</label>
        <input type="text" name="${field.name}" 
            value="${shop?.shopname}" class="${field.errorClass}" />
    #{/field}

    <legend>Addressen</legend>
    #{list items: shop.addresses, as: "address"}
        <input type="hidden" name="shop.addresses[${address_index - 1}].id" value="${address.id}"/>
        <label>Location</label>
        <input name="shop.addresses[${address_index - 1}].location" type="text" value="${address.location}"/>
    #{/list}

     <input type="submit" class="btn primary" value="Save changes" />
#{/form}

我只有商店本身的ID和通过POST发送的商店名称,如:?shop.shopname=foo

有趣的部分是地址列表,我有地址的Id和位置,结果将是:?shop.shopname=foo&shop.addresses[0].id=1&shop.addresses[0].location=bar

现在数据的控制器部分:

public class Shops extends CRUD {

public static void form(Long id) {

    if (id != null) {
        Shop shop = Shop.findById(id);
        render(shop);
    }
    render();
}

public static void save(Long id, Shop shop) {

    // set owner manually (dont edit from FE)
    User user = User.find("byEmail", Security.connected()).first();
    shop.owner = user;

    // Validate
    validation.valid(shop);
    if (validation.hasErrors()) 
        render("@form", shop);

    shop.save();
    index();
}

现在问题:当我更改地址数据时,代码到达shop.save();,对象商店充满了所有数据,一切看起来都很好,但是当hibernate尝试保留数据时,错误detached entity passed to persist发生:(

我试图改变获取模式,cascadetype,我也尝试过:

Shop shop1 = shop.merge();
shop1.save();

不幸的是,没有任何工作,无论是发生错误,还是不会存储地址数据。 有没有办法以这种方式存储数据?

如果有什么不清楚请写信给我,我很乐意尽可能多地提供信息。

更新1 我也把问题放在google user group

更新2 + 3 在用户组的帮助下(感谢bryan w。)和mericano1的答案,我发现了一个通用的解决方法。

首先,您必须从shop.class中的属性cascade=CascadeType.ALL中删除addresses。然后你必须改变shops.class中的方法save

public static void save(Long id, Shop shop) {

    // set owner manually (dont edit from FE)
    User user = User.find("byEmail", Security.connected()).first();
    shop.owner = user;

    // store complex data within shop
    storeData(shop.addresses, "shop.addresses");
    storeData(shop.links, "shop.links");

    // Validate
    validation.valid(shop);
    if (validation.hasErrors()) 
        render("@form", shop);

    shop.save();
    index();
}

存储数据的通用方法如下:

private static <T extends Model> void  storeData(List<T> list, String parameterName) {
    for(int i=0; i<list.size(); i++) {
        T relation = list.get(i);

        if (relation == null)
            continue;

        if (relation.id != null) {
            relation = (T)Model.Manager.factoryFor(relation.getClass()).findById(relation.id);
            StringBuffer buf = new StringBuffer(parameterName);
            buf.append('[').append(i).append(']');
            Binder.bind(relation, buf.toString(), request.params.all());
        }

        // try to set bidiritional relation (you need an interface or smth)
        //relation.shop = shop;
        relation.save();
    }
}

我在Shop.class中添加了链接的列表,但我不会更新其他代码段,因此如果发生编译错误则会收到警告。

4 个答案:

答案 0 :(得分:6)

当您在Hibernate中更新复杂实例时,您需要确保它来自数据库(首先获取它,然后更新同一个实例)以避免此“分离实例”问题。

我通常更喜欢始终先获取,然后只更新我期望从UI中获取的特定字段。

您可以使用

使代码更通用
(T)Model.Manager.factoryFor(relation.getClass()).findById(relation.id);

答案 1 :(得分:2)

不确定这是答案,因为我不了解Play,但Shop-Address双向关联不正确:必须使用@OneToMany(mappedBy="shop", ...)将商店一侧标记为另一侧的反面。

此外,如果savemerge分别与Session.saveSession.merge相对应,则在合并后进行保存是没有意义的。保存用于将新的瞬态实体插入到会话中。如果已调用merge,则在调用save时它已经持久。

答案 2 :(得分:2)

这不会让你开心。我已经纠结了相同的错误,即将在SO上提出相同的问题并看到了你的问题。这件事不起作用,这是一个主要的错误。在我发现的文档中“因为在大对象图上显式调用save()可能很繁琐,save()调用会自动级联到用cascade = CascadeType.ALL属性注释的关系。”但它根本行不通。

我甚至调试了SQL,你和(和我)的关联发生了什么,它们被删除并重新关联到父级,但它们永远不会用新值更新。它是这样的:

//Here's it's updating a LaundryList where I've modified the address and one of the laundry items
//it first updates the simple values on the LaundryList:
update LaundryList set address=?, washDate=? where id=?
binding parameter [1] as [VARCHAR] - 123 bong st2
binding parameter [2] as [DATE] - Fri Mar 11 00:00:00 CET 2011
binding parameter [3] as [BIGINT] - 413

//then it deletes the older LaundryItem:
delete from LaundryList_LaundryItem where LaundryList_id=?
binding parameter [1] as [BIGINT] - 413
binding parameter [2] as [BIGINT] - 407

//here it's associating the laundry list to a different laundry item
insert into LaundryList_LaundryItem (LaundryList_id, laundryItems_id) values (?, ?)
binding parameter [1] as [BIGINT] - 413
binding parameter [2] as [BIGINT] - 408

//so where did you issue the SQL that adds the updated values to the associated LaundryItem?? 

我看到了你的解决方法,我真诚地感谢你的努力,以及你煞费苦心地发布它(因为它会帮助那些坚持这个的人),但这违背了快速开发和ORM的目的。如果我被迫做一些通常应该是自动的操作(或者甚至不需要,为什么它会删除旧的辅助并将父级关联到一个新的,而不是简单地一次性更新该关联?)那么它并不是真的“快速”也不是有效的“对象关系映射”。

就我而言,他们采用了完美的工作框架(JPA),并通过修改其行为将其变为无用的东西。在这种情况下,这反映了这样一个事实:JPA的merge调用不再符合它的预期,他们告诉你他们已经添加了他们自己的方法,称为“save”,这有助于此(为什么???)并且该东西不能按照其网站上的文档和示例中描述的方式工作(更多关于我在 this 问题中发布的内容。

更新:

嗯,这也是我的解决方法:

我现在只是忽略将更新的关联的ID发送到控制器,这样Play会认为它们是要添加到数据库的新实体,并且在调用merge(...)和save()时在他们的父实体上,所有daya都将被正确保存。然而,这会给您带来另一个错误:从现在开始,每次修改某些关联并保存父关联时,这些关联都被视为要创建的新实体(它们具有id = null),因此旧的关联将从其父关系中孤立出来。保存所有这些,要么在数据库中留下一大堆孤立的,无用的实体,要么强迫您编写更多的解决方法代码以清除您要保存的父实体上的孤立关联实体。

更新2:

此时,我认为最好等待Play 2.0,目前正处于测试阶段,并将很快推出。这并不太酷,因为引用monsieur Bort(来自内存)“你将无法直接将Play 1.x项目迁移到Play 2,但是一些轻量代码复制粘贴应该足以传输它们”。复制/粘贴您的代码以获得胜利!并考虑其他框架制造商花费多少时间使他们的新产品版本向后兼容! 无论如何,在article介绍Play 2.0路线图时,他们说他们会用其他一些ORM框架取代Hibernate / JPA,同时承认他们已经攻击了标准的JPA实现Hibernate为了实现......好吧,我们都看到了所取得的成就。这是引用:

“今天,Play Java应用程序访问SQL数据库的首选方式是由Hibernate提供支持的Play模型库。但是,因为在像Play这样的无状态Web框架中管理有状态实体(例如已定义的实体)很烦人在官方的JPA规范中,我们提供了一种特殊的JPA风格,保持尽可能无状态的东西。这迫使我们以一种长期可能无法持续的方式破解Hibernate。为了解决这个问题,我们计划迁移到现有的无状态JPA实现称为EBean。“

这可能是一个潜在的好消息,因为新的ORM似乎更适合他们的要求,并且更有可能避免现在的坏事。祝他们一切顺利。

答案 3 :(得分:1)

根据Play文档,您应该提供如下的查询字符串:

?shop.addresses[0].id=123
&shop.addresses[1].id=456
&shop.addresses[2].id=789

我不确定您是否正确提供。试试这个:

<input type="hidden" name="shop.addresses[${address_index - 1}].id" value="${address.id}"/>