Warum fügt saveAll () immer Daten ein, anstatt sie zu aktualisieren?

Dec 06 2020

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 @GeneratedValuewie 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 = 2stattdessen:

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

2 crizzis Dec 07 2020 at 00:44

Das Problem ist wahrscheinlich, dass Spring Data , da das @Idnicht 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 IotEntityGerät zu erstellen Persistableund falsevon 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).

catch23 Dec 14 2020 at 19:10

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 insertDaten zu verarbeiten. (Tabellen werden bei jedem Start neu erstellt).

Alle späteren Abrufe sind in Ordnung, sie werden nur per @SceduledThread 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);
}
ZafarAli Dec 19 2020 at 00:36

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.