por que saveAll () sempre insere dados em vez de atualizá-los?
Spring Boot 2.4.0
, DB é MySql 8
.
Os dados são buscados a cada 15 segundos remotamente com REST e armazenados no banco de dados MySql com saveAll()
.
Que chamam o método save () para todas as entidades fornecidas .
Todos os dados possuem um ID definido.
E estou esperando que, se não houver tal id no DB - ele será inserido .
Se tal ID já for apresentado no DB - ele será atualizado .
Aqui está um recorte do 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
Aqui está como buscar e salvar:
@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 !!!");
}
}
Este método é executado a cada 15 segundos .
Entidade:
@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
e Repositório:
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
Aqui está recortado para entidade iot no formato 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" : "",
...
Portanto, o ID está definitivamente definido.
Além disso, o envio em lote está habilitado para um projeto. Não deve ter nenhum impacto na economia.
Não consegui entender porque ele tenta inserir uma nova entidade em vez de atualizar a existente?
Por que ele não conseguia distinguir a diferença entre as entidades antigas e novas?
ATUALIZAR:
Persistível implementado para a entidade:
@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;
}
No entanto, ele falha com a mesma exceção - Duplicate entry '1' for key 'iot_entity.PRIMARY'
Se eu adicionar @GeneratedValue
o seguinte:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
Não iria falhar. No entanto, ele atualizará o valor do ID sozinho.
Por exemplo, foi obtido com id = 15
:
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
E deve ser salvo da seguinte forma:

Na verdade, em id = 2
vez disso:

E está incorreto.
Tentei adicionar ao serviço de armazenamento:
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
Falha com a mesma exceção (com ou sem implementação de Persistable). Tenta inserir o valor -insert into ... Duplicate entry '15' for key '... .PRIMARY'
Snippet 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
Aqui você pode ver o conteúdo do arquivo pom .
Como consertar este problema?
Respostas
O problema é que, uma vez que o @Id
não está marcado com @GeneratedValue
, o Spring Data assume que todas as entidades desanexadas (transitórias) passadas save()/saveAll()
devem ter sido EntityManager.persist()
invocadas.
Tente fazer IotEntity
implementar Persistable
e retornando false
a partir isNew()
. Isso dirá ao Spring Data para sempre usar em EntityManager.merge()
vez disso, o que deve ter o efeito desejado (ou seja, inserir entidades inexistentes e atualizar as existentes).
Parece que encontrei a raiz desse comportamento.
O iniciador principal do aplicativo se parece com:
@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();
}
Todas as 3 etapas têm sequência de execução estrita. E o primeiro deve ser repetido para atualizar os dados localmente, se necessário. Dois outros servidores que funcionam apenas com dados armazenados.
Onde o primeiro método se parece com:
@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");
}
Além disso, essa execução também está programada.
Quando o aplicativo é iniciado, ele tenta executar esta busca duas vezes:
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 são executados na mesma captura e armazenamento em paralelo - tentando insert
dados. (as tabelas são recriadas a cada início).
Todas as buscas posteriores são boas, elas são executadas apenas por @Sceduled
thread.
Se comentário @Sceduled
- funcionará bem sem quaisquer exceções.
SOLUÇÃO:
Adicionada propriedade booleana adicional à classe de serviço:
@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);
}
E controle o valor depois que o aplicativo for iniciado:
@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);
}
O Spring Data JPA usa a combinação do campo @version @Id para decidir se deve mesclar ou inserir.
- null @id e null @version significariam novo registro, portanto, insira
- se @id estiver presente, o campo @version é usado para decidir se deve mesclar ou inserir.
- A atualização só é chamada quando (atualização .... onde id = xxx e versão = 0)
Porque você tem @id e @version faltando, está tentando inserir, porque o sistema subjacente decidiu que este é um novo registro e quando executar sql você obterá um erro.