Как изменить тип объекта в JPA?

В моем конкретном случае я использую стратегию столбца дискриминатора. Это означает, что моя реализация JPA (Hibernate) создает таблицу users со специальным столбцом DTYPE. Этот столбец содержит имя класса объекта. Например, моя таблица users может иметь подклассы TrialUser и PayingUser. Имена этих классов будут находиться в столбце DTYPE, чтобы при загрузке объекта из базы данных EntityManager знал, какой тип класса следует создавать.

Я пробовал два способа преобразования типов Entity, и оба кажутся грязными хаками:

  1. Используйте собственный запрос, чтобы вручную выполнить ОБНОВЛЕНИЕ для столбца, изменив его значение. Это работает для сущностей, чьи ограничения свойств аналогичны.
  2. Создайте новый объект целевого типа, выполните вызов BeanUtils.copyProperties() для перемещения по свойствам, сохраните новый объект, затем вызовите именованный запрос, который вручную заменяет новый идентификатор старым идентификатором. так что все ограничения внешнего ключа сохраняются.

Проблема с № 1 заключается в том, что когда вы вручную меняете этот столбец, JPA не знает, как обновить/повторно прикрепить этот объект к контексту сохранения. Ожидается TrialUser с идентификатором 1234, а не PayingUser с идентификатором 1234. Ошибка. Здесь я, вероятно, мог бы сделать EntityManager.clear() и отсоединить все Entities/очистить Per. Context, но поскольку это Service bean, он удалит ожидающие изменения для всех пользователей системы.

Проблема №2 заключается в том, что при удалении TrialUser все свойства, для которых установлено значение Cascade=ALL, также будут удалены. Это плохо, потому что вы пытаетесь только поменять местами другого пользователя, а не удалить весь расширенный граф объектов.

Обновление 1: проблемы № 2 сделали его почти непригодным для меня, поэтому я отказался от попыток заставить его работать. Самый элегантный из хаков определенно №1, и я добился определенного прогресса в этом отношении. Ключ в том, чтобы сначала получить ссылку на базовый сеанс Hibernate (если вы используете Hibernate в качестве реализации JPA) и вызвать метод Session.evict(user) для удаления только этого единственного объекта из вашего контекста постоянства. К сожалению, для этого нет чистой поддержки JPA. Вот пример кода:

  // Make sure we save any pending changes
  user = saveUser(user);

  // Remove the User instance from the persistence context
  final Session session = (Session) entityManager.getDelegate();
  session.evict(user);

  // Update the DTYPE
  final String sqlString = "update user set user.DTYPE = '" + targetClass.getSimpleName() + "' where user.id = :id";
  final Query query = entityManager.createNativeQuery(sqlString);
  query.setParameter("id", user.getId());
  query.executeUpdate();

  entityManager.flush();   // *** PROBLEM HERE ***

  // Load the User with its new type
  return getUserById(userId); 

Обратите внимание на ручную flush(), которая генерирует это исключение:

org.hibernate.PersistentObjectException: detached entity passed to persist: com.myapp.domain.Membership
at org.hibernate.event.def.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:102)
at org.hibernate.impl.SessionImpl.firePersistOnFlush(SessionImpl.java:671)
at org.hibernate.impl.SessionImpl.persistOnFlush(SessionImpl.java:663)
at org.hibernate.engine.CascadingAction$9.cascade(CascadingAction.java:346)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:291)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:239)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:319)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:265)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:242)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:192)
at org.hibernate.engine.Cascade.cascade(Cascade.java:153)
at org.hibernate.event.def.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:154)
at org.hibernate.event.def.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:145)
at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:88)
at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58)
at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:996)
at org.hibernate.impl.SessionImpl.executeNativeUpdate(SessionImpl.java:1185)
at org.hibernate.impl.SQLQueryImpl.executeUpdate(SQLQueryImpl.java:357)
at org.hibernate.ejb.QueryImpl.executeUpdate(QueryImpl.java:51)
at com.myapp.repository.user.JpaUserRepository.convertUserType(JpaUserRepository.java:107)

