Pregunta

En mi caso particular, estoy haciendo uso de una estrategia columna discriminadora. Esto significa que mi aplicación APP (Hibernate) crea un usuarios de mesa con especial DTYPE columna. Esta columna contiene el nombre de la clase de la entidad. Por ejemplo, mi usuarios tabla puede tener subclases de TrialUser y PayingUser . Estos nombres de las clases serían en el DTYPE Columna de modo que cuando el EntityManager carga la entidad de la base de datos, se sabe qué tipo de clase a instanciar.

He intentado dos formas de convertir tipos de entidad y ambos siento como cortes sucios:

  1. Utilice una consulta nativa que hacer manualmente una actualización de la columna, cambiar su valor. Esto funciona para entidades cuya propiedad limitaciones son similares.
  2. Crear una nueva entidad del tipo de destino, hacer un BeanUtils.copyProperties () llaman a moverse sobre las propiedades, guardar la nueva entidad, a continuación, llamar a una consulta con nombre que sustituye manualmente la nueva identificación con la la antigua ID para que todas las restricciones de clave externa se mantienen.

El problema con el # 1 es que cuando se cambia manualmente esta columna, JPA no sabe cómo actualizar / volver a conectar esta Entidad con el Contexto persistencia. Se espera un TrialUser con el identificador 1234, no es un PayingUser con el identificador 1234. Fracasa a cabo. Aquí probablemente podría hacer un EntityManager.clear () y separar todas las Entidades / borrar el Per. Contexto, pero ya que este es un grano de servicio, sería limpiar cambios para todos los usuarios del sistema en espera.

El problema con # 2 es que cuando se elimina el TrialUser todas las propiedades que ha establecido a Cascade = ALL se eliminarán también. Esto es malo, ya que sólo está tratando de cambiar de usuario diferente, no eliminar todo el gráfico de objeto extendido.

Actualización 1 : Los problemas de la # 2 han hecho casi inservible para mí, así que he renunciado a tratar de conseguir que funcione. El más elegante de los cortes es sin duda # 1, y lo han hecho algunos progresos en este sentido. La clave es conseguir primero una referencia a la sesión de Hibernate subyacente (si está usando Hibernate como su aplicación APP) y llame al método Session.evict (usuario) para extraer solamente el único objeto de su contexto de persistencia. Unfortunitely no hay apoyo APP puro para esto. Aquí hay un código de ejemplo:

  // 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); 

Observe el manual flush () que lanza esta excepción:

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)

Se puede ver que el Afiliación entidad, de los cuales usuario tiene un conjunto OneToMany, está causando algunos problemas. No sé lo suficiente sobre lo que está pasando detrás de las escenas de romper esta nuez.

Actualización 2 : La única cosa que funciona hasta ahora es cambiar DTYPE como se muestra en el código anterior, a continuación, llamar entityManager.clear ()

No entiendo por completo las ramificaciones de la limpieza de todo el contexto de persistencia, y me hubiera gustado conseguir Session.evict () a trabajar en la entidad particular que se actualizan en su lugar.

¿Fue útil?

Solución

Así que finalmente cuenta de una solución de trabajo:

Pasa de la EntityManager para actualizar DTYPE . Esto es principalmente porque Query.executeUpdate () debe funcionar dentro de una transacción. Usted puede intentar ejecutar dentro de la transacción existente, pero que probablemente está ligado a la misma contexto de persistencia de la entidad que se está modificando. Lo que esto significa es que después de actualizar DTYPE usted tiene que encontrar una manera de evict () la Entidad. La manera más fácil es llamar entityManager.clear () , pero esto da lugar a todo tipo de efectos secundarios (leído en la especificación JPA). La mejor solución es conseguir que el delegado subyacente (en mi caso, una hibernación Sesión ) y llamar a Session.evict (usuario) . Esto probablemente funcionará en los gráficos de dominio simples, pero la mía era muy complejo. Nunca fui capaz de obtener @Cascade (CascadeType.EVICT) para funcionar correctamente con mis anotaciones JPA existentes, como @OneToOne (cascada = CascadeType.ALL) . Yo también trató de pasar manualmente mi gráfico del dominio de una Sesión y que cada entidad matriz desalojar a sus hijos. Esto también no funcionaba por razones desconocidas.

Me dejaron en una situación donde sólo entityManager.clear () funcionaría, pero no podía aceptar los efectos secundarios. Luego trató de crear una unidad de persistencia separada específicamente para las conversiones de entidad. Pensé que podía localizar el claro) operación (para PC que sólo se encarga de las conversiones. He creado un nuevo PC, un EntityManagerFactory , un nuevo gestor de transacciones nueva correspondiente para ello, e inyectar manualmente este administrador de transacciones en el repositorio para el embalaje manual de la executeUpdate () en una transacción correspondiente a la PC adecuado. Aquí tengo que decir que no sé lo suficiente sobre la primavera / JPA transacciones gestionada por contenedor, ya que terminó siendo una pesadilla tratando de conseguir la transacción / manual de local para executeUpdate () a jugar muy bien con la transacción gestionada contenedor dejándonos arrastrar por la capa de servicio.

En este punto Tiré todo y creé esta clase:

@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);
    }
}

