pourquoi saveAll () insère toujours des données au lieu de les mettre à jour?

Dec 06 2020

Spring Boot 2.4.0, DB est MySql 8.

Les données sont extraites toutes les 15 secondes à distance avec REST et stockées dans MySql DB avec saveAll().

Qui appelle la méthode save () pour toutes les entités données .

Toutes les données ont défini un ID.
Et je m'attends à ce que s'il n'y a pas un tel identifiant à DB - il sera inséré .
Si un tel ID est déjà présenté à DB - il sera mis à jour .

Voici extrait de la console:

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

Voici comment récupérer et enregistrer l'apparence suivante:

@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 !!!");
    }
}

Cette méthode s'exécute toutes les 15 secondes .

Entité:

@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

et référentiel:

public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}

Voici extrait pour l'entité iot au format 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" : "",
  ...

L'ID est donc définitivement défini.

En outre, le traitement par lots est activé pour un projet. Cela ne devrait avoir aucun impact sur l'épargne.

Je ne pouvais pas comprendre pourquoi il essaie d'insérer une nouvelle entité au lieu de mettre à jour l'existant?
Pourquoi ne pouvait-il pas distinguer la différence entre les anciennes et les nouvelles entités?


MISE À JOUR:

Implémenté Persistable pour l'entité:

@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;
    }

Cependant, il échoue avec la même exception - Duplicate entry '1' for key 'iot_entity.PRIMARY'

Si j'ajouterai @GeneratedValuecomme suit:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

Cela n'échouerait pas. Cependant, il mettra à jour la valeur ID par lui-même.

Par exemple, il a été récupéré avec id = 15:

[ {
  "id" : 15,
  "carParkRef" : 15,
  "name" : "UF Haus 1/2",

Et devrait être enregistré comme suit:

En fait, il a à la id = 2place:

Et c'est incorrect.


J'ai essayé d'ajouter au service de stockage:

private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);

Échoue avec la même exception (avec ou sans implémentation de Persistable). Il essaie d'insérer la valeur -insert into ... Duplicate entry '15' for key '... .PRIMARY'

Extrait de 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

Ici, vous pouvez voir le contenu du fichier pom .

Comment résoudre ce problème?

Réponses

2 crizzis Dec 07 2020 at 00:44

Le problème est probable que, puisque le @Idn'est pas marqué par @GeneratedValue, Spring Data suppose que toutes les entités détachées (transitoires) passées à save()/saveAll()devraient avoir été EntityManager.persist()appelées sur elles.

Essayez de faire la IotEntitymise en œuvre Persistableet le retour falsede isNew(). Cela indiquera à Spring Data de toujours utiliser à la EntityManager.merge()place, ce qui devrait avoir l'effet souhaité (c'est-à-dire insérer des entités inexistantes et mettre à jour celles existantes).

catch23 Dec 14 2020 at 19:10

On dirait que j'ai trouvé la racine de ce comportement.

Le lanceur principal de l'application ressemble à ceci:

@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();
    }

Les 3 étapes ont une séquence d'exécution stricte. Et le premier doit répéter pour mettre à jour les données localement si nécessaire. Deux autres serveurs simples qui ne fonctionnent qu'avec des données stockées.

À quoi ressemble la première méthode:

@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");
}

De plus, cette exécution est également planifiée.

Lorsque l'application démarre, elle a essayé d'exécuter cette récupération deux fois:

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 fonctionnent à la même capture et stockent en parallèle - en essayant de insertdonnées. (les tables sont recréées à chaque démarrage).

Toutes les récupérations ultérieures sont correctes, elles ne sont exécutées que par @Sceduledthread.

Si un commentaire @Sceduled- cela fonctionnera très bien sans aucune exception.


SOLUTION:

Ajout d'une propriété booléenne supplémentaire à la classe de service:

@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);
}

Et contrôlez la valeur après le démarrage de l'application:

@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 utilise la combinaison du champ @version @Id pour décider s'il faut fusionner ou insérer.

  • null @id et null @version signifieraient un nouvel enregistrement donc insérer
  • si @id est présent, le champ @version est utilisé pour décider s'il faut fusionner ou insérer.
  • La mise à jour n'est appelée que lorsque (update .... où id = xxx et version = 0)

Beacuse vous avez @id et @version manquants, il essaie d'insérer, parce que le système sous-jacent a décidé qu'il s'agissait d'un nouvel enregistrement et lors de l'exécution de sql, vous obtenez une erreur.