Why does hibernate need to save the parent when saving the child and cause a OptimisticLockException even if there no change to the parent?

huangapple go评论72阅读模式
英文:

Why does hibernate need to save the parent when saving the child and cause a OptimisticLockException even if there no change to the parent?

问题

我们试图在很短的时间内保存许多孩子,但是使用 hibernate 时不断出现 OptimisticLockException 异常。这里是一个简单的例子:

University
id
name
audit_version

Student 
id
name 
university_id
audit_version

其中 university_id 可以为 null。

Java 对象如下:

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

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

似乎当我们分配了 university 并保存 Student 后,如果在短时间内执行超过 4 次,就会出现 OptimisticLockException 异常。似乎 hibernate 在 University 表上创建了更新版本,尽管 University 在数据库级别上并没有更改。

更新:保存学生的代码

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);
}

我们尝试了以下方法:

  • @OptimisticLock(excluded = true):无效,仍然会出现 optimistic lock 异常。
  • @JoinColumn(name = "university_id", updatable=false):只在更新时有效,因为我们不在更新时保存。
  • @JoinColumn(name = "university_id", insertable=false):虽然有效,但不会保存关系,university_id 总是 null。

更改级联行为。唯一一个有意义的值似乎是 Cascade.DETACH,但会导致 org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing. 异常。

其他可能的解决方案:

  1. 为客户端返回 409(冲突)错误:在 409 后,客户端必须重试 POST 请求,但我们不希望客户端管理此错误。
  2. 在 OptimisticLockException 后重试:虽然不是很干净,但当条目来自队列时,我们已经在重试,这可能是目前为止最好的解决方案。
  3. 使父级成为关系的所有者:如果关系不多,这可能是可行的,但在某些情况下可能会有数百甚至数千个关系,这会导致对象过大,无法通过队列或 REST 调用发送。
  4. 悲观锁:尽管我们整个数据库目前都在使用 optimistic locking,但到目前为止我们已经成功防止了这些 optimistic locking 的情况,不希望仅因为这个案例就改变整个锁策略。可能只为模型的这个子集强制使用悲观锁,但我还没有查看是否可行。
英文:

We are trying to save many child in a short amount of time and hibernate keep giving OptimisticLockException.
Here a simple exemple of that case:

University
id
name
audit_version

Student 
id
name 
university_id
audit_version

Where university_id can be null.

The java object look like:

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

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

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

    @OptimisticLock(excluded = true)
    @OneToMany(mappedBy = &quot;student&quot;)
    @ToString.Exclude
    private List&lt;Student&gt; student;
}

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

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

    @Column(name = &quot;name&quot;)
    private String name;

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

    @OptimisticLock(excluded = true)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;university_id&quot;)
    @ToString.Exclude
    private University university;
}

It seem when we assign university and then save Student, if we do more than 4 in a short amount of time we will get the OptimisticLockException.
It seem hibernate is creating update version on the University table even though the University didn't change at the db level.

UPDATE: code that save the student

    Optional&lt;University&gt; universityInDB = universidyRepository.findById(universtityId);
	universityInDB.ifPresent(university -&gt; student.setUniversity(university);
	Optional&lt;Student&gt; 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 = {&quot;id&quot;, &quot;createdOn&quot;, &quot;updatedOn&quot;, &quot;auditVersion&quot;};
public void copyContentProperties(Object source, Object target) {
    BeanUtils.copyProperties(source, target, Arrays.asList(IGNORE_PROPERTIES)));
}

We tried the following

@OptimisticLock(excluded = true)
Doesn't work, still give the optimistic lock exception.

@JoinColumn(name = &quot;university_id&quot;, updatable=false)
only work on a update since we don't save on the update

@JoinColumn(name = &quot;university_id&quot;, insertable=false)
work but don't save the relation and university_id is always null

Change the Cascade behaviour.
The only one value that seem to made sense was Cascade.DETACH, but give a org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing.

Other solution we though of but are not sure what to pick

  1. Give the client a 409 (Conflict) error

After the 409 the client must retry his post.
for a object sent via the queue the queue will retry that entry
later.
We don't want our client to manage this error

  1. Retry after a OptimisticLockException

It's not clean since when the entry come from the queue we already doing it but might be the best solution so far.

  1. Make the parent owner of the relationship

This might be fine if there are not a big number of relation, but we have case that might go in the 100 even in the 1000, which
will
make the object to big to be sent on a queue or via a Rest call.

  1. Pessimistic Lock

Our whole db is currently in optimisticLocking
and we managed to prevent these case of optimisticLocking so far, we
don't want to change our whole locking strategy just because of this
case. Maybe force pessimistic locking for that subset of the model
but I haven't look if it can be done.

答案1

得分: 5

除非你需要,否则不需要它。
按照以下步骤操作:

    University universityProxy = universidyRepository.getOne(universityId);
    student.setUniversity(universityProxy);

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


<h3>解释</h3>
Hibernate 在内部相当复杂。当你将一个实体加载到上下文中时,它会创建一个字段的快照副本,并跟踪你是否更改了其中任何字段。它还做了更多的事情... 因此,我想这个解决方案是最简单的,它应该会有所帮助(除非你在同一会话范围内的其他地方更改了 university 对象)。其他部分是否隐藏很难说。


