代码之家  ›  专栏  ›  技术社区  ›  genaray

我们如何更新深度克隆实体?

  •  1
  • genaray  · 技术社区  · 5 年前

    描述

    我正在开发一个小型java游戏服务器。。。为了在另一个线程中更新和保存游戏,我不得不深度克隆我的一些实体。其他发生内部休眠异常时: "ConcurrentModificationException" when updating my entities

    所以我的流程现在看起来是这样的:

    • 标记要更新的游戏实体
    • 将这些实体传递到另一个线程
    • 克隆那些实体
    • 对克隆的实体调用“session.update”
    • 一分钟后重复

    它在简单类中运行良好,但在关系方面存在巨大问题。

    问题

    当我深度克隆我的“chunk”实体(见下文)时,它的集合(inChunk)也会被深度克隆。 我使用深度克隆实体并将其传递给“session.update”。

    在更新期间,区块始终插入其集合子项。它从不更新它们。 因为我每分钟都会重复这个更新过程(见上文),所以在第二个更新周期会出现“重复条目”异常。

    
    // Run the database operation for updating the entities async in a new thread, return updated entities once done
            return CompletableFuture.runAsync(() -> {
    
                var session = database.openSession();
                session.beginTransaction();
    
                try {
    
                    // Save entities
                    for (var entity: entities)
                        session.update(entity);
    
                    session.flush();
                    session.clear();
    
                    session.getTransaction().commit();
                } catch (Exception e){
    
                    var messageComposer = new ExceptionMessageComposer(e);
                    GameExtension.getInstance().trace("Update : "+messageComposer.toString());
                    session.getTransaction().rollback();
                }
    
                session.close();
            }).thenApply(v -> entities);
    
    @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;
        }
    
    }
    

    问题

    为什么hibernate总是在不检查孩子是否已经插入的情况下插入我的孩子?我能做些什么来预防/修复这个问题?

    我尝试过的事情

    • 删除“@SelectBeforeUpdate”批注
    • 将级联添加到“Chunk.InChunk”与“All”或“Merge”的关系中
    • 运行“session.merge”而不是“session.update”,这会导致相同的重复条目异常,只需忽略其子项已插入的事实。

    他们都没有工作

    免责声明

    我需要克隆实体,否则会导致内部休眠异常,请参见上面的链接。

    我正在使用一个名为“DeppClone”的库 https://github.com/kostaskougios/cloning 在另一个线程中克隆我的实体。

    如果需要更多信息,请写评论。这是一个复杂的问题,很难一概而论,但我希望我已经正确地描述了它。

    0 回复  |  直到 5 年前
        1
  •  1
  •   motzmann    5 年前

    你应该考虑实现两大改变:

    1. 关注点分离(读写)
    2. 选择一致性原则

    使用#1,您不必再处理ConcurrentModificationException。修改实体只涉及一个组件或服务。

    第二个组件或服务仅在单独的上下文中读取实体。所有实体必须是不变的,以避免实施事故。

    两者都需要一个契约或接口(在一般意义上,不是Java接口),这将把你带到#2。一旦发生更改,只读上下文中的任何加速技巧都必须刷新缓存或重新读取/合并更改。根据 CAP-theorem 你必须牺牲三者中的一个,这取决于你喜欢的一致性策略。由于没有关于写入、实体和读取约束数量的提示,我无法给出更详细的建议。

    如果我现在必须实现它,那么至少有3个模块(Java 11/Jigsaw):

    • API模块只包含接口,以强制执行以下两个模块中的统一get方法
    • 具有Hibernate magic的读卡器模块,用于读取实体并将其提供给其他人(通过REST、RCP等),向编写器注册自身,以便在发生更改时进行某种刷新。

    读卡器模块还可以在启动时读取数据库,并使用编写器生成的事件。这些事件将是某种命令,在读卡器模块的内存中更改,而不是从数据库中重新读取。因此,您可以通过简单的内存缓存和一些事件建模来完全取消Hibernate。通过使用BlockingQueues和ConcurrentHashMap(作为缓存),这仍然可以在单个JVM中工作。简单的JDBC足以引导您的模型。

    如果通过深度复制和一点线程就这么简单的话。其他人也会这样做。由于有很多关于并发持久性的模型、策略和模式,我建议重构。