复制的父项插入子项,而不是对其进行更新

时间:2020-11-07 18:48:01

标签: java spring multithreading hibernate jpa

描述

为了在hibernate和jpa中实现多线程,我深入复制了一些实体。会话使用这些副本来添加,删除或更新实体。

问题

到目前为止,它运行良好,但是我遇到了父母/子女关系的问题。 当我更新我的父母时,其“孩子”总是“插入” ...他们从不接受任何形式的更新。这非常糟糕,因为在第二次父更新迭代中我收到了“重复密钥”异常。

我的流程目前看起来如下...

  • 触发了游戏更新
  • 标记为“更新”的深层复制实体。
  • 将这些深层副本实体传递到更新线程(多线程环境)
  • 打开会话,让会话更新它们
  • 等待下一次游戏更新并重复循环

父母子女

那些类代表孩子/父母的关系。

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

问题

知道为什么我的深克隆父母总是插入孩子吗?以及如何在仍然使用多线程的同时防止这种情况(当我不使用克隆的对象时,会发生休眠内部异常)...

2 个答案:

答案 0 :(得分:0)

我猜这是更新配置之前的选择问题。由于您正在使用Session.update,而AFAIK不适用于@SelectBeforeUpdate(value = false),Java Docs中也对此文档进行了说明,因此Hibernate无法知道该对象是否存在,因此它总是尝试插入该对象。

我认为这是Blaze-Persistence Entity Views的完美用例。

Blaze-Persistence是JPA之上的查询构建器,它支持JPA模型之上的许多高级DBMS功能。我在其顶部创建了实体视图,以允许在JPA模型和自定义接口定义的模型之间轻松映射,例如类固醇上的Spring Data Projections。这个想法是您以自己喜欢的方式定义目标结构,并通过JPQL表达式将属性(获取器)映射到实体模型。由于属性名称用作默认映射,因此大多数情况下您不需要显式映射,因为80%的用例都是将DTO作为实体模型的子集。

带有实体视图的投影可能看起来像下面这样简单

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

查询是将实体视图应用于查询的问题,最简单的方法就是按ID查询。

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

但是Spring Data集成允许您像使用Spring Data Projections一样使用它:https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features

List<ChunkDto> findAll();

您还可以使用可更新的实体视图,这将减少获取的数据量,并仅冲回您实际要更改的部分:

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

现在您可以获取该对象,然后在更改状态后将其刷新回数据库:

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

并且它将仅通过插入刷新新的标识,并通过插入到联接表刷新与块的关联,就像您在SQL中看到的那样。 Blaze-Persistence实体视图支持真正的脏跟踪,该跟踪允许刷新更新(并且也仅刷新真正更改的状态,例如@DynamicUpdate),而无需选择。

答案 1 :(得分:0)

我做了一些测试,发现了以下内容。

我遍历这些块并向它们添加新的实体,几乎每一帧。 更新每隔一分钟发生一次,这意味着每个块都有许多不同的新的或旧的被删除的子代。

即使我在主线程上更新/合并这些线程,hibernate也会抛出重复的条目异常。我认为这与我们更新这些大块孩子的时间有关。可能会发生一个孩子被删除,添加,删除,添加然后停留的情况,因此休眠模式尝试复制此行为并失败。

但是我可能是错的,我添加/删除了不同的层叠设置,合并而不是更新,它们都有相同的问题。

解决方案

没有真正的解决方案...绕过该异常的一种方法是添加自定义@SQLInsert批注,以忽略重复的键异常。然后,它在主线程上运行良好。它甚至似乎可以与深度克隆的实体一起使用,即使仅出现针对孩子的插入语句,也绝不会出现任何delete或remove语句。

为什么?我认为这可能会起作用,因为我在自定义sql查询中定义了重复键错误应该发生的情况,这样,每个父级都会插入其子级并覆盖旧值...因为每个子级都是一个父级的子级,所以它可以工作完美无瑕。可能在其他关系中存在问题。

这可以通过合并更新的深度克隆对象或将原始对象替换为更新的深度克隆对象来解决。可能甚至还有我们在这里错过的一些休眠持久性上下文黑客。