pourquoi saveAll () insère toujours des données au lieu de les mettre à jour?
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 @GeneratedValue
comme 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 = 2
place:

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
Le problème est probable que, puisque le @Id
n'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 IotEntity
mise en œuvre Persistable
et le retour false
de 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).
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 insert
donné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 @Sceduled
thread.
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);
}
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.