В моем конкретном случае я использую стратегию столбца дискриминатора. Это означает, что моя реализация JPA (Hibernate) создает таблицу users со специальным столбцом DTYPE. Этот столбец содержит имя класса объекта. Например, моя таблица users может иметь подклассы TrialUser и PayingUser. Имена этих классов будут находиться в столбце DTYPE, чтобы при загрузке объекта из базы данных EntityManager знал, какой тип класса следует создавать.
Я пробовал два способа преобразования типов Entity, и оба кажутся грязными хаками:
- Используйте собственный запрос, чтобы вручную выполнить ОБНОВЛЕНИЕ для столбца, изменив его значение. Это работает для сущностей, чьи ограничения свойств аналогичны.
- Создайте новый объект целевого типа, выполните вызов 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() работал над конкретной обновляемой сущностью.