почему saveAll () всегда вставляет данные, а не обновляет их?
Spring Boot 2.4.0
, БД - это MySql 8
.
Данные загружаются каждые 15 секунд с пульта дистанционного управления с помощью REST и сохраняются в базе данных MySql с помощью saveAll()
.
Которые вызывают метод save () для всех данных сущностей .
Все данные имеют идентификатор.
И я ожидаю, что если такого id в БД нет - он будет вставлен .
Если такой идентификатор уже присутствует в БД - он будет обновлен .
Вот вырезано из консоли:
Hibernate:
insert
into
iot_entity
(controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
...
2020-12-05 23:18:28.269 ERROR 15752 --- [ restartedMain] o.h.e.jdbc.batch.internal.BatchingBatch : HHH000315: Exception executing batch [java.sql.BatchUpdateException: Duplicate entry '1' for key 'iot_entity.PRIMARY'], SQL: insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2020-12-05 23:18:28.269 WARN 15752 --- [ restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1062, SQLState: 23000
2020-12-05 23:18:28.269 ERROR 15752 --- [ restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper : Duplicate entry '1' for key 'iot_entity.PRIMARY'
2020-12-05 23:18:28.269 DEBUG 15752 --- [ restartedMain] o.s.orm.jpa.JpaTransactionManager : Initiating transaction rollback after commit exception
org.springframework.dao.DataIntegrityViolationException: could not execute batch; SQL [insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)]; constraint [iot_entity.PRIMARY]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute batch
Вот как получить и сохранить:
@Override
@SneakyThrows
@Scheduled(fixedDelay = 15_000)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void fetchAndStoreData() {
IotEntity[] entities = restTemplate.getForObject(properties.getIotEntitiesUrl(), IotEntity[].class);
log.debug("ENTITIES:\n{}", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entities));
if (entities != null && entities.length > 0) {
entityRepository.saveAll(List.of(entities));
} else {
log.warn("NO entities data FETCHED !!!");
}
}
Этот метод запускается каждые 15 секунд .
Сущность:
@Data
@Entity
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
// other fields
и репозиторий:
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
Вот фрагмент для объекта iot в формате JSON:
2020-12-05 23:18:44.261 DEBUG 15752 --- [pool-3-thread-1] EntityService : ENTITIES:
[ {
"id" : 1,
"controllerRef" : null,
"name" : "Local Controller Unterföhring",
"description" : "",
"deviceId" : "",
...
Так что ID определенно установлен.
Также для проекта включена пакетная обработка. Это не должно влиять на сохранение.
Я не мог понять, почему он пытается вставить новую сущность вместо обновления существующей?
Почему он не мог различить старые и новые сущности?
ОБНОВИТЬ:
Реализован Persistable для Entity:
@Data
@Entity
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable, Persistable<Integer> {
private static final long serialVersionUID = 1L;
@Id
private Integer id;
@Override
public boolean isNew() {
return false;
}
@Override
public Integer getId() {
return this.id;
}
Однако это не удается с тем же исключением - Duplicate entry '1' for key 'iot_entity.PRIMARY'
Если я добавлю @GeneratedValue
следующее:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
Это не подведет. Однако он сам обновит значение идентификатора.
Например, он получил id = 15
:
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
И должен быть сохранен следующим образом:

На самом деле id = 2
вместо этого:

И это неверно.
Пытался добавить в службу хранения:
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
Сбой с тем же исключением (с реализацией Persistable или без нее). Он пытается вставить значение -insert into ... Duplicate entry '15' for key '... .PRIMARY'
Фрагмент из application.yml
:
spring:
# ===============================
# = DATA SOURCE
# ===============================
datasource:
url: jdbc:mysql://localhost:3306/demo_db
username: root
password: root
initialization-mode: always
# ===============================
# = JPA / HIBERNATE
# ===============================
jpa:
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
generate_statistics: true
Здесь вы можете увидеть содержимое файла pom .
Как исправить эту проблему?
Ответы
Вероятно, проблема в том, что, поскольку @Id
объект не отмечен значком @GeneratedValue
, Spring Data предполагает, что все переданные отсоединенные (временные) объекты save()/saveAll()
должны были EntityManager.persist()
вызывать их.
Попробуйте сделать IotEntity
орудие Persistable
и вернуться false
из него isNew()
. Это укажет Spring Data всегда использовать EntityManager.merge()
вместо этого, что должно иметь желаемый эффект (например, вставка несуществующих сущностей и обновление существующих).
Похоже, я нашел причину такого поведения.
Основная панель запуска приложений выглядит так:
@AllArgsConstructor
@SpringBootApplication
public class Application implements CommandLineRunner {
private final DataService dataService;
private final QrReaderServer qrReaderServer;
private final MonitoringService monitoringService;
@Override
public void run(String... args) {
dataService.fetchAndStoreData();
monitoringService.launchMonitoring();
qrReaderServer.launchServer();
}
Все 3 шага имеют строгую последовательность выполнения. И первый должен повториться для обновления данных локально, если это необходимо. Два других просто сервера, которые работают только с сохраненными данными.
Где выглядит первый способ:
@Scheduled(fixedDelay = 15_000)
public void fetchAndStoreData() {
log.debug("START_DATA_FETCH");
carParkService.fetchAndStoreData();
entityService.fetchAndStoreData();
assignmentService.fetchAndStoreData();
permissionService.fetchAndStoreData();
capacityService.fetchAndStoreData();
log.debug("END_DATA_FETCH");
}
Кроме того, это выполнение тоже запланировано.
Когда приложение запускается, оно дважды пытается выполнить эту выборку:
2020-12-14 14:00:46.208 DEBUG 16656 --- [pool-3-thread-1] c.s.s.s.data.impl.DataServiceImpl : START_DATA_FETCH
2020-12-14 14:00:46.208 DEBUG 16656 --- [ restartedMain] c.s.s.s.data.impl.DataServiceImpl : START_DATA_FETCH
2 потока выполняются в одном и том же захвате и хранении параллельно - пытаясь insert
данные. (таблицы воссоздаются при каждом запуске).
Все последующие выборки в порядке, они выполняются только по @Sceduled
потоку.
Если прокомментировать @Sceduled
- все будет нормально без исключений.
РЕШЕНИЕ:
В класс обслуживания добавлено дополнительное логическое свойство:
@Getter
private static final AtomicBoolean ifDataNotFetched = new AtomicBoolean(true);
@Override
@Scheduled(fixedDelay = 15_000)
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public void fetchAndStoreData() {
ifDataNotFetched.set(true);
log.debug("START_DATA_FETCH");
// fetch and store data with `saveAll()`
log.debug("END_DATA_FETCH");
ifDataNotFetched.set(false);
}
И контролируем значение после запуска приложения:
@Value("${sharepark.remote-data-fetch-timeout}")
private int dataFetchTimeout;
private static int fetchCounter;
@Override
public void run(String... args) {
waitRemoteDataStoring();
monitoringService.launchMonitoring();
qrReaderServer.launchServer();
}
private void waitRemoteDataStoring() {
do {
try {
if (fetchCounter == dataFetchTimeout) {
log.warn("Data fetch timeout reached: {}", dataFetchTimeout);
}
Thread.sleep(1_000);
++fetchCounter;
log.debug("{} Wait for data fetch one more second...", fetchCounter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (DataServiceImpl.getIfDataNotFetched().get() && fetchCounter <= dataFetchTimeout);
}
Spring Data JPA использует комбинацию поля @version @Id, чтобы решить, следует ли объединить или вставить.
- null @id и null @version означают новую запись, поэтому вставьте
- если присутствует @id, поле @version используется, чтобы решить, объединить или вставить.
- Обновление вызывается только тогда, когда (update .... где id = xxx и версия = 0)
Поскольку у вас отсутствуют @id и @version, он пытается вставить, потому что базовая система решила, что это новая запись, и при запуске sql вы получите ошибку.