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行为:
- 当你调用
session.merge(ticketEntity)时,Hibernate会遍历关联的TaskEntity:如果这个Task是游离态(比如是从外部传入、未被当前Session管理的实例),且它的ID为0(因为你用的是IDENTITY生成策略,0代表未持久化),Hibernate会判定它是新实体,执行插入操作而非更新。 - 另外,
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
相关产品推荐
相关产品推荐

