I have a system that interfaces with a third party system to create and store car data. The user selects a ThirdPartyCar
and uses it to create a Car
in my system.
A service method saves the car. But it should only save if someone else has not already tried to save the car using that ThirdPatyCar
:
@Transactional(transactionManager="mySystemTransactionManager", isolation=Isolation.?)
public void saveNewCarAndMapToThirdPartyCar(Car car, Long thirdPartyCarId) {
// Mapping table tracks which ThirdPartyCar was used to create my Car.
// The thirdPartyCarId is the primary key of the table.
if (!thirdPartyCarMapRepo.existsById(thirdPartyCarId)) {
// sleep to help test concurrency issues
log.debug("sleep");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("awake");
Car car = carRepository.save(car);
thirdPartyCarMapRepo.save(new ThirdPartyCarMap(thirdPartyCarId, car));
}
}
Here's a scenario that results in the problem:
User A User B
existsById |
| |
| existsById
| |
| |
carRepo.save |
thirdPartyCarMapRepo.save |
| |
| |
| carRepo.save
| thirdPartyCarMapRepo.save
With isolation set to anything lower than Isolation.SERIALIZABLE, it appears that both users will have existsById return false. This results in two cars being created and only the last saved one being mapped back to the third party car.
If I set Isolation.SERIALIZABLE, user cannot concurrently create cars even from different third party cars.
How can I prevent user B from creating a car when the third party car is the same as user A but still allow both to concurrently create when the third party cars are different?
Update
After thinking and researching about this a bit more, I believe this may not be an transaction isolation issue.
Here's the ThirdPartyCarMap table:
thirdPartyCarId (PK, bigint, not null)
carId (FK, bigint, not null) /* reference my system's car table */
As an example using the scenario diagram above:
User A thirdPartyCarMapRepo.save
does:
inserts into ThirdPartyCarMap (thirdPartyCarId, carId) values (45,1)
However, user B thirdPartyCarMapRepo.save
does:
update ThirdPartyCarMap set carId =2 where thirdPartyCarId=45
So, it's the dual nature of the save invocation that is causing issues. I think I have the following possible solutions:
- Approach 1: Implement Persistable and override the isNew behavior as described here
- Approach 2: Add a native query to the repository that does the insert
- Approach 3: Add a surrogate primary key (such as a auto incrementing id) and and remove
thirdPartyCarId
as the primary but make it unique.
I think the last option is probably best but each option has issues: - Approach 1: isNew would return true even when reading data - Approach 2: Seems a bit like a hack - Approach 3: Extra data added to the table just for the sake of JPA it seems.
Is there another better approach that doesn't have these issues?