警告!!! TL; DR
MySQL 5.6.39
mysql:mysql-connector-java:5.1.27
org.hibernate.common:hibernate-commons-annotations:4.0.5.Final
org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final
org.hibernate:hibernate-core:4.3.6.Final
org.hibernate:hibernate-entitymanager:4.3.6.Final
org.hibernate:hibernate-validator:5.0.3.Final
HTTP方法:POST,API路径:/ reader
实体“读者”引擎:innoDB
id
name
total_pages_read
类映射:
@Entity
@Table(name = "reader")
public class Reader{
@Column(name = "id")
private Long id;
@Column(name = "name")
private String name;
@Column(name = "total_pages_read")
private Long total_pages_read;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "reader", orphanRemoval = true)
private Set<Book_read> book_reads;
...
}
我在Reader写服务类中使用方法createEntity()和recalculateTotalPageRead():
@Service
public class ReaderWritePlatformServiceJpaRepositoryImpl{
private final ReaderRepositoryWrapper readerRepositoryWrapper;
...
@Transactional
public Long createEntity(final Long id, final String name, final Long total_pages_read){
try {
final Reader reader = new Reader(id, name, total_pages_read);
this.readerRepositoryWrapper.saveAndFlush(reader);
return 1l;
} catch (final Exception e) {
return 0l;
}
}
...
}
HTTP方法:POST,API路径:/ bookread
实体“book_read”引擎:innoDB
id
reader_id
book_title
number_of_pages
类映射:
@Entity
@Table(name = "book_read")
public class Book_read{
@Column(name = "id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "reader_id")
private Reader reader;
@Column(name = "book_title")
private String book_title;
@Column(name = "number_of_pages")
private Long number_of_pages;
...
}
我在Book_read写服务类中使用方法createEntity()
和recalculateTotalPageRead()
:
@Service
public class Book_readWritePlatformServiceJpaRepositoryImpl{
private final ReaderRepositoryWrapper readerRepositoryWrapper;
private final Book_readRepositoryWrapper bookReadRepositoryWrapper;
...
@Transactional
public Long createEntity(final Long id, final Long reader_id, final String book_title, final Long number_of_pages){
try {
final Reader reader = this.readerRepositoryWrapper.findOneWithNotFoundDetection(reader_id);
final Book_read book_read = new Book_read(id, reader, book_title, number_of_pages);
this.bookReadRepositoryWrapper.saveAndFlush(book_read);
this.recalculateTotalPageRead(reader);
return 1l;
} catch (final Exception e) {
return 0l;
}
}
private void recalculateTotalPageRead(final Reader reader){
Long total_pages_read = Long.valueOf(0);
Set<Book_read> book_reads = reader.getBook_reads();
for (Book_read book_read : book_reads){
total_pages_read += book_read.getNumber_of_pages();
}
reader.setTotal_pages_read(total_pages_read);
this.readerRepositoryWrapper.saveAndFlush(reader);
}
...
}
当我尝试创建两个实体时:
示例“读者”:
id | name | total_pages_read
-----------------------------------
1 | Foo Reader | 0(by default)
示例“book_read”:2个独立的POST方法调用
id | reader_id | book_title | number_of_pages
---------------------------------------------
1 | 1 | Foo Book | 2
2 | 1 | Bar Book | 3
在创建“book_read”-s之后期望对实体“reader”的更改如上例所示:
样本阅读器:
id | name | total_pages_read
-----------------------------------
1 | Foo Reader | 5
但是从我一直遇到的情况来看,同时创建这两个“book_read”记录的情况恰好是3个案例:
案例1(好):
案例2(确定):
案例3(不行):
我如何解决案例3?
干杯,快乐的节目:D
您所经历的被称为丢失更新,它实际上不是JPA级别的问题,您可以在MySQL shell中轻松地重现它。我假设您没有对数据库本身进行任何更改,因此您的默认事务隔离级别为REPEATABLE READ
。
在MySQL中,REPEATABLE READ
没有检测到可能丢失的更新(即使这是对这种隔离级别的共同理解)。您可以查看this answer on SO和评论主题以了解更多信息。
基本上使用MVCC,MySQL试图避免争用和死锁。在你的情况下,你将不得不做出权衡,并选择牺牲一些速度,以保持一致性。
您可以选择使用SELECT ... FOR UPDATE
语句或设置更严格的隔离级别,即SERIALIZABLE
(您可以为单个事务执行此操作)。这两个选项都将阻止读取,直到并发事务提交/回滚。因此,您将看到数据的一致视图,稍后(或许多之后,取决于应用程序的要求)。
并发很难。 :)
更新:在考虑下面的评论之后,实际上还有另一种选择:为您的数据模型实施乐观锁定。 JPA对此有所支持,请查看here和here。您实现的基本相同,但采用了一种不同的方法(您必须重新启动未能使版本不匹配的事务)并减少由于锁定较少而导致的争用。
我认为你面临的问题是FK关系的属性锁定是由关系的拥有方控制的。
由于book_reads
集合使用@OneToMany(mappedBy = "reader")
进行注释,因此它不会控制锁定,因此另一方面会这样做,这意味着在更新集合时会获得两个单独的锁定,而这些锁定并不能真正识别彼此。
删除Book_read.reader
和mappedBy
注释应该解决这个问题。
所有这些实际上都适用于带有版本属性的乐观锁定,这是推荐的做事方式。
另见Vlad Mihalcea关于这个主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/