Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error: detached entity passed to persist - try to persist complex data (Play-Framework)

I have a problem with persisting data via play-framework. Maybe it's not possible to achive that result, but it would be really nice if it would work.

Simple: I have a complex Model (Shop with Addresses) and I want to change the shop with addresses at once and store them in the same way (shop.save()). But the error detached entity passed to persistoccurs.

Udate History 05.11

  • 05.11

    • update Model Shop with attribute mappedBy="shop"
    • update link to google user group
  • 09.11

    • find a workaround, but it's ot generic
  • 16.11

    • update example html form, thanks to @Pavel
    • update workaround (update 09.11) to a generic method, thanks to @mericano1
  • 21.11
    • I gave up trying to find a solution and waiting for play 2.0...

Dateil: I try to cut down the problem to a minimum:
Model:

@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;
}

now my Frontendcode:

#{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}

I have just the Id from the Shop itself and the shopname to deliver via POST like: ?shop.shopname=foo

The interssting part is the list of addresses and there I have the Id and the location from the address and the result would be somthin like: ?shop.shopname=foo&shop.addresses[0].id=1&shop.addresses[0].location=bar.

Now the Controller part for the data:

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();
}

Now the problem: When i change the address data the code reaches the shop.save(); the object shop is filled with all data and everything looks fine, but when hibernate tryes to persist the data, the error detached entity passed to persist occurs :(

I tried to change the fetch mode, the cascadetype and i also tried:

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

Unfortunately nothing worked, either the error occurs, or no address data will be stored. Is there a way to store the data in that way?

If there is somthing not clear please write to me, I would be glad to give as much information as possible.

Update 1 I also put the problem on the google user group

Update 2 + 3 With the help of the user group (thanks to bryan w.) and an Answer from mericano1 here I found a generic workaround.

First you have to remove cascade=CascadeType.ALL from attribute addresses in shop.class. Then you have to change the method save within shops.class.

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();
}

the generic method to store the data looks like that:

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();
    }
}

I added in Shop.class a list of Links, but I won't update the other code snippets, so be warned if compiling errors occur.

like image 921
Tobias Sarnow Avatar asked Nov 02 '11 20:11

Tobias Sarnow


3 Answers

When you update a complex instance in Hibernate you need to make sure it is coming from the database (fetch it first, then update that same instance) to avoid this 'detached instance' problem.

I generally prefer to always fetch first and then only update specific fields I'm expecting from the UI.

You can make your code a bit more generic using

(T)Model.Manager.factoryFor(relation.getClass()).findById(relation.id);
like image 143
mericano1 Avatar answered Oct 20 '22 00:10

mericano1


This will not make you happy. I've stubmled over the same error, was about to pose the same question on SO and saw yours. This thing doesn't work, it's a major bug. In the docs I've found "since it could be tedious to explicitly call save() on a large object graph, the save() call is automatically cascaded to the relationships annotated with the cascade=CascadeType.ALL attribute." but it simply doesn't work.

I've even debugged the SQL, what happends to your (and my) associations is that they are deleted and re-associated to the parent, but they're never updated with the new values. It's something like this:

//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?? 

I saw your workaround and I honestlly appreciate your effort, and the fact that you took the trouble to post it (as it will help people who are stuck with this) but this defeats the purpose of Rapid Development and ORM. If I'm forced to do some operations that normally should be automatic (or not even necessary, why is it deleting the old assication and associating the parent to a new one instead of simply updating that association in one go?) then it's not really "rapid" nor valid "object relational mapping".

As far as I'm concerned, they've took a perfectlly working framework (JPA) and turned it into something useless by modifiying its behaviour. In this case this refferes to the fact that JPA's merge call no longer does what it's supposed to, they tell you they've added they're own method called "save" which helps with that (why???) and that thing doesn't work the way it's described in the docs and examples on their site (more on this in this question I've posted.

UPDATE:

Well, here's my workaround as well:

I now simply ignore to send in the ids of the updated associations to the controller, this way Play will think they're new entities to be added to the db, and when calling merge(...) and save() on their parent entity all daya will be saved correctlly. This however gets you another error: Since now, each time you modify some associations and save the parent, those associations are treated as new entities to be created (they have id=null), and thus the old ones are orphaned from their parent when saving all this, which either leaves you with a big bunch of orphaned, useless entities in the db, or forces you to write even more workaround code to clear those orphaned associated entities on a parent entity you're about to save.

UPDATE 2:

At this point, I think it's best to wait for Play 2.0, which is currentlly in Beta and will be launched soon. This is not too cool, as, to quote monsieur Bort (from memory) "you will not be able to migrate your Play 1.x projects to Play 2 directlly, but some light code copy-paste should suffice to transfer them". Copy/paste your code for the win! And to think how much time other framework makers spend making their new product version backwards compatible ! At any rate, in their article introducing the Play 2.0 roadmap, they say that they'll replace Hibernate/JPA with some other ORM framework, and at the same time admit that they've hacked the standard JPA implementation done by Hibernate in order to achieve... well, we all saw what that achieved. Here's the quote:

"Today, the preferred way for a Play Java application to access an SQL database is the Play Model library that is powered by Hibernate. But since it's annoying in a stateless Web framework like Play to manage stateful entities such as the ones defined in the official JPA specification, we have provided a special flavor of JPA keeping things as stateless as possible. This forced us to hack Hibernate in a way that is probably not sustainable in the long term. To address this, we plan to move to an existing implementation of stateless JPA called EBean."

This could be potential good news, as the new ORM seems more suited to their requirements, and is more likely to avoid the bad things they have now. I wish them all the luck.

like image 24
Shivan Dragon Avatar answered Oct 20 '22 00:10

Shivan Dragon


Not sure this is the answer, because I don't know about Play, but the Shop-Address bidirectional association is not correct: The shop side must be marked as the inverse of the other side, using @OneToMany(mappedBy="shop", ...).

Also, if save and merge corrspond to Session.save and Session.merge respectively, doing a save after a merge makes no sense. Save is used to insert a new, transient entity into the session. If merge has been called, it's already persistent at the time save is called.

like image 2
JB Nizet Avatar answered Oct 20 '22 01:10

JB Nizet