使用@EmbeddedId复合主键时Spring Data JPA实体保存后未托管问题
复合主键实体Spring Data JPA保存后更新不生效问题解析
问题场景
我有一个基于User和Department表使用复合主键的Course实体,在同一@Transactional事务中调用JpaRepository.save()保存实体后,直接对原实体调用setter更新字段,发现更新操作并未生效,看起来新保存的实体未被托管。后来改用save方法返回的实体进行更新则生效,推测是因为主键非空时Spring会执行merge而非persist操作,原实体与返回实体并非同一实例。
实体代码示例
@Entity @Table(name = "user") @Data @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor @ToString public class User{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; } @Entity @Table(name = "department") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Department { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; } @Entity @Table(name = "COURSE") @Data @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public class Course { @EmbeddedId private CourseId courseId; private String name; } @Embeddable @Data @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor public class CourseId implements Serializable { @ManyToOne @JoinColumn(name = "USER_ID") private User user; @ManyToOne @JoinColumn(name = "DEPARTMENT_ID") private Department department; } public interface CourseRepository extends JpaRepository<Course, CourseId> {} // 有问题的事务方法 @Transactional public void createCourse(){ User user=userRepository.findOneByName("user1"); Department department=departmentRepository.findOneByName("department"); CourseId courseId=new CourseId(user,department); Course course=new Course(); course.setCourseId(courseId); courseRepository.save(course);// 保存成功 course.setName("test"); // 这个修改不生效 }
原因分析
- Spring Data JPA save()方法的逻辑:
- 当实体的主键非空且对应数据库中已存在记录时,
save()会调用EntityManager.merge();只有主键为空时才会调用EntityManager.persist()。 - 你的Course实体使用的复合主键CourseId,其中的User和Department都是已存在的托管实体,因此CourseId是非空且存在对应数据库记录的有效主键,触发了merge操作。
- 当实体的主键非空且对应数据库中已存在记录时,
- merge操作的特性:
- merge会创建一个新的托管实体实例,将原游离实体的属性复制到新实例中,然后将新实例纳入EntityManager的托管上下文并返回。
- 你最初创建的Course实体始终处于游离状态,EntityManager不会跟踪它的属性变化,所以对它的修改不会同步到数据库。
解决方案
必须使用save()方法返回的实体进行后续更新,因为这个返回的实例才是被EntityManager托管的对象,对它的修改会在事务提交时自动同步到数据库。修改后的事务方法如下:
@Transactional public void createCourse(){ User user = userRepository.findOneByName("user1"); Department department = departmentRepository.findOneByName("department"); CourseId courseId = new CourseId(user, department); Course course = new Course(); course.setCourseId(courseId); // 接收save返回的托管实体 Course managedCourse = courseRepository.save(course); managedCourse.setName("test"); // 此修改会正常生效 }
注意事项
- 复合主键如果关联了其他实体,要确保关联实体处于托管状态(比如示例中通过Repository查询得到的User和Department),否则可能导致merge逻辑异常。
- 避免对游离状态的实体进行修改,Hibernate不会自动跟踪这些对象的变化,除非再次调用
save()或merge()方法手动同步。
内容的提问来源于stack exchange,提问作者Mohammad Karmi
相关产品推荐
相关产品推荐

