Ошибка: удаленная сущность, переданная для сохранения, - попытаться сохранить сложные данные (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}"/>