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
- update Model Shop with attribute
-
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.
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