<h3>潜在问题</h3>

  • 错误的 @OneToMany 映射
    @OneToMany(mappedBy = &quot;student&quot;) // 应该是 (mappedBy = &quot;university&quot;)
    @ToString.Exclude
    private List&lt;Student&gt; student;
  • 集合应该被初始化。Hibernate 使用自己的集合实现,你不应该手动设置字段。只能调用诸如 add()remove()clear() 等方法
    private List&lt;Student&gt; student; // 应该是 ... = new ArrayList&lt;&gt;();

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

英文:

It does NOT need it unless you need it.
Do this:

    University universityProxy = universidyRepository.getOne(universityId);
    student.setUniversity(universityProxy);

In order to assign a University you don't have to load a University entity into the context. Because technically, you just need to save a student record with a proper foreign key (university_id). So when you have a university_id, you can create a Hibernate proxy using the repository method getOne().


<h3>Explanation</h3>
Hibernate is pretty complex under the hood. When you load an entity to the context, it creates a snapshot copy of its fields and keeps track if you change any of it. It does much more... So I guess this solution is the simplest one and it should help (unless you change the university object somewhere else in the scope of the same session). It's hard to say when other parts are hidden.


<h3>Potential issues</h3>

  • wrong @OneToMany mapping
    @OneToMany(mappedBy = &quot;student&quot;) // should be (mappedBy = &quot;university&quot;)
    @ToString.Exclude
    private List&lt;Student&gt; student;
  • the collection should be initialized. Hibernate uses it's own impls of collections, and you should not set fields manually. Only call methods like add() or remove(), or clear()
    private List&lt;Student&gt; student; // should be ... = new ArrayList&lt;&gt;();

*overall some places are not clear, like studentRepository.findById(student);. So if you want to have a correct answer it's better to be clear in your question.

答案2

得分: 3

如果您从 Hibernate 启用查询日志,查看一下您的 ORM 所执行的查询可能会很有价值。您很可能会意识到您的 ORM 做了太多的事情。

在您的应用程序属性或配置文件中启用 hibernate.show_sql=true

如果您对一个 Student 的单次更新变成了对一个 University 的更新,然后又更新了其包含的所有 Students,这也不足为奇。每个实体都会获得一个版本提升。

ORM 和实体映射用于有策略地检索数据。不应该用于实际定义对象关系。

您应该根据在 REST 端点中如何使用它们来制定策略并设计您的实体。

您在问题中指定了您正试图保存一个 Student,但您注意到每次 Student 更新时都会连带更新 University

很可能永远不会有这样的情况,即一个 Student 应该更新一个 University

保持实体精简!

您可以以支持这种单向关系的方式来构建您的实体。我删除了一些注释以演示结构。您需要牢记,创建实体时,是根据它们的检索方式来编写的...

public class University {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    @OneToMany
    private List<Student> student;
}

public class Student {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    private Long universityId;

}

这将确保对学生的更新保持有针对性和干净。您只需为学生分配一个大学 ID,从而建立这种关系。

通常要尊重 LockExceptions。在出现 LockException 时重试只是迫使数据库服从,随着应用程序的扩展,这只会带来更多麻烦。

您始终可以选择使用精简的实体并创建自定义响应或消息对象,将结果组合在一起。

ORM 不用于创建快捷方式

在索引/外键上执行的 SELECT 的性能后果大致相当于联接它们的成本... 您只会引入一点额外的网络延迟。第二次访问数据库并不总是一个坏主意。(通常情况下,这正是 Hibernate 获取实体的方式)

您不需要编写查询,但仍需要理解检索和更新策略。

为了方便的 .getChild() 方法而牺牲数据库性能并引入复杂性是不值得的。您会发现,通过删除注释而不是添加注解来解决更多性能/锁定问题。

英文:

If you enable your query logs from Hibernate, it would be worthwhile to see the queries that your ORM is performing. You'll likely realize that your ORM is doing too much.

In your application properties or config file enable hibernate.show_sql=true

I wouldn't be surprised if your single update to a Student becomes an update to a University which becomes an update to all of its containing Students. Everything gets a version bump.

ORM and entity mappings are for strategically retrieving data. They should not be used to actually define object relationships.

You'll want to visit strategies and design your entities based on how they are used in their REST endpoints.

You specified in your question that you are trying to save a Student but you're noticing that the University also gets updated along with every Student update.

Likely there would never be a time when a Student should ever update a University

Keep your entities lean!

You can structure your entity in such a way that supports this unidirectional relationship. I removed some of the annotation just to demonstrate the structure. You will want to keep in mind that when creating entities, you are writing them for how they are retrieved...

public class University {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    @OneToMany
    private List&lt;Student&gt; student;
}

public class Student {

    @Id
    private Long id;
    private String name;
    private Long auditVersion;
    private Long universityId;

}

This will ensure that updates to the student remains targeted and clean. You are simply assigning a university id to the student therefore establishing that relationship.

You typically want to respect LockExceptions. Retrying upon a LockException is simply bullying your database into submission and will cause more headaches as your application scales.

You always have the option to work with lean entities and create custom response or message objects that would zip the results together.

ORMs are not to be used to create shortcuts

The performance consequence of a SELECT on an indexed/foreign key is roughly the same cost of grabbing them joined... you only introduce a little extra network latency. A second trip to the database is not always a bad idea. (Often times, this is exactly how Hibernate fetches your entities)

You won't have to write queries, but you will still need to understand the retrieval and update strategies.

You're sacrificing database performance and introducing complexity for a convenient .getChild() method. You'll find that you resolve more performance/locking issues by removing annotations, not adding them.

huangapple
  • 本文由 发表于 2020年8月19日 19:17:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/63485814.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定