Предотвратить создание одной и той же сущности двумя транзакциями, но разрешить одновременное создание

У меня есть система, которая взаимодействует со сторонней системой для создания и хранения данных об автомобиле. Пользователь выбирает ThirdPartyCar и использует его для создания Car в моей системе.

Сервисный метод спасает автомобиль. Но это должно сохраняться только в том случае, если кто-то еще не пытался сохранить машину с помощью этого 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));
    }
}

Вот сценарий, который приводит к проблеме:

User A                        User B
 existsById                      |
    |                            |
    |                         existsById
    |                            |
    |                            |
 carRepo.save                    |
 thirdPartyCarMapRepo.save       |
    |                            |
    |                            |
    |                          carRepo.save
    |                          thirdPartyCarMapRepo.save

Если для изоляции задано значение ниже Isolation.SERIALIZABLE, кажется, что оба пользователя будут иметь existsById, возвращающий false. Это приводит к созданию двух автомобилей, и только последний сохраненный автомобиль сопоставляется со сторонним автомобилем.

Если я установлю Isolation.SERIALIZABLE, пользователь не сможет одновременно создавать автомобили даже из разных сторонних автомобилей.

Как я могу запретить пользователю B создавать автомобиль, когда сторонний автомобиль такой же, как у пользователя A, но при этом разрешить обоим одновременно создавать, когда сторонние автомобили разные?

Обновить

Немного подумав и изучив этот вопрос, я пришел к выводу, что это может быть не проблема изоляции транзакций.

Вот таблица ThirdPartyCarMap:

thirdPartyCarId (PK, bigint, not null)
carId (FK, bigint, not null)  /* reference my system's car table */

В качестве примера на приведенной выше схеме сценария:

Пользователь А thirdPartyCarMapRepo.save делает:

inserts into ThirdPartyCarMap (thirdPartyCarId, carId) values (45,1)

Однако пользователь B thirdPartyCarMapRepo.save делает:

update ThirdPartyCarMap set carId =2 where thirdPartyCarId=45

Таким образом, двойная природа вызова сохранения вызывает проблемы. Я думаю, что у меня есть следующие возможные решения:

  • Подход 1. Реализуйте Persistable и переопределите поведение isNew, как описано здесь
  • Подход 2: добавьте собственный запрос в репозиторий, который выполняет вставку
  • Подход 3: добавьте суррогатный первичный ключ (например, автоматически увеличивающийся идентификатор) и удалите thirdPartyCarId в качестве основного, но сделайте его уникальным.

Я думаю, что последний вариант, вероятно, лучше, но у каждого варианта есть проблемы: - Подход 1: isNew вернет true даже при чтении данных - Подход 2: немного похоже на взлом - Подход 3: Дополнительные данные добавлены в таблицу просто ради JPA кажется.

Есть ли другой лучший подход, который не имеет этих проблем?


person James    schedule 17.04.2019    source источник
comment
Вы пытались добавить идентификатор в ThirdPartyCarMap и сохранить новый экземпляр с нулевым значением?   -  person yegodm    schedule 18.04.2019
comment
@yegodm, да, у меня был идентификатор thirdPartyCarId, но проблема в том, что PK не равен нулю при сохранении, потому что он всегда имеет значение из сторонней системы. Итак, я попытался заменить ПК на независимый от сторонней системы (т.е. автоматически увеличивающийся идентификатор). Пожалуйста, смотрите обновленный пост, чтобы узнать мои мысли об этом подходе и обновленный вопрос. Спасибо.   -  person James    schedule 18.04.2019
comment
Ой, извините, я невнимательно прочитал подход №3. Собственно это я и имел в виду. Тем не менее, я бы все равно выбрал это, не беспокоясь о дополнительных данных. С другой точки зрения, если я правильно помню, возможен еще один подход. Оба идентификатора могут составлять составной ключ с именем, скажем, Key(thirdPartyCarId:Long, carId:Long), сопоставленный с полем key: Key с аннотацией @EmbeddedId.   -  person yegodm    schedule 18.04.2019
comment
Без проблем. Спасибо за дополнительную опцию, которая позволит избежать дополнительного столбца в БД, но я полагаю, что это может добавить немного больше кода. Я склоняюсь к подходу 3.   -  person James    schedule 19.04.2019


Ответы (2)


Одним из вариантов может быть использование уникального ограничения на уровне БД, которое может гарантировать, что у вас не может быть двух физических автомобилей с одним и тем же VIN (если это ваш уникальный идентификатор в вашей БД для физического автомобиля). Это означает, что в вашем случае, когда два потока попытаются добавить PhysicalCar, один из них завершится с ошибкой уникального ограничения. Spring уже оборачивает исключение БД в DataIntegrityViolationException, но вы можете извлечь из него имя ограничения, и если вы ожидаете, что оно будет выбрано в случае дубликатов для вашего PhysicalCar, вы можете действовать на него.

person Ioan M    schedule 18.04.2019
comment
Спасибо за ответ. Вы предлагаете мне добавить еще один уникальный столбец в таблицу сопоставления? Таким образом, таблица сопоставления будет иметь следующие столбцы: thirdPartyCarId - PK, carId - FK, VIN (или что-то уникальное) - unique constraint. Я думаю, проблема в том, что сохранение пользователя B просто обновит запись сопоставления, которую пользователь A вставил во время операции сохранения. - person James; 18.04.2019
comment
Поскольку Spring проверяет свойство @Id, чтобы выяснить, должно ли оно выполнять обновление или вставку, у меня будет в этой таблице инкрементный @Id, а также уникальное ограничение для ThirdPartyCarId. Таким образом, второе создание не будет передавать какой-либо идентификатор, spring будет рассматривать это как вставку (новую запись), и ваш DataConstraintViolation будет выбран, а затем вы должны действовать по мере необходимости (возвратить ошибку пользователю и т. д.) - person Ioan M; 18.04.2019
comment
А теперь я понимаю. Это подход 3 в моем обновлении OP. Это то, как я склоняюсь, если нет лучшего варианта. - person James; 19.04.2019

Вы можете создать контрольную сумму объектов ThirdPartyCar и Car пользователя A и пользователя B, используя hashCode() или MD5, и проверить на равенство. Если вы обнаружите, что машина пользователя B такая же, как и у пользователя A ThirdPartyCar, бросьте RuntimeException, иначе продолжайте.

person Alin    schedule 18.04.2019