merge对比引发的identifier of an instance of entity was altered from .. to ..报错

解决Spring ORM JPA实体ID冲突问题
本文探讨了在使用Spring ORM JPA时,通过session.merge()方法更新实体ID时遇到的问题及解决方法,主要关注于缓存机制对ID更新的影响,并提供了清除缓存的解决方案。

1.问题背景

创建了一个实体CodeItem,包含上级分类parent(类型为CodeItemType)这个属性。在更改某个对象的上级分类parent属性时,调用了session.merge()的方法,程序报错:

org.springframework.orm.jpa.JpaSystemException: identifier of an instance of com.sunyway.sys.dict.domain.CodeItemType was altered from 28 to 48;

而此处的merge方法是这么实现的:

 

 

2.问题解析

session.merge ()方法:该方法将修改表中记录,其所需要的实体状态为脱管状态,但是注意,它并不影响调用方法前后的状态,也即该实体依然是脱管状。session.merge()方法会首先发送一句select语句,去数据库端获取parent持久化标识所对应的表记录;然后自动生成一个持久化状态的parent实体,与脱管状态的parent实体做比较是否有所改变;一旦发生了改变,才会发送update语句执行更新。而按执行顺序,若两句session.merge()方法针对同一个脱管状态的parent实体,那其结果只会执行最后一个session.merge()方法所发出的update语句。

 

在此处,更改CodeItem对象的parent属性后调用merge()方法的实质是:在缓存中本身就存在更改之前的上级分类的代理对象。随后拿到了前台传过来的更改之后的parent.oid这个值。注意此处只有parent的oid这个值,并不是完整的parent实体对象。

 

在调用merge()方法时,session.merge()方法发送了一句查询CodeItem对象的select语句,发现CodeItem对象的parent属性有所更改后就进行了对比。对比的结果是更改之前parent的代理对象与页面传过来的parent对象只有oid属性不同,因此进行合并,想将代理对象的oid更改为页面的oid的值。但实际上,oid是parent所属类CodeItemType的主键,怎么可以进行更改呢?!这便是上面报错的由来。查看底层实现可知一二:

 

 

 

3.问题解决

merge进行比对之前,清除缓存中的代理对象,便可以实现parent的更改。因此,在session.merge()方法之前执行一步session.clear()方法便可以成功实现此次需求。

【网友解答】:原因是缓存导致,你需要在通过id回去parent前 clear()缓存就可以了。 因为在get前缓存中有parent的缓存对象或者是个没有加载数据的代理对象,而且这个对象呗form提交的数据修改了id,此时通过form传过来的idget这个parent对象则直接在缓存中找到了 所以在flush的时候就报修改id的错误了。

<think>我们正在处理用户关于Spring Data JPA与Hibernate中出现的JpaSystemException异常,具体是标识符(identifier)被修改(altered)的问题。 根据经验,这种异常通常发生在实体对象的持久化状态中,当尝试修改实体的主键(标识符)时,Hibernate会抛出此异常。因为主键一旦设定就不应该改变。 可能的原因: 1. 在实体类中,主键字段(通常用@Id标注)被意外修改。 2. 在实体类中,可能使用了可变类型(如String)作为主键,并在实体处于持久化状态时修改了它的值。 解决方案: 1. 确保在实体对象被持久化(例如,通过save方法保存到数据库)后,不再修改其主键字段。 2. 如果主键是自动生成的(如使用@GeneratedValue),则不应该手动设置或修改它。 3. 检查代码中是否有地方在实体被管理(managed)状态时修改了主键。 另外,注意Hibernate的session状态:当一个实体被关联到Hibernate session(例如,通过getOne或findById方法获取)时,它处于持久化状态。此时修改其主键会导致异常。 示例代码: 假设有一个实体类User: ```java @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // getters and setters } ``` 在以下代码中,如果尝试修改id,就会抛出异常: ```java @Service public class UserService { @Autowired private UserRepository userRepository; public void updateUserName(Long userId, String newName) { User user = userRepository.findById(userId).orElseThrow(); // 错误:尝试修改主键 user.setId(100L); // 这会导致JpaSystemException: identifier altered user.setName(newName); userRepository.save(user); } } ``` 因此,修复方法就是避免修改主键。 如果用户确实需要修改主键,那么正确的做法是先删除旧记录,再插入一条新记录(使用新的主键值)。但注意,这可能会带来关联数据的问题(如外键约束)。 此外,如果用户遇到的是级联操作中相关实体的主键被修改,也需要检查相关实体的主键是否被修改。 根据以上分析,我们给出回答。</think>### Spring Data JPA Hibernate 标识符被修改异常分析与解决 当使用 Spring Data JPA 和 Hibernate 时,遇到 `JpaSystemException: identifier of an instance was altered` 异常,通常表示**实体对象的主键(ID)在持久化状态中被非法修改**。以下是详细分析和解决方案: --- ### 原因分析 1. **主键不可变性** Hibernate 要求实体主键在对象被持久化后(关联到 Session 或 EntityManager)保持不可变。尝试修改主键会触发此异常[^1]。 2. **典型场景** ```java @Entity public class User { @Id // 主键字段 private Long id; // 其他字段... } // 错误代码示例:修改持久化对象的主键 User user = userRepository.findById(1L).get(); user.setId(2L); // 触发异常! userRepository.save(user); ``` 3. **根本原因** Hibernate 使用主键跟踪实体状态。修改主键会破坏 Hibernate 的内部标识机制,导致状态不一致[^1]。 --- ### 解决方案 #### ✅ 方法 1:避免直接修改主键 - **原则**:主键应在对象创建时设置,后续永不修改 - **正确做法**: ```java // 创建新对象代替修改主键 User newUser = new User(); newUser.setId(2L); // 仅允许在瞬态(未持久化)状态下设置ID newUser.setName("New Name"); userRepository.save(newUser); // 删除旧对象(如需替换) userRepository.deleteById(1L); ``` #### ✅ 方法 2:使用业务键代替主键修改 若需"修改ID",实际应通过业务逻辑实现: ```java // 1. 创建新实体 User newUser = copyProperties(oldUser); newUser.setId(newId); // 2. 保存新实体 userRepository.save(newUser); // 3. 删除旧实体 userRepository.delete(oldUser); // 4. 更新关联关系(需事务管理) orderRepository.updateUserId(oldUser.getId(), newUser.getId()); ``` #### ✅ 方法 3:检查级联操作 当主键被级联操作意外修改时: ```java @Entity public class Order { @Id private Long id; @ManyToOne @JoinColumn(name = "user_id") private User user; // 关联实体 } // 错误:修改关联实体的主键 Order order = orderRepository.findById(orderId).get(); order.getUser().setId(newId); // 触发异常! ``` **修复**:修改关联关系而非主键: ```java User newUser = userRepository.findById(newId).get(); order.setUser(newUser); // 正确:替换关联引用 ``` --- ### 预防措施 1. **实体设计规范**: - 主键字段添加 `final` 修饰符(如适用) - 使用 `@GeneratedValue` 自动生成不可变ID ```java @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private final Long id; // final 防止修改 ``` 2. **代码审查重点**: - 检查所有 `setId()` 调用是否发生在持久化对象上 - 使用 `@Immutable` 注解标记不可变实体 ```java @Entity @Immutable // Hibernate 特殊注解 public class AuditLog { ... } ``` 3. **Hibernate 配置增强**: ```properties # 开启调试日志定位非法修改 spring.jpa.properties.hibernate.event.merge.entity_state_logging=true ``` > **关键原则**:数据库主键对应 OOP 中的对象标识,修改它等同于创建新对象。业务上需要"修改ID"的需求,应通过创建新实体+迁移数据实现[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值