Как изменить тип сущности в JPA? - PullRequest
10 голосов
/ 13 апреля 2009

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

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

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

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

Проблема с # 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 () работала над обновлением конкретной сущности.

Ответы [ 5 ]

3 голосов
/ 18 апреля 2009

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

Отключить 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 () хорошо играть с Управляемая контейнером транзакция извлекается из уровня Service.

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

@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 (распространение = распространение.NOT_SUPPORTED) который должен приостановить управляемую контейнером транзакцию, поступающую с уровня Service, и разрешить преобразование происходить вне ПК.
  2. Перед попыткой перезагрузить преобразованную сущность обратно в ПК, я выселяю старую копию, хранящуюся в данный момент на ПК, с помощью userService.evictUser (user); . Код для этого - просто получить экземпляр Session и вызвать evict (user) . См. Комментарии в коде для получения более подробной информации, но в основном, если мы не сделаем этого, любые вызовы getUser попытаются вернуть кэшированный объект еще на ПК, за исключением того, что он выдаст ошибку о типе быть другим.

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

2 голосов
/ 14 апреля 2009

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

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

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

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

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

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

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

1 голос
/ 16 апреля 2009

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

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

  • Сопоставить сущность с помощью 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
          }
        }
      }
0 голосов
/ 09 июня 2009

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

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

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

0 голосов
/ 13 апреля 2009

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

public PayingUser(TrialUser theUser) {
...
}

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

...