El padre copiado inserta los hijos en lugar de actualizarlos

Nov 08 2020

Descripción

Para implementar subprocesos múltiples en hibernate & jpa, realizo una copia profunda de algunas de mis entidades. Esas copias son utilizadas por las sesiones para agregar, eliminar o actualizar las entidades.

Problema

Funcionó bien hasta ahora, pero encontré un problema con las relaciones entre padres e hijos. Cuando actualizo a mi padre, sus hijos "siempre" se insertan ... nunca reciben ningún tipo de actualización. Y esto es bastante malo, porque recibo una excepción de "Clave duplicada" en la segunda iteración de actualización principal.

Actualmente, mi flujo se parece al siguiente ...

  • Actualización del juego activada
  • Entidades de copia profunda que están marcadas como "actualización".
  • Pase esas entidades de copia profunda al hilo de actualización (entorno multiproceso)
  • Sesión abierta, deja que la sesión los actualice
  • Espere la próxima actualización del juego y repita el ciclo

Padre-hijo

Y esas clases representan la relación padre / hijo.

/**
 * A component which marks a {@link com.artemis.Entity} as a chunk and stores its most valuable informations.
 */
@Entity
@Table(name = "chunk", uniqueConstraints = {@UniqueConstraint(columnNames={"x", "y"})}, indexes = {@Index(columnList = "x,y")})
@Access(value = AccessType.FIELD)
@SelectBeforeUpdate(false)
public class Chunk extends HibernateComponent{

    public int x;
    public int y;
    public Date createdOn;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "chunk_identity", joinColumns = @JoinColumn(name = "identity_id"), inverseJoinColumns = @JoinColumn(name = "id"), inverseForeignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
    @Fetch(FetchMode.JOIN)
    @BatchSize(size = 50)
    public Set<Identity> inChunk = new LinkedHashSet<>();

    @Transient
    public Set<ChunkLoader> loadedBy = new LinkedHashSet<>();

    public Chunk() {}
    public Chunk(int x, int y, Date createdOn) {
        this.x = x;
        this.y = y;
        this.createdOn = createdOn;
    }
}

/**
 * Represents a ID of a {@link com.artemis.Entity} which is unique for each entity and mostly the database id
 */
@Entity
@Table(name = "identity")
@Access(AccessType.FIELD)
@SQLInsert(sql = "insert into identity(tag, typeID, id) values(?,?,?) ON DUPLICATE KEY UPDATE id = VALUES(id), tag = values(tag), typeID = values(typeID)")
@SelectBeforeUpdate(value = false)
public class Identity extends Component {

    @Id public long id;
    public String tag;
    public String typeID;

    public Identity() {}
    public Identity(long id, String tag, String typeID) {
        this.id = id;
        this.tag = tag;
        this.typeID = typeID;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        var identity = (Identity) o;
        return id == identity.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, tag, typeID);
    }
}

Pregunta

¿Alguna idea de por qué mi padre clonado profundo siempre inserta sus hijos? ¿Y cómo podría evitar esto mientras sigo usando subprocesos múltiples (cuando no uso objetos clonados, se produce una excepción interna de hibernación) ...

Respuestas

ChristianBeikov Nov 09 2020 at 15:37

Supongo que el problema es seleccionar antes de actualizar la configuración. Dado que está utilizando Session.update, con lo que AFAIK no funciona y @SelectBeforeUpdate(value = false)que también está documentado en Java Docs, Hibernate no tiene forma de saber si el objeto existe, por lo que siempre intenta insertarlo.

Creo que este es un caso de uso perfecto para las vistas de entidad Blaze-Persistence .

Blaze-Persistence es un generador de consultas sobre JPA que admite muchas de las funciones avanzadas de DBMS sobre el modelo JPA. Creé Entity Views encima para permitir un mapeo fácil entre los modelos JPA y los modelos definidos por la interfaz personalizada, algo así como Spring Data Projections con esteroides. La idea es que defina su estructura de destino de la manera que desee y asigne atributos (captadores) a través de expresiones JPQL al modelo de entidad. Dado que el nombre del atributo se usa como asignación predeterminada, la mayoría de las veces no necesita asignaciones explícitas, ya que el 80% de los casos de uso es tener DTO que son un subconjunto del modelo de entidad.