Hay dos partes importantes de este método que yo creo que funcione:

  1. he marcado con @Transactional (propagación = Propagation.NOT_SUPPORTED) que debe suspender la operación de contenedores gestionado desde la capa de servicio y permite realizar la conversión se lleve a cabo externa de la PC.
  2. Antes de intentar recargar la Entidad se convierte de nuevo en el PC, que desaloje la vieja copia almacenada actualmente en el PC con userService.evictUser (usuario); . El código para esto es simplemente recibiendo un Sesión instancia y llamando a evict (usuario) . Véanse los comentarios en el código para más detalles, pero básicamente, si no hacemos esto todas las llamadas a getUser a tratar de devolver la Entidad en caché todavía en el PC, excepto que generará un error acerca de la escriba ser diferente.

A pesar de mis pruebas iniciales han ido bien, esta solución todavía puede tener algunos problemas. Voy a mantener este actualiza a medida que se descubren.

Otros consejos

¿Usted realmente necesita para representar el nivel de suscripción en el tipo de usuario? ¿Por qué no añades un tipo de suscripción en su sistema?

La relación sería entonces expresarse como: Un usuario tiene una suscripción. También podría ser modelado como una relación de uno a muchos, si se desea mantener un historial de las suscripciones.

A continuación, cuando se desea cambiar el nivel de suscripción de un usuario, en lugar de crear instancias de un nuevo usuario debe crear una nueva suscripción y asignarlo a un usuario.

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

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

Es un TrialUser realmente tan diferente de un PayingUser? Probablemente no. Se podría ser mejor servido con la agregación en lugar de herencia.

los datos de suscripción se pueden almacenar en una tabla separada (asignada como una entidad) o puede ser almacenado dentro de la tabla Usuarios (asignada como un componente).

El problema está más relacionado con Java que cualquier otra cosa. No se puede cambiar un tipo de tiempo de ejecución de una instancia (en tiempo de ejecución), de modo de hibernación no prevé este tipo de escenarios (a diferencia de los carriles, por ejemplo).

Si quiere desalojar al usuario de la sesión, que tendrá que desalojar a las entidades asociadas. Usted tiene algunas opciones:

  • Mapa de la entidad con la evict = cascada (ver el Sesión # desalojar javadoc)
  • desalojar manualmente todos ENTIDADES asociados (esto puede ser engorroso)
  • Borrar la sesión, desalojando por lo tanto todas las entidades (por supuesto, si no se pierden la memoria caché de sesión local)

He tenido un problema similar en griales, y mi solución es similar a solución de grigory. Copio todos los campos de instancia, incluidas las entidades asociadas, a continuación, elimine el antiguo entidad y escribir la nueva. Si usted no tiene esa cantidad de relaciones que esto puede ser fácil, pero si su modelo de datos es complejo que va a ser mejor usar la solución SQL nativo que ha descrito.

Aquí está la fuente, en caso de que esté interesado:

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
          }
        }
      }
  

El problema está más relacionado con Java   que cualquier otra cosa. No se puede cambiar una   tipo de tiempo de ejecución de una instancia (en   tiempo de ejecución), de modo de hibernación no lo hace   prever este tipo de escenarios   (A diferencia de los carriles por ejemplo).

Me encontré con este post porque soy (o pensaba que era) que tiene un problema similar. He llegado a creer que el problema no es con la mecánica del cambio de tipos en la tecnología, sino que la evolución de los tipos, en general, no es en absoluto fácil o sencillo. La cuestión es que los tipos muy a menudo tienen atributos diferentes. A no ser que se está correlacionando con diferentes tipos sólo por las diferencias de comportamiento, no está claro qué hacer con la información adicional. Por esta razón, tiene sentido para migrar tipos basado en la construcción de nuevos objetos.

El problema no está "relacionada con Java. que está relacionada con el tipo teoría en general y el objeto-relacional más específicamente mapeo aquí. Si su idioma apoya a escribir, me gustaría saber cómo se puede cambiar automáticamente el tipo de un objeto en tiempo de ejecución y mágicamente hacer algo inteligente con la información de sobra. Mientras estamos difundiendo FUD: cuidado los que se toma el consejo de:. Rails es un marco, Ruby es un lenguaje

Cuando dice conversión es probable que tergiversan el problema. Lo que realmente está tratando de hacer en mi opinión es la construcción de instancia de una clase basada en una instancia de la otra clase, por ejemplo:

public PayingUser(TrialUser theUser) {
...
}

A continuación, puede eliminar el usuario de prueba de edad y persisten nuevo usuario el pago.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top