Ошибка: удаленная сущность, переданная для сохранения, - попытаться сохранить сложные данные (Play-Framework)
У меня проблема с постоянными данными через play-framework. Возможно, это невозможно достичь этого результата, но было бы очень приятно, если бы это сработало.
Простой. У меня есть сложная модель (магазин с адресами), и я хочу сразу изменить магазин с адресами и хранить их одинаково (shop.save()). Но возникает ошибка detached entity passed to persist
.
История Udate
05.11
-
05,11
- обновить Model Shop с атрибутом
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;
}
теперь мой 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}
У меня есть только Id из самого Магазина и имя магазина для доставки через POST, например: ?shop.shopname=foo
Межстраничная часть - это список адресов, и там у меня есть ИД и местоположение от адреса, и результат будет примерно таким: ?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
Обновление 2 + 3
С помощью группы пользователей (спасибо bryan w.) И ответа от mericano1 здесь я нашел общий обходной путь.
Сначала вам нужно удалить cascade=CascadeType.ALL
из атрибута addresses
в shop.class. Затем вы должны изменить метод save
в 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();
}
общий метод хранения данных выглядит следующим образом:
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 список Ссылки, но я не буду обновлять другие фрагменты кода, поэтому будьте предупреждены, если возникнут компиляция ошибок.
Ответы
Ответ 1
Когда вы обновляете сложный экземпляр в Hibernate, вам нужно убедиться, что он поступает из базы данных (сначала выберите его, затем обновите тот же экземпляр), чтобы избежать этой проблемы с отдельным экземпляром.
Обычно я предпочитаю всегда извлекать сначала, а затем обновлять только определенные поля, которые я ожидаю от пользовательского интерфейса.
Вы можете сделать свой код более универсальным с помощью
(T)Model.Manager.factoryFor(relation.getClass()).findById(relation.id);
Ответ 2
Не уверен, что это ответ, потому что я не знаю о Play, но двунаправленная ассоциация Shop-Address неверна: сторона магазина должна быть отмечена как обратная с другой стороны, используя @OneToMany(mappedBy="shop", ...)
.
Кроме того, если save
и merge
соответствуют Session.save
и Session.merge
соответственно, выполнение сохранения после слияния не имеет смысла. Сохранить используется для вставки нового, переходного объекта в сеанс. Если было вызвано слияние, оно уже сохраняется в момент вызова save
.
Ответ 3
Это не сделает вас счастливыми. Я проделал ту же ошибку, собирался задать тот же вопрос на SO и увидел ваш. Эта вещь не работает, это серьезная ошибка. В документах, которые я нашел ", поскольку было бы утомительно явно вызвать save() на большом объектном графе, вызов save() автоматически каскадируется в отношения, аннотированные с помощью атрибута cascade = CascadeType.ALL". но он просто не работает.
Я даже отлаживал SQL, что происходит с вашими (и моими) ассоциациями в том, что они удаляются и повторно связаны с родителем, но они никогда не обновляются новыми значениями. Это что-то вроде этого:
//Here it 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 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??
Я видел ваше обходное решение, и я честно оцениваю ваши усилия и тот факт, что вы взяли на себя труд, чтобы опубликовать его (поскольку это поможет людям, которые застряли в этом), но это побеждает цель Rapid Development и ORM. Если я вынужден выполнять некоторые операции, которые обычно должны быть автоматическими (или даже не требуемыми), почему он удаляет старое утверждение и связывает родителя с новым, а не просто обновляет эту связь за один раз?), То это не реально "быстрое" и не действительное "реляционное сопоставление объектов".
Насколько я понимаю, они взяли совершенно рабочую структуру (JPA) и превратили ее в нечто бесполезное, изменив ее поведение. В этом случае это связано с тем, что вызов JPA merge
больше не выполняет то, что он должен был, они говорят вам, что они добавили, что это собственный метод под названием "save
", который помогает с этим (почему???) и эта вещь не работает так, как она описана в документах и примерах на их сайте (подробнее об этом в этом вопросе, который я опубликовал.
UPDATE:
Ну, здесь также обходной путь:
Теперь я просто игнорирую, чтобы отправлять идентификаторы обновленных ассоциаций контроллеру, таким образом Play будет думать, что это новые сущности, которые будут добавлены в db, и при вызове merge (...) и save() на их родительском сущности весь день будет сохранен правильно. Это, однако, вызывает дополнительную ошибку: теперь, каждый раз, когда вы изменяете некоторые ассоциации и сохраняете родителя, эти ассоциации рассматриваются как новые создаваемые объекты (они имеют id = null), и, таким образом, старые становятся сиротами от своего родителя, когда сохраняя все это, что либо оставляет вас с большим количеством сиротских, бесполезных объектов в db, либо заставляет вас писать еще более обходной код, чтобы очистить те осиротевшие ассоциированные объекты на родительском объекте, который вы собираетесь сохранить.
ОБНОВЛЕНИЕ 2:
На данный момент, я думаю, что лучше подождать Play 2.0, который в настоящее время находится в бета-версии и скоро будет запущен. Это не слишком здорово, так как, цитируя месье Борта (из памяти), "вы не сможете перенести свои Play 1.x-проекты на Play 2 напрямую, но для их переноса достаточно скопировать-вставить световой код". Скопируйте/вставьте код для победы! И подумать, сколько времени другие разработчики инфраструктуры тратят на то, чтобы сделать их новую версию продукта обратной совместимостью!
Во всяком случае, в своей статье в которой представлена дорожная карта Play 2.0, они говорят, что они заменит Hibernate/JPA на некоторые другие рамки ORM и в то же время признают, что они взломали стандартную реализацию JPA, выполненную Hibernate, чтобы достичь... ну, мы все увидели, что достигли. Здесь цитата:
"Сегодня предпочтительным способом для приложения Play Java для доступа к базе данных SQL является библиотека Play Model, работающая от Hibernate. Но поскольку это раздражает в безстоящей веб-среде, например Play, для управления объектами с сохранением состояния, такими как определенные в официальной спецификации JPA, мы предоставили особый колорит JPA, который сохранил вещи как безгражданство, насколько это возможно. Это заставило нас взломать Hibernate таким образом, который, вероятно, не будет устойчивым в долгосрочной перспективе. Чтобы решить эту проблему, мы планируем перейти к существующая реализация JPA без гражданства называется EBean."
Это может быть потенциальной хорошей новостью, поскольку новый ORM кажется более подходящим для их требований, и с большей вероятностью избежит плохих вещей, которые у них есть сейчас. Я желаю им всей удачи.
Ответ 4
В соответствии с документацией 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}"/>