I have following error in code, when I save objects in 2 different transactions:
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [] with identifier []: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : []
I have been trying to write test for the exception. However, all tests have been passed.
My test:
@Slf4j
@Rollback
@Transactional
@DataJpaTest
@ActiveProfiles({"test", "h2"})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(value = {
StatusService.class
})
class StatusServiceTest {
@Autowired
private RequestRepository repository;
@Autowired
private StatusService statusService;
private Request request;
private final UUID requestUID = UUID.randomUUID();
@BeforeEach
void setUp() {
request = new Request();
request.setRequestUID(requestUID);
repository.save(request);
}
@Test
void updateStatus() {
Request request1 = repository.findByRequestUID(requestUID);
Request request2 = repository.findByRequestUID(requestUID);
statusService.updateStatus(request1, "Done");
statusService.updateStatus(request2, "In_progress");
}
}
Service:
@Service
@Slf4j
@RequiredArgsConstructor
public class StatusService {
private final RequestRepository repository;
@Transactional
public void updateStatus(Request request, String status) {
// Some simple logic
request.setStatus(status);
repository.save(request); // The line with error
}
}
Model:
@Entity
@Getter
@Setter
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Request {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@JsonIgnore
protected Long id;
protected UUID requestUID;
private String status;
// Some additional fields
@JsonIgnore
@Version
private Integer version;
}
Where am I wrong? How to write test for triggering error?
CodePudding user response:
@DataJpaTest
includes @Transactional
so your test runs in a single transaction.
But for a single transaction the 1st level cache of JPA ensures that you'll get always the same instance for a given class and id.
This means request1
and request2
are actually the same instance and there happens only a single flush will can't create an optimistic locking exception.
There are two approaches to create an optimistic locking exception.
Actually use two or more transactions.
TransactionTemplate
is very useful for this. You could put the loading of therequest1
in a first transaction then load, modify, save, and flushrequest2
and then modify, save, and flushrequest1
. If you want to have it even closer to what is happen in production you could start two threads doing those operations and by usingCountDownLatch
make sure they proceed in the desired order.Alternatively you should be able to trigger the desired exception by loading a request, modifying its version attribute and flush the transaction. This of course is rather far from what happens in production but as said it should trigger the exception.