Вопрос

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

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

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

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

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

Обновление 1:Проблемы с # 2 сделали его практически непригодным для меня, поэтому я отказался от попыток заставить его работать.Более элегантный из хаков, безусловно, № 1, и я добился некоторого прогресса в этом отношении.Ключ в том, чтобы сначала получить ссылку на базовый сеанс гибернации (если вы используете 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); 

Обратите внимание на руководство пользователя промывка() который выдает это исключение:

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()

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

Это было полезно?

Решение

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

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

  • Сопоставьте объект с помощью evict=cascade (см. Сеанс#выселение 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
          }
        }
      }

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

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

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

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

public PayingUser(TrialUser theUser) {
...
}

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top