В моем конкретном случае я использую стратегию столбца дискриминатора. Это означает, что моя реализация JPA (Hibernate) создает таблицу пользователей со специальным столбцом DTYPE. Этот столбец содержит имя класса объекта. Например, таблица моих пользователей может иметь подклассы TrialUser и PayingUser. Эти имена классов будут находиться в столбце DTYPE, так что когда EntityManager загрузит объект из базы данных, он знает, какой тип класса должен быть создан.
Я пробовал два способа преобразования типов Entity, и оба чувствуют себя как грязные хаки:
- Используйте собственный запрос, чтобы вручную сделать UPDATE в столбце, изменив его значение. Это работает для объектов, чьи ограничения свойств схожи.
- Создайте новый объект целевого типа, вызовите вызов BeanUtils.copyProperties() для перемещения по свойствам, сохраните новый объект, затем вызовите именованный запрос, который вручную заменяет новый идентификатор старым идентификатором, чтобы все ограничения внешнего ключа сохраняются.
Проблема С# 1 заключается в том, что когда вы вручную меняете этот столбец, JPA не знает, как обновить/привязать этот Entity к контексту Persistance. Он ожидает TrialUser с идентификатором 1234, а не PayingUser с идентификатором 1234. Он выходит из строя. Здесь я мог бы, вероятно, сделать EntityManager.clear() и отсоединить все объекты/очистить Per. Контекст, но поскольку это служба bean, он уничтожит ожидающие изменения для всех пользователей системы.
Проблема С# 2 заключается в том, что при удалении TrialUser все свойства, которые вы установили в Cascade = ALL, также будут удалены. Это плохо, потому что вы пытаетесь поменять местами другой пользователь, а не удалять весь расширенный граф объектов.
Обновление 1. Проблемы №2 сделали все, кроме непригодных для меня, поэтому я отказался от попыток заставить его работать. Более элегантные хаки определенно №1, и я добился определенного прогресса в этом отношении. Ключ должен сначала получить ссылку на базовый сеанс Hibernate (если вы используете Hibernate в качестве вашей реализации JPA) и вызвать метод Session.evict(user) для удаления только одного объекта из контекста persistance. К сожалению, нет никакой чистой поддержки 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() работала над конкретным обновляемым объектом.