saveAll ()이 항상 데이터를 업데이트하는 대신 삽입하는 이유는 무엇입니까?

Dec 06 2020

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 파일 내용을 볼 수 있습니다 .

이 문제를 해결하는 방법은 무엇입니까?

답변

2 crizzis Dec 07 2020 at 00:44

문제는가로 @Id표시되지 않았기 때문에 @GeneratedValueSpringData는 전달 된 모든 분리 된 (일시적인) 엔티티 save()/saveAll()EntityManager.persist()호출 되어야 한다고 가정 합니다.

만드는 시도 IotEntity구현 Persistable및 반환 false에서 isNew(). 이것은 SpringData에게 항상 EntityManager.merge()대신 사용하도록 지시 합니다. 이것은 원하는 효과를 가져야합니다 (즉, 존재하지 않는 엔티티 삽입 및 기존 엔티티 업데이트).

catch23 Dec 14 2020 at 19:10

이 행동의 근원을 찾은 것 같습니다.

기본 앱 런처는 다음과 같습니다.

@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);
}
ZafarAli Dec 19 2020 at 00:36

SpringData JPA는 @version @Id 필드의 조합을 사용하여 병합 또는 삽입 여부를 결정합니다.

  • null @id 및 null @version은 새 레코드를 의미하므로 삽입
  • @id가있는 경우 @version 필드는 병합 또는 삽입 여부를 결정하는 데 사용됩니다.
  • 업데이트는 (update .... 여기서 id = xxx 및 version = 0) 인 경우에만 호출됩니다.

@id 및 @version이 누락되어 삽입하려고했기 때문에 기본 시스템이 이것이 새 레코드라고 결정하고 sql을 실행할 때 오류가 발생했습니다.