【发布时间】:2018-07-01 09:45:27
【问题描述】:
我们有两个具有一对多关系的表。当我们跨多个线程(更具体地说是跨多个 REST Web 请求)将多条记录插入到子表中时,由于竞争条件,我们会遇到丢失更新的问题。
我们需要做的是让 JPA 在插入子记录之前识别出实体已在其他地方更新。我尝试使用 @Version 注释方法,但这似乎并没有解决问题,因为更新/插入(我猜......)正在另一个表上发生。我尝试在每次更新时更新的父表上添加一个版本时间戳列,但这似乎也没有奏效。
我认为我真正需要做的是直接获取对 EntityManager 的引用,以便在调用save() 之前在记录上发出lock() 命令。我对 Spring 太陌生了,不知道是否
A)这确实是正确的方法,
B)如果有更好/更简单的方法来完成我们想要完成的工作,以及
C) 如何真正做到这一点。
另外,我知道@OneToMany 注释,但这似乎没有任何作用。
为了简洁起见,我已经截断了下面的代码,我还 created a trimmed down version of the code 演示了这个问题,希望可以更容易地看到我正在尝试做什么。在测试中如果将线程池号更改为 1 可以看到测试通过。
参与类:
@Entity
public class Engagement implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@ElementCollection(fetch = EAGER)
private List<String> assignedUsers;
@Version
private Long version;
private LocalDateTime updatedOn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getVersion(){return version;}
public void setVersion(Long version){this.version = version;}
public LocalDateTime getUpdatedOn(){
return updatedOn;
}
public void setUpdatedOn(LocalDateTime updatedOn) {
this.updatedOn = updatedOn;
}
public List<String> getAssignedUsers() {
return assignedUsers;
}
public void setAssignedUsers(List<String> assignedUsers) {
this.assignedUsers = assignedUsers;
}
public Engagement() {
}
}
用户类别:
public final class User {
private final String name;
private final String email;
private final String userId;
private final List<Engagement> engagements;
@ConstructorProperties({"roles", "name", "email", "userId", "engagements"})
User(String name, String email, String userId, List<Engagement> engagements) {
this.name = name;
this.email = email;
this.userId = userId;
this.engagements = engagements;
}
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
public String getName() {
return this.name;
}
public String getEmail() {
return this.email;
}
public String getUserId() {
return this.userId;
}
public List<Engagement> getEngagements() {
return this.engagements;
}
public static final class UserBuilder {
private String name;
private String email;
private String userId;
private List<Engagement> engagements;
UserBuilder() {
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder email(String email) {
this.email = email;
return this;
}
public User.UserBuilder userId(String userId) {
this.userId = userId;
return this;
}
public User.UserBuilder engagements(List<Engagement> engagements) {
this.engagements = engagements;
return this;
}
public User build() {
return new User(this.name, this.email, this.userId, this.engagements);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", email=" + this.email + ", userId=" + this.userId + ", engagements=" + this.engagements + ")";
}
}
}
线程测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class EngagementTest {
@Mock
UsersAuthService usersService;
@Autowired
EngagementsRepository engagementsRepository;
UsersAuthService authService;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
authService = new UsersAuthServiceImpl(usersService, engagementsRepository);
}
@Test
public void addingMultipleUsersAtOnceSucceeds() throws InterruptedException {
Long engagementId = 1L;
String userId1 = "user1";
String userId2 = "user2";
String userId3 = "user3";
String userId4 = "user4";
String userId5 = "user5";
String auth = "asdf";
User adminUser = User.builder()
.userId("adminUser")
.email("user@user.com")
.name("Admin User")
.build();
Engagement engagement = new Engagement();
engagement.setAssignedUsers(new ArrayList<>());
engagement.getAssignedUsers().add(adminUser.getUserId());
engagementsRepository.save(engagement);
ExecutorService executorService = Executors.newFixedThreadPool(5);//change this to 1 to see the test pass
List<Callable<Engagement>> callableList = Arrays.asList(
addUserThread(engagementId, userId1, auth, adminUser),
addUserThread(engagementId, userId2, auth, adminUser),
addUserThread(engagementId, userId3, auth, adminUser),
addUserThread(engagementId, userId4, auth, adminUser),
addUserThread(engagementId, userId5, auth, adminUser));
executorService.invokeAll(callableList);
Engagement after = engagementsRepository.findById(engagementId);
assertEquals(6, after.getAssignedUsers().size());
}
private Callable<Engagement> addUserThread(Long engagementId, String userId1, String auth, User adminUser) {
return () -> authService.addUserTo(engagementId, userId1, auth, adminUser);
}
}
【问题讨论】:
标签: spring jpa spring-boot spring-data spring-data-jpa