saveAll ()이 항상 데이터를 업데이트하는 대신 삽입하는 이유는 무엇입니까?
Spring Boot 2.4.0
, DB는 MySql 8
입니다.
REST를 사용하여 원격에서 15 초마다 데이터를 가져와 .NET을 사용하여 MySql DB에 저장합니다 saveAll()
.
주어진 모든 엔티티에 대해 save () 메소드를 호출합니다 .
모든 데이터에는 세트 ID가 있습니다.
그리고 DB에 이러한 ID가 없으면 삽입 될 것으로 예상하고 있습니다 .
이러한 ID가 이미 DB에있는 경우 업데이트됩니다 .
다음은 콘솔에서 가져온 것입니다.
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
가져오고 저장하는 방법은 다음과 같습니다.
@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 !!!");
}
}
이 방법은 15 초마다 실행 됩니다 .
실재:
@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
및 저장소 :
public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}
다음은 JSON 형식의 iot 엔티티에 대한 내용입니다.
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" : "",
...
그래서 ID는 확실히 설정되었습니다.
또한 프로젝트에 대해 일괄 처리가 활성화됩니다. 저축에 영향을 미치지 않아야합니다.
기존 항목을 업데이트하는 대신 새 항목을 삽입하려는 이유를 이해할 수 없습니까?
왜 이전 엔티티와 새 엔티티의 차이를 구별 할 수 없습니까?
최신 정보:
엔티티에 대한 지속성 구현 :
@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;
}
그러나 동일한 예외로 실패합니다. Duplicate entry '1' for key 'iot_entity.PRIMARY'
@GeneratedValue
다음과 같이 추가 하면 :
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
실패하지 않을 것입니다. 그러나 자체적으로 ID 값을 업데이트합니다.
예를 들어 다음과 같이 가져 왔습니다 id = 15
.
[ {
"id" : 15,
"carParkRef" : 15,
"name" : "UF Haus 1/2",
다음과 같이 저장해야합니다.

실제로 id = 2
다음과 같습니다.

그리고 그것은 올바르지 않습니다.
저장 서비스에 추가하려고했습니다.
private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);
동일한 예외 (Persistable 구현 여부에 관계없이)로 실패합니다. 값을 삽입하려고합니다.insert into ... Duplicate entry '15' for key '... .PRIMARY'
스 니펫 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
여기에서 pom 파일 내용을 볼 수 있습니다 .
이 문제를 해결하는 방법은 무엇입니까?
답변
문제는가로 @Id
표시되지 않았기 때문에 @GeneratedValue
SpringData는 전달 된 모든 분리 된 (일시적인) 엔티티 save()/saveAll()
가 EntityManager.persist()
호출 되어야 한다고 가정 합니다.
만드는 시도 IotEntity
구현 Persistable
및 반환 false
에서 isNew()
. 이것은 SpringData에게 항상 EntityManager.merge()
대신 사용하도록 지시 합니다. 이것은 원하는 효과를 가져야합니다 (즉, 존재하지 않는 엔티티 삽입 및 기존 엔티티 업데이트).
이 행동의 근원을 찾은 것 같습니다.
기본 앱 런처는 다음과 같습니다.
@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();
}
세 단계 모두 엄격한 실행 순서가 있습니다. 그리고 첫 번째는 필요한 경우 데이터를 로컬로 업데이트하기 위해 반복해야합니다. 저장된 데이터로만 작동하는 다른 두 개의 서버.
첫 번째 방법은 다음과 같습니다.
@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");
}
또한이 실행도 예정되어 있습니다.
앱이 시작될 때이 가져 오기를 두 번 실행하려고했습니다.
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 개의 스레드가 동일한 catch에서 실행되고 병렬로 저장- insert
데이터를 시도합니다 . (테이블은 시작할 때마다 다시 생성됩니다).
이후의 모든 페치는 괜찮으며 @Sceduled
스레드에 의해서만 실행됩니다 .
의견이 @Sceduled
있으면 예외없이 잘 작동합니다.
해결책:
서비스 클래스에 추가 부울 속성을 추가했습니다.
@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);
}
그리고 응용 프로그램이 시작된 후 값을 제어하십시오.
@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);
}
SpringData JPA는 @version @Id 필드의 조합을 사용하여 병합 또는 삽입 여부를 결정합니다.
- null @id 및 null @version은 새 레코드를 의미하므로 삽입
- @id가있는 경우 @version 필드는 병합 또는 삽입 여부를 결정하는 데 사용됩니다.
- 업데이트는 (update .... 여기서 id = xxx 및 version = 0) 인 경우에만 호출됩니다.
@id 및 @version이 누락되어 삽입하려고했기 때문에 기본 시스템이 이것이 새 레코드라고 결정하고 sql을 실행할 때 오류가 발생했습니다.