dlaczego saveAll () zawsze wstawia dane zamiast je aktualizować?
Spring Boot 2.4.0
, DB to MySql 8
.
Dane są pobierane co 15 sekund ze zdalnego za pomocą REST i zapisywane w MySql DB za pomocą saveAll()
.
Które wywołują metodę save () dla wszystkich podanych jednostek .
Wszystkie dane mają ustawiony identyfikator.
I spodziewam się, że jeśli nie ma takiego id w DB - zostanie wstawiony .
Jeśli taki identyfikator jest już przedstawiony w DB - zostanie zaktualizowany .
Tutaj jest wycięty z konsoli:
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
Oto jak pobrać i zapisać wyglądać:
@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 !!!");
}
}
Ta metoda jest uruchamiana co 15 sekund .
Jednostka:
@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
i repozytorium:
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
Tutaj jest wycięty dla encji iot w formacie 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" : "",
...
Więc ID jest zdecydowanie ustawione.
Dla projektu włączone jest również przetwarzanie wsadowe. Nie powinno to mieć żadnego wpływu na oszczędzanie.
Nie mogłem zrozumieć, dlaczego próbuje wstawić nową jednostkę zamiast aktualizować istniejącą?
Dlaczego nie potrafił odróżnić starych i nowych podmiotów?
AKTUALIZACJA:
Zaimplementowany trwały dla jednostki:
@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;
}
Jednak kończy się niepowodzeniem z tym samym wyjątkiem - Duplicate entry '1' for key 'iot_entity.PRIMARY'
Jeśli dodam @GeneratedValue
jak poniżej:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
To nie zawodzi. Jednak sam zaktualizuje wartość identyfikatora.
Na przykład zostało pobrane z id = 15
:
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
I należy je zapisać w następujący sposób:

W rzeczywistości ma id = 2
zamiast tego:

I to jest niepoprawne.
Próbowano dodać do usługi przechowywania:
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
Zawodzi z tym samym wyjątkiem (z implementacją Persistable lub bez niej). Próbuje wstawić wartość -insert into ... Duplicate entry '15' for key '... .PRIMARY'
Fragment z 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
Tutaj możesz zobaczyć zawartość pliku pom .
Jak rozwiązać ten problem?
Odpowiedzi
Problem jest prawdopodobnie taki, że skoro @Id
nie jest oznaczony @GeneratedValue
, Spring Data zakłada, że wszystkie odłączone (przejściowe) jednostki przekazane do save()/saveAll()
powinny były EntityManager.persist()
na nich wywołać.
Spróbuj wykonać IotEntity
narzędzie Persistable
i wróć false
z isNew()
. Dzięki temu Spring Data będzie zawsze używać EntityManager.merge()
zamiast tego, co powinno przynieść pożądany efekt (tj. Wstawienie nieistniejących jednostek i zaktualizowanie istniejących).
Wygląda na to, że znalazłem źródło tego zachowania.
Główny program uruchamiający aplikacje wygląda tak:
@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();
}
Wszystkie 3 kroki mają ścisłą sekwencję wykonywania. I pierwszy należy powtórzyć, aby zaktualizować dane lokalnie, jeśli jest to konieczne. Dwa inne serwery, które działają tylko z przechowywanymi danymi.
Gdzie wygląda pierwsza metoda:
@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");
}
Również to wykonanie jest zaplanowane.
Po uruchomieniu aplikacja dwukrotnie próbowała wykonać to pobieranie:
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 wątki działają w tym samym haczyku i przechowują równolegle - próbując uzyskać insert
dane. (tabele są odtwarzane przy każdym starcie).
Wszystkie późniejsze pobrania są w porządku, są wykonywane tylko przez @Sceduled
wątek.
Jeśli komentarz @Sceduled
- będzie działać dobrze bez żadnych wyjątków.
ROZWIĄZANIE:
Dodano dodatkową właściwość logiczną do klasy usługi:
@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);
}
I kontroluj wartość po uruchomieniu aplikacji:
@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 wykorzystuje kombinację pola @version @Id, aby zdecydować, czy scalić, czy wstawić.
- null @id i null @version oznaczałyby nowy rekord, stąd wstaw
- jeśli @id jest obecny, pole @version służy do podjęcia decyzji, czy scalić, czy wstawić.
- Aktualizacja jest wywoływana tylko wtedy, gdy (aktualizacja .... gdzie id = xxx i wersja = 0)
Ponieważ brakuje @id i @version, próbuje on wstawić, ponieważ podstawowy system zdecydował, że jest to nowy rekord i po uruchomieniu sql pojawi się błąd.