Una proyección con vistas de entidad podría parecer tan simple como lo siguiente

@EntityView(Chunk.class)
interface ChunkDto {
    @IdMapping
    Long getId();
    int getX();
    int getY();
    @Mapping(fetch = MULTISET) // This is a much more efficient fetch strategy
    Set<IdentityDto> getIdentities();
}
@EntityView(Identity.class)
interface IdentityDto {
    @IdMapping
    Long getId();
    String getTag();
    String getTypeID();
}

La consulta es una cuestión de aplicar la vista de entidad a una consulta, la más simple es solo una consulta por id.

ChunkDto dto = entityViewManager.find(entityManager, ChunkDto.class, id);

Pero la integración de Spring Data le permite usarla casi como Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

List<ChunkDto> findAll();

También puede hacer uso de vistas de entidades actualizables que reducen la cantidad de datos recuperados y eliminan solo las partes que realmente desea cambiar:

@CreatableEntityView
@UpdatableEntityView
@EntityView(Chunk.class)
interface ChunkDto {
    @IdMapping
    Long getId();
    void setId(Long id);
    int getX();
    int getY();
    @Mapping(fetch = MULTISET) // This is a much more efficient fetch strategy
    Set<IdentityDto> getIdentities();
    default void addIdentity(String tag, String typeID) {
        IdentityDto dto = evm().create(IdentityDto.class);
        dto.setTag(tag);
        dto.setTypeID(typeID);
        getIdentities().add(dto);
    }
    EntityViewManager evm();
}
@CreatableEntityView
@UpdatableEntityView
@EntityView(Identity.class)
interface IdentityDto {
    @IdMapping
    Long getId();
    void setId(Long id);
    String getTag();
    void setTag(String tag);
    String getTypeID();
    void setTypeID(String typeID);
}

Ahora puede recuperar ese objeto y luego, después de cambiar el estado, devolverlo a la base de datos:

ChunkDto o = repository.findOne(123L);
o.getIdentities().addIdentity("my-tag", "my-type-id");
repository.save(o);

Y solo eliminará la nueva identidad a través de una inserción y la asociación al fragmento a través de una inserción a la tabla de combinación, como verá en el SQL. Blaze-Persistence Entity-Views admite un seguimiento sucio real que permite descargar actualizaciones (y también solo elimina el estado que realmente cambió, es decir, me gusta @DynamicUpdate) sin la necesidad de selecciones.

genaray Nov 11 2020 at 02:37

Hice algunas pruebas y noté lo siguiente.

Repito los fragmentos y les agrego nuevas entidades, casi en cada cuadro. La actualización se produce cada minuto una vez, lo que significa que cada fragmento tiene muchos, muchos elementos secundarios eliminados nuevos o antiguos diferentes.

Incluso cuando actualizo / fusiono los del mainthread, hibernate arroja una excepción de entrada duplicada. Creo que esto está relacionado con la cantidad de veces que actualizamos esos fragmentos de niños. Puede suceder que un niño sea eliminado, agregado, eliminado, agregado y luego se quede, por lo que hibernate intenta replicar este comportamiento y falla.

Pero podría estar equivocado, agregué / eliminé diferentes configuraciones de cascada, fusioné en lugar de actualizar y todas tenían el mismo problema.

Solución

No hay una solución real ... una forma de evitar esa excepción es agregar anotaciones @SQLInsert personalizadas para ignorar la excepción de clave duplicada. Entonces funciona muy bien en el hilo principal. Incluso parece funcionar con entidades clonadas en profundidad, incluso si solo aparecen declaraciones de inserción para los niños, nunca ninguna declaración de eliminación o eliminación.

Por qué ? Creo que podría funcionar porque defino en la consulta sql personalizada lo que debería suceder en un error de clave de duplicación, de esta manera, cada padre inserta su hijo y sobrescribe los valores antiguos ... porque cada hijo es solo el hijo de uno de los padres, funciona perfecto. Podría tener problemas en otras relaciones.

Esto podría resolverse fusionando el objeto clonado profundo actualizado o reemplazando el original con el objeto clonado profundo actualizado. Probablemente hay incluso algún truco de contexto de persistencia de hibernación que nos perdimos aquí.