Warum fügt saveAll () immer Daten ein, anstatt sie zu aktualisieren?
Spring Boot 2.4.0
, DB ist MySql 8
.
Daten werden alle 15 Sekunden von Remote mit REST abgerufen und mit in MySql DB gespeichert saveAll()
.
Welche rufen die save () -Methode für alle angegebenen Entitäten auf .
Alle Daten haben ID gesetzt.
Und ich erwarte, dass wenn es bei DB keine solche ID gibt, sie eingefügt wird .
Wenn eine solche ID bereits bei DB vorhanden ist, wird sie aktualisiert .
Hier wird von der Konsole abgeschnitten:
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
So rufen Sie ab und speichern:
@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 !!!");
}
}
Diese Methode wird alle 15 Sekunden ausgeführt .
Entität:
@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
und Repository:
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
Hier ist für iot Entität im JSON-Format geschnitten:
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" : "",
...
Die ID ist also definitiv festgelegt.
Außerdem ist die Stapelverarbeitung für ein Projekt aktiviert. Es sollte keinen Einfluss auf das Speichern haben.
Ich konnte nicht verstehen, warum versucht wird, eine neue Entität einzufügen, anstatt die vorhandene zu aktualisieren.
Warum konnte es den Unterschied zwischen alten und neuen Einheiten nicht unterscheiden?
AKTUALISIEREN:
Persistent für Entität implementiert:
@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;
}
Es schlägt jedoch mit der gleichen Ausnahme fehl - Duplicate entry '1' for key 'iot_entity.PRIMARY'
Wenn ich @GeneratedValue
wie folgt hinzufügen möchte:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
Es würde nicht scheitern. Der ID-Wert wird jedoch von selbst aktualisiert.
Zum Beispiel wurde es abgerufen mit id = 15
:
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
Und sollte wie folgt gespeichert werden:

In der Tat hat es id = 2
stattdessen:

Und es ist falsch.
Versucht, zum Speicherservice hinzuzufügen:
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
Schlägt mit derselben Ausnahme fehl (mit oder ohne Implementierung von Persistable). Es wird versucht, den Wert einzufügen -insert into ... Duplicate entry '15' for key '... .PRIMARY'
Ausschnitt aus 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
Hier konnte man den Inhalt der POM-Datei sehen .
Wie kann ich dieses Problem beheben?
Antworten
Das Problem ist wahrscheinlich, dass Spring Data , da das @Id
nicht mit markiert ist @GeneratedValue
, davon ausgeht, dass alle getrennten (vorübergehenden) Entitäten, an die übergeben wurde, save()/saveAll()
diese EntityManager.persist()
aufgerufen haben sollten.
Versuchen Sie, das IotEntity
Gerät zu erstellen Persistable
und false
von zurückzukehren isNew()
. Dadurch wird Spring Data angewiesen, EntityManager.merge()
stattdessen immer zu verwenden , was den gewünschten Effekt haben sollte (dh nicht vorhandene Entitäten einfügen und vorhandene aktualisieren).
Sieht so aus, als hätte ich die Wurzel dieses Verhaltens gefunden.
Der Haupt-App-Launcher sieht folgendermaßen aus:
@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();
}
Alle 3 Schritte haben eine strikte Ausführungsreihenfolge. Und der erste muss wiederholt werden, um Daten lokal zu aktualisieren, wenn sie benötigt werden. Zwei weitere Server, die nur mit gespeicherten Daten arbeiten.
Wo die erste Methode aussieht:
@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");
}
Auch diese Ausführung ist geplant.
Beim Start der App wurde versucht, dieses Abrufen zweimal auszuführen:
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 Threads werden am selben Fang ausgeführt und parallel gespeichert - versuchen insert
Daten zu verarbeiten. (Tabellen werden bei jedem Start neu erstellt).
Alle späteren Abrufe sind in Ordnung, sie werden nur per @Sceduled
Thread ausgeführt.
Wenn Kommentar @Sceduled
- es wird ohne Ausnahmen gut funktionieren.
LÖSUNG:
Zusätzliche boolesche Eigenschaft zur Serviceklasse hinzugefügt:
@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);
}
Und steuern Sie den Wert nach dem Start der Anwendung:
@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 verwendet eine Kombination aus dem Feld @version @Id, um zu entscheiden, ob zusammengeführt oder eingefügt werden soll.
- null @id und null @version würden bedeuten, dass ein neuer Datensatz eingefügt wird
- Wenn @id vorhanden ist, wird das @ version-Feld verwendet, um zu entscheiden, ob zusammengeführt oder eingefügt werden soll.
- Update wird nur aufgerufen, wenn (Update .... wobei id = xxx und version = 0)
Weil @id und @version fehlen, wird versucht, sie einzufügen, da das zugrunde liegende System entschieden hat, dass dies ein neuer Datensatz ist, und wenn SQL ausgeführt wird, wird ein Fehler angezeigt.