Вы видите, что объект Членство, в котором Пользователь имеет набор OneToMany, вызывает некоторые проблемы. Я недостаточно знаю о том, что происходит за кулисами, чтобы расколоть этот орех.

Обновление 2. Единственное, что работает на данный момент, это изменить DTYPE, как показано в приведенном выше коде, а затем вызвать entityManager.clear().

Я не совсем понимаю последствия очистки всего контекста персистентности, и мне бы хотелось, чтобы вместо этого Session.evict() работал над конкретной обновляемой сущностью.


person Robert Campbell    schedule 13.04.2009    source источник


Ответы (5)


Итак, я наконец нашел рабочее решение:

Откажитесь от EntityManager для обновления DTYPE. В основном это связано с тем, что Query.executeUpdate() должен выполняться внутри транзакции. Вы можете попробовать запустить его в существующей транзакции, но это, вероятно, связано с тем же контекстом сохраняемости объекта, который вы изменяете. Это означает, что после обновления DTYPE вы должны найти способ evict() сущности. Самый простой способ — вызвать entityManager.clear(), но это приводит к всевозможным побочным эффектам (читайте об этом в спецификации JPA). Лучшее решение — получить базового делегата (в моем случае Hibernate Session) и вызвать Session.evict(user). Это, вероятно, будет работать на простых доменных графах, но мои были очень сложными. Мне так и не удалось заставить @Cascade(CascadeType.EVICT) правильно работать с моими существующими аннотациями JPA, такими как @OneToOne(cascade = CascadeType.ALL). Я также попытался вручную передать моему графу домена Session и заставить каждую родительскую сущность вытеснить своих дочерних элементов. Это также не сработало по неизвестным причинам.

Я оказался в ситуации, когда сработает только entityManager.clear(), но я не мог смириться с побочными эффектами. Затем я попытался создать отдельную единицу персистентности специально для конверсий сущностей. Я подумал, что могу локализовать операцию clear() только для того ПК, который отвечает за конверсии. Я настроил новый ПК, новую соответствующую EntityManagerFactory, новый менеджер транзакций для него и вручную внедрил этот менеджер транзакций в репозиторий для ручной упаковки executeUpdate(). в транзакции, соответствующей соответствующему ПК. Здесь я должен сказать, что я недостаточно знаю о транзакциях, управляемых контейнером Spring/JPA, потому что это закончилось кошмаром, пытаясь получить локальную/ручную транзакцию для executeUpdate(), чтобы хорошо работать с транзакция, управляемая контейнером, извлекается из уровня службы.

В этот момент я выбросил все и создал этот класс:

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class JdbcUserConversionRepository implements UserConversionRepository {

@Resource
private UserService userService;

private JdbcTemplate jdbcTemplate;

@Override
@SuppressWarnings("unchecked")
public User convertUserType(final User user, final Class targetClass) {

        // Update the DTYPE
        jdbcTemplate.update("update user set user.DTYPE = ? where user.id = ?", new Object[] { targetClass.getSimpleName(), user.getId() });

        // Before we try to load our converted User back into the Persistence
        // Context, we need to remove them from the PC so the EntityManager
        // doesn't try to load the cached one in the PC. Keep in mind that all
        // of the child Entities of this User will remain in the PC. This would
        // normally cause a problem when the PC is flushed, throwing a detached
        // entity exception. In this specific case, we return a new User
        // reference which replaces the old one. This means if we just evict the
        // User, then remove all references to it, the PC will not be able to
        // drill down into the children and try to persist them.
        userService.evictUser(user);

        // Reload the converted User into the Persistence Context
        return userService.getUserById(user.getId());
    }

    public void setDataSource(final DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
}

Есть две важные части этого метода, которые, я считаю, заставляют его работать:

  1. Я пометил его с помощью @Transactional(propagation = Propagation.NOT_SUPPORTED), что должно приостановить транзакцию, управляемую контейнером, поступающую из сервисного уровня, и разрешить преобразование вне ПК.
  2. Перед попыткой перезагрузить преобразованную сущность обратно на ПК я удаляю старую копию, хранящуюся в настоящее время на ПК, с помощью userService.evictUser(user);. Код для этого просто получает экземпляр Session и вызывает evict(user). См. комментарии в коде для получения более подробной информации, но в основном, если мы не сделаем этого, любые вызовы getUser попытаются вернуть кэшированную сущность, все еще находящуюся на ПК, за исключением того, что она выдаст ошибку о тип отличается.

Хотя мои первоначальные тесты прошли хорошо, это решение все еще может иметь некоторые проблемы. Я буду держать это в курсе, как только они будут обнаружены.

person Robert Campbell    schedule 18.04.2009

Вам действительно нужно представлять уровень подписки в типе User? Почему бы вам не добавить тип подписки в вашу систему?

Тогда отношение будет выражено следующим образом: Пользователь имеет одну Подписку. Его также можно смоделировать как отношение «один ко многим», если вы хотите сохранить историю подписок.

Затем, когда вы хотите изменить уровень подписки пользователя, вместо создания экземпляра нового пользователя вы должны создать новую подписку и назначить ее пользователю.

// When you user signs up
User.setSubscription(new FreeSubscription());

// When he upgrades to a paying account
User.setSubscription(new PayingSubscription());

Действительно ли TrialUser сильно отличается от PayingUser? Возможно нет. Вам лучше будет обслуживать агрегацию вместо наследования.

Данные подписки могут храниться в отдельной таблице (сопоставляться как сущность) или внутри таблицы пользователей (сопоставляться как компонент).

person Patrick Lafleur    schedule 14.04.2009
comment
Я согласен, но, возможно, я выбрал неудачный пример. Моя реальная жизненная ситуация более сложна: суперкласс User, подкласс Customer, который имеет подклассы FamilyHead, FamilyMember, Individual, Pro и т. д., и подкласс Employee с подклассами Staff, Admin и т. д. У них есть функции, которые я бы не хотел извлекать - person Robert Campbell; 16.04.2009

Проблема больше связана с Java, чем с чем-либо еще. Вы не можете изменить тип экземпляра во время выполнения (во время выполнения), поэтому спящий режим не предусматривает такие сценарии (в отличие, например, от рельсов).

Если вы хотите исключить пользователя из сеанса, вам придется исключить связанные сущности. У вас есть несколько вариантов:

  • Сопоставьте сущность с помощью evict=cascade (см. Session#evict javadoc)
  • Вручную удалить все связанные объекты (это может быть обременительно)
  • Очистить сеанс, тем самым вытеснив все объекты (конечно, вы потеряете локальный кеш сеанса)

У меня была аналогичная проблема в Grails, и мое решение похоже на решение григория. Я копирую все поля экземпляра, включая связанные сущности, затем удаляю старую сущность и записываю новую. Если у вас не так много отношений, это может быть легко, но если ваша модель данных сложна, вам будет лучше использовать описанное вами собственное решение sql.

Вот источник, если интересно:

def convertToPacient = {
    withPerson(params.id) {person ->
      def pacient = new Pacient()
      pacient.properties = person.properties
      pacient.id = null
      pacient.processNumber = params.processNumber
      def ap = new Appointment(params)
      pacient.addToAppointments(ap);

      Person.withTransaction {tx ->
        if (pacient.validate() && !pacient.hasErrors()) {

          //to avoid the "Found two representations of same collection" error
          //pacient.attachments = new HashSet(person.attachments);
          //pacient.memberships = new HashSet(person.memberships);
          def groups = person?.memberships?.collect {m -> Group.get(m.group.id)}
          def attachs = []
          person.attachments.each {a ->
            def att = new Attachment()
            att.properties = a.properties
            attachs << att
          }

          //need an in in order to add the person to a group
          person.delete(flush: true)
          pacient.save(flush: true)
          groups.each {g -> pacient.addToGroup(g)};
          attachs.each {a -> pacient.addToAttachments(a)}
          //pacient.attachments.each {att -> att.id = null; att.version = null; att.person = pacient};

          if (!pacient.save()) {
            tx.setRollbackOnly()
            return
          }
        }
      }
person Miguel Ping    schedule 16.04.2009
comment
Я действительно этот ответ. Сначала я попробовал ваш третий вариант: очистить сеанс с помощью EntityManager.clear(). Это работает. Проблема в том, что я потеряю любые незафиксированные изменения в другом месте в контексте постоянства. Теперь я делаю первое предложение, добавляя @Cascade(value = CascadeType.EVICT) к вложенным сущностям, вызывающим проблему. Однако это не работает, и я все еще получаю ту же ошибку (отдельный объект передается для сохранения). Я помещаю тег Hibernate поверх тега JPA. Может быть поэтому. - person Robert Campbell; 17.04.2009
comment
Я также попробовал второй вариант, передав сеанс объекту пользователя, который вытеснил бы его дочерние сущности, передал бы его им, чтобы они могли сделать то же самое, и т. д. вниз по графу объектов. Ничего не работает. Все три стратегии заканчиваются одной и той же ошибкой в ​​одной и той же дочерней сущности: отсоединенная сущность передается для сохранения - person Robert Campbell; 17.04.2009

Проблема больше связана с Java, чем с чем-либо еще. Вы не можете изменить тип экземпляра во время выполнения (во время выполнения), поэтому спящий режим не предусматривает такие сценарии (в отличие, например, от рельсов).

Я столкнулся с этим сообщением, потому что у меня (или думал, что у меня есть) аналогичная проблема. Я пришел к выводу, что проблема не в механике смены типов в технологии, а в том, что эволюция типов в целом вовсе не проста и не проста. Проблема в том, что типы довольно часто имеют различные атрибуты. Если вы не сопоставляете разные типы только для различий в поведении, неясно, что делать с дополнительной информацией. По этой причине имеет смысл мигрировать типы на основе построения новых объектов.

Проблема не связана с Java. это связано с теорией типов в целом и, более конкретно, с объектно-реляционным отображением здесь. Если ваш язык поддерживает ввод текста, я хотел бы услышать, как вы можете автоматически изменять тип объекта во время выполнения и волшебным образом делать что-то разумное с оставшейся информацией. Пока мы распространяем FUD: остерегайтесь того, от кого вы советуетесь: Rails — это фреймворк, Ruby — это язык.

person Community    schedule 08.06.2009

Когда вы говорите о преобразовании, вы, вероятно, искажаете проблему. На мой взгляд, вы действительно пытаетесь создать экземпляр одного класса на основе экземпляра другого класса, например:

public PayingUser(TrialUser theUser) {
...
}

Затем вы можете удалить старого пробного пользователя и сохранить нового платного пользователя.

person topchef    schedule 13.04.2009
comment
Правильно, но, как я уже упоминал в посте, это нарушит все ограничения внешнего ключа. Существует множество таблиц, относящихся к объекту TrialUser, включая записи транзакций, предпочтений, файлов и т. д. Все это необходимо перенести в объект PayingUser, иначе пользователь потеряет все. - person Robert Campbell; 13.04.2009
comment
Извините, я пропустил внешние ключи. Или вы добавили их после моего ответа? В любом случае, ваш конструктор копирования также должен копировать отношения. Это кажется дорогим, поэтому не планируйте, чтобы это происходило более одного или двух раз за время существования объекта. - person topchef; 13.04.2009