You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Hibernate OneToMany集合更新导致原有实体被重复保存为新ID记录的问题

Hibernate OneToMany集合更新导致原有实体被重复保存为新ID记录的问题

首先,先整理你提供的实体、仓库及服务代码,方便问题分析:

你的实体与业务代码

TaskEntity

@Setter
@Getter
@Entity
@Table(name = "Task")
@NoArgsConstructor
public class TaskEntity implements AbstractEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "mapName")
    private String mapName;

    @Column(name = "mapPath")
    private String mapPath;

    @Column(name = "mapCost")
    private String mapCost;

    @Column(name = "mapCostResult")
    private String mapCostResult;

    @Enumerated(EnumType.STRING)
    @Column(name = "taskStatus")
    private TaskStatus taskStatus;

    @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinColumn(name = "ticketId")
    private TicketEntity ticket;

    @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
        name = "Builder_Task",
        joinColumns = @JoinColumn(name = "taskId"),
        inverseJoinColumns = @JoinColumn(name = "builderDiscordId")
    )
    private Set<BuilderEntity> builders = new HashSet<>();

    public @NotNull TaskEntity addBuilder(@NotNull BuilderEntity builderEntity) {
        builders.add(builderEntity);
        return this;
    }

    public @NotNull TaskEntity removeBuilder(@NotNull BuilderEntity builderEntity) {
        builders.remove(builderEntity);
        return this;
    }
}

TicketEntity

@Setter
@Getter
@Entity
@Table(name = "Ticket")
@NoArgsConstructor
public class TicketEntity implements AbstractEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @Column(name = "channelId", unique = true)
    private String channelId;

    @Column(name = "creatorName")
    private String creatorName;

    @Column(name = "creationDate")
    @CreationTimestamp
    private Timestamp creationDate;

    @Column(name = "deadline", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private Timestamp deadline;

    @Column(name = "available")
    private boolean available;

    @Column(name = "mapsAmount")
    private int mapsAmount;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true)
    @OrderColumn(name = "ticketId")
    private Set<TaskEntity> tasks = new HashSet<>();

    public @NotNull TicketEntity addTask(@NotNull TaskEntity taskEntity) {
        tasks.add(taskEntity);
        return this;
    }

    public @NotNull TicketEntity removeTask(@NotNull TaskEntity taskEntity) {
        tasks.remove(taskEntity);
        return this;
    }
}

TicketRepository & TicketService

// TicketRepository
public class TicketRepository implements CRUDRepository<TicketEntity, Integer> {
    // Only needed function for question
    @Override
    public @NotNull TicketEntity update(@NotNull TicketEntity ticketEntity) {
        Transaction transaction = null;
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            transaction = session.beginTransaction();
            session.merge(ticketEntity);
            transaction.commit();
            return ticketEntity;
        } catch (Exception e) {
            if (transaction != null) {
                transaction.rollback();
            }
            throw e;
        }
    }
}
// TicketService
public class TicketService {

      public @NotNull Optional<TicketEntity> addTask(@NotNull Integer ticketId, @NotNull TaskEntity task) {
          return updateTicketEntity(ticketId, ticketEntity -> {
          ticketEntity.addTask(task);
          task.setTicket(ticketEntity);
      });
    }

    private @NotNull Optional<TicketEntity> updateTicketEntity(@NotNull Integer ticketId, @NotNull Consumer<TicketEntity> updater) {
        Optional<TicketEntity> entity = findById(ticketId);
        return Preconditions.checkAndReturn(entity.isPresent(),
            () -> {
                TicketEntity ticketEntity = entity.get();
                updater.accept(ticketEntity);
                TicketEntity savedEntity = ticketRepository.update(ticketEntity);
                ticketCache.put(ticketId, savedEntity);
                return Optional.of(savedEntity);
            },
        Optional::empty);
    }
}

问题根源分析

你遇到的重复插入问题,核心原因是Hibernate对游离态(detached)实体的merge行为:

  1. 当你调用session.merge(ticketEntity)时,Hibernate会遍历关联的TaskEntity:如果这个Task是游离态(比如是从外部传入、未被当前Session管理的实例),且它的ID为0(因为你用的是IDENTITY生成策略,0代表未持久化),Hibernate会判定它是新实体,执行插入操作而非更新。
  2. 另外,Set<TaskEntity>依赖equals/hashCode判断元素是否存在,如果TaskEntity没有正确实现这两个方法,Set会把同一个Task识别为不同元素,进而触发重复添加。

具体解决方案

根据你的业务场景,推荐以下几种解决方式:

1. 确保关联的Task处于托管(Managed)状态

如果要添加已存在的Task到Ticket,不要直接传入游离态的Task实例,先从数据库加载它(让它成为当前Session管理的托管态):

// 修改TicketService的addTask方法,支持添加已存在的Task
public @NotNull Optional<TicketEntity> addExistingTask(@NotNull Integer ticketId, @NotNull Integer taskId) {
    // 先从数据库加载Task(托管态)
    Optional<TaskEntity> existingTask = taskRepository.findById(taskId);
    return existingTask.flatMap(task -> updateTicketEntity(ticketId, ticketEntity -> {
        ticketEntity.addTask(task);
        task.setTicket(ticketEntity);
    }));
}

如果是添加全新的Task,确保它的ID为0(符合IDENTITY策略的未持久化标识),此时merge会正常插入新Task,不会重复。

2. 给TaskEntity实现正确的equals/hashCode

Set依赖这两个方法去重,如果没有正确实现,集合中会出现重复Task,进而触发重复插入。建议基于数据库主键实现:

// 在TaskEntity中添加以下实现
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    TaskEntity that = (TaskEntity) o;
    return id == that.id;
}

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

注意:IDENTITY策略下,未持久化的Task ID为0,这会导致equals/hashCode在持久化前后变化。如果业务允许,可以改用UUID作为主键(生成策略用GenerationType.UUID),实体创建时就有唯一标识,从根源避免这个问题。

3. 调整Merge逻辑或级联策略

你当前Ticket的OneToMany级联是CascadeType.ALL(包含MERGE),会自动级联merge所有关联Task。可以通过以下方式优化:

  • 手动加载托管态Ticket,同步tasks集合:替代直接merge游离态Ticket,这种方式更可控:
    // 修改TicketRepository的update方法
    @Override
    public @NotNull TicketEntity update(@NotNull TicketEntity ticketEntity) {
        Transaction transaction = null;
        try (Session session = HibernateUtil.getSessionFactory().openSession()) {
            transaction = session.beginTransaction();
            // 加载数据库中的Ticket(托管态)
            TicketEntity managedTicket = session.get(TicketEntity.class, ticketEntity.getId());
            // 同步tasks集合
            managedTicket.getTasks().clear();
            for (TaskEntity task : ticketEntity.getTasks()) {
                task.setTicket(managedTicket);
                managedTicket.getTasks().add(task);
                // 新Task手动持久化,已有Task由Hibernate自动管理
                if (task.getId() == 0) {
                    session.persist(task);
                }
            }
            transaction.commit();
            return managedTicket;
        } catch (Exception e) {
            if (transaction != null) transaction.rollback();
            throw e;
        }
    }
    

4. 验证双向关联维护

你在addTask中已经做了task.setTicket(ticketEntity),这部分是正确的——双向关联必须同时维护两边的引用,否则会导致关联关系不持久或数据不一致。这部分你当前的代码没问题,但要确保所有修改关联的操作都同步两边。


总结

最常见的解决场景是:

  • 若添加已存在的Task:先从数据库加载它(托管态)再添加到Ticket;
  • 若添加新Task:确保ID为0,同时维护好双向关联;
  • 给TaskEntity实现正确的equals/hashCode,避免集合中出现重复元素。

通过以上调整,就能解决Task被重复插入成新记录的问题。


备注:内容来源于stack exchange,提问作者GeoD

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 10:44:50