【问题标题】:Copied parent inserts childs instead of updating them复制的父级插入子级而不是更新它们
【发布时间】:2020-11-07 18:48:01
【问题描述】:

说明

为了在 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);
    }
}

问题

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

【问题讨论】:

    标签: java spring multithreading hibernate jpa


    【解决方案1】:

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

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

    Blaze-Persistence 是基于 JPA 的查询构建器,它支持 JPA 模型之上的许多高级 DBMS 功能。我在它之上创建了实体视图,以允许在 JPA 模型和自定义接口定义模型之间轻松映射,例如 Spring Data Projections on steroids。这个想法是您以您喜欢的方式定义您的目标结构,并通过 JPQL 表达式将属性(getter)映射到实体模型。由于属性名称用作默认映射,因此您通常不需要显式映射,因为 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 Entity-Views 支持真正的脏跟踪,它允许刷新更新(也只刷新真正改变的状态,例如@DynamicUpdate)而不需要选择。

    【讨论】:

      【解决方案2】:

      我做了一些测试并注意到以下内容。

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

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

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

      解决方案

      没有真正的解决方案...绕过该异常的一种方法是添加自定义@SQLInsert 注释以忽略重复键异常。然后它在主线程上工作得很好。它甚至似乎适用于深度克隆的实体,即使只出现子项的插入语句,从不出现任何删除或删除语句。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2023-03-17
        • 2015-08-14
        • 2013-11-24
        • 1970-01-01
        • 2013-01-31
        • 2021-10-29
        • 2012-09-03
        相关资源
        最近更新 更多