为什么hibernate在保存子项时需要保存父项,即使父项没有变化也会导致OptimisticLockException?

Chr*_*ris 6 java hibernate jpa

我们试图在短时间内拯救许多孩子,并且休眠状态不断给出 OptimisticLockException。这是该案例的一个简单示例:

University
id
name
audit_version

Student 
id
name 
university_id
audit_version
Run Code Online (Sandbox Code Playgroud)

其中 university_id 可以为 null。

java对象看起来像:

@Entity
@Table(name = "university")
@DynamicUpdate
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class University {

    @Id
    @SequenceGenerator(name = "university_id_sequence_generator", sequenceName = "university_id_sequence", allocationSize = 1)
    @GeneratedValue(strategy = SEQUENCE, generator = "university_id_sequence_generator")
    @EqualsAndHashCode.Exclude
    private Long id;

    @Column(name = "name")
    private String name;
    @Version
    @Column(name = "audit_version")
    @EqualsAndHashCode.Exclude
    private Long auditVersion;

    @OptimisticLock(excluded = true)
    @OneToMany(mappedBy = "student")
    @ToString.Exclude
    private List<Student> student;
}

@Entity
@Table(name = "student")
@DynamicUpdate
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class Student {

    @Id
    @SequenceGenerator(name = "student_id_sequence_generator", sequenceName = "student_id_sequence", allocationSize = 1)
    @GeneratedValue(strategy = SEQUENCE, generator = "student_id_sequence_generator")
    @EqualsAndHashCode.Exclude
    private Long id;

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

    @Version
    @Column(name = "audit_version")
    @EqualsAndHashCode.Exclude
    private Long auditVersion;

    @OptimisticLock(excluded = true)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "university_id")
    @ToString.Exclude
    private University university;
}
Run Code Online (Sandbox Code Playgroud)

好像我们分配大学然后保存Student时,如果我们在短时间内做4个以上,我们就会得到OptimisticLockException。即使大学在数据库级别没有改变,休眠似乎也在大学表上创建更新版本。

更新:保存学生的代码

    Optional<University> universityInDB = universidyRepository.findById(universtityId);
    universityInDB.ifPresent(university -> student.setUniversity(university);
    Optional<Student> optionalExistingStudent = studentRepository.findById(student);
    if (optionalExistingStudent.isPresent()) {
        Student existingStudent = optionalExistingStudent.get();
        if (!student.equals(existingStudent)) {
            copyContentProperties(student, existingStudent);
            studentToReturn = studentRepository.save(existingStudent);
        } else {
            studentToReturn = existingStudent;
        }
    } else {
        studentToReturn = studentRepository.save(student);
    }

private static final String[] IGNORE_PROPERTIES = {"id", "createdOn", "updatedOn", "auditVersion"};
public void copyContentProperties(Object source, Object target) {
    BeanUtils.copyProperties(source, target, Arrays.asList(IGNORE_PROPERTIES)));
}
Run Code Online (Sandbox Code Playgroud)

我们尝试了以下

@OptimisticLock(excluded = true) 不起作用,仍然给出乐观锁异常。

@JoinColumn(name = "university_id", updatable=false) 只处理更新,因为我们不保存更新

@JoinColumn(name = "university_id", insertable=false) 工作但不保存关系并且 university_id 始终为空

更改级联行为。唯一一个似乎有意义的值是Cascade.DETACH,但给出一个 org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object 引用未保存的瞬态实例 - 在刷新之前保存瞬态实例。

我们想到的其他解决方案但不确定选择什么

  1. 给客户端一个 409(冲突)错误

在 409 之后,客户端必须重试他的帖子。对于通过队列发送的对象,队列稍后将重试该条目。我们不希望我们的客户管理这个错误

  1. 在 OptimisticLockException 之后重试

它不干净,因为当条目来自队列时,我们已经在做,但可能是迄今为止最好的解决方案。

  1. 使关系的父所有者

如果没有大量关系,这可能没问题,但是我们有可能在 100 中甚至在 1000 中的情况,这将使对象变得很大,以便在队列中或通过 Rest 调用发送。

  1. 悲观锁

我们的整个数据库目前处于 optimisticLocking 中,到目前为止我们设法防止了这些乐观锁定的情况,我们不想仅仅因为这种情况而改变我们的整个锁定策略。也许对模型的那个子集强制悲观锁定,但我还没有看是否可以完成。

Tar*_*ras 5

除非您需要它,否则它不需要它。 做这个:

    University universityProxy = universidyRepository.getOne(universityId);
    student.setUniversity(universityProxy);
Run Code Online (Sandbox Code Playgroud)

为了分配 a,University您不必将University实体加载到上下文中。因为从技术上讲,您只需要使用适当的外键()保存学生记录university_id。因此,当您拥有一个时university_id,您可以使用存储库方法创建一个 Hibernate 代理getOne()


解释

Hibernate 的底层相当复杂。**当您将实体加载到上下文时,它会创建其字段的快照副本,并跟踪您是否更改其中任何一个**。它的作用更多......所以我想这个解决方案是最简单的解决方案,它应该有所帮助(除非您在同一会话范围内的其他地方更改“university”对象)。很难说其他部分何时被隐藏。

潜在问题

  • 错误的@OneToMany映射
    @OneToMany(mappedBy = "student") // should be (mappedBy = "university")
    @ToString.Exclude
    private List<Student> student;
Run Code Online (Sandbox Code Playgroud)
  • 集合应该被初始化。Hibernate 使用它自己的集合实现,并且您不应该手动设置字段。只调用像add()or remove(), or 这样的方法clear()
    private List<Student> student; // should be ... = new ArrayList<>();
Run Code Online (Sandbox Code Playgroud)

*总体来说有些地方不太清楚,比如studentRepository.findById(student);。因此,如果你想得到正确的答案,最好把你的问题说清楚。