dlaczego saveAll () zawsze wstawia dane zamiast je aktualizować?

Dec 06 2020

Spring Boot 2.4.0, DB to MySql 8.

Dane są pobierane co 15 sekund ze zdalnego za pomocą REST i zapisywane w MySql DB za pomocą saveAll().

Które wywołują metodę save () dla wszystkich podanych jednostek .

Wszystkie dane mają ustawiony identyfikator.
I spodziewam się, że jeśli nie ma takiego id w DB - zostanie wstawiony .
Jeśli taki identyfikator jest już przedstawiony w DB - zostanie zaktualizowany .

Tutaj jest wycięty z konsoli:

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

Oto jak pobrać i zapisać wyglądać:

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

Ta metoda jest uruchamiana co 15 sekund .

Jednostka:

@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

i repozytorium:

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

Tutaj jest wycięty dla encji iot w formacie 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" : "",
  ...

Więc ID jest zdecydowanie ustawione.

Dla projektu włączone jest również przetwarzanie wsadowe. Nie powinno to mieć żadnego wpływu na oszczędzanie.

Nie mogłem zrozumieć, dlaczego próbuje wstawić nową jednostkę zamiast aktualizować istniejącą?
Dlaczego nie potrafił odróżnić starych i nowych podmiotów?


AKTUALIZACJA:

Zaimplementowany trwały dla jednostki:

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

Jednak kończy się niepowodzeniem z tym samym wyjątkiem - Duplicate entry '1' for key 'iot_entity.PRIMARY'

Jeśli dodam @GeneratedValuejak poniżej:

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

To nie zawodzi. Jednak sam zaktualizuje wartość identyfikatora.

Na przykład zostało pobrane z id = 15:

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

I należy je zapisać w następujący sposób:

W rzeczywistości ma id = 2zamiast tego:

I to jest niepoprawne.


Próbowano dodać do usługi przechowywania:

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

Zawodzi z tym samym wyjątkiem (z implementacją Persistable lub bez niej). Próbuje wstawić wartość -insert into ... Duplicate entry '15' for key '... .PRIMARY'

Fragment z 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

Tutaj możesz zobaczyć zawartość pliku pom .

Jak rozwiązać ten problem?

Odpowiedzi

2 crizzis Dec 07 2020 at 00:44

Problem jest prawdopodobnie taki, że skoro @Idnie jest oznaczony @GeneratedValue, Spring Data zakłada, że ​​wszystkie odłączone (przejściowe) jednostki przekazane do save()/saveAll()powinny były EntityManager.persist()na nich wywołać.

Spróbuj wykonać IotEntitynarzędzie Persistablei wróć falsez isNew(). Dzięki temu Spring Data będzie zawsze używać EntityManager.merge()zamiast tego, co powinno przynieść pożądany efekt (tj. Wstawienie nieistniejących jednostek i zaktualizowanie istniejących).

catch23 Dec 14 2020 at 19:10

Wygląda na to, że znalazłem źródło tego zachowania.

Główny program uruchamiający aplikacje wygląda tak:

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

Wszystkie 3 kroki mają ścisłą sekwencję wykonywania. I pierwszy należy powtórzyć, aby zaktualizować dane lokalnie, jeśli jest to konieczne. Dwa inne serwery, które działają tylko z przechowywanymi danymi.

Gdzie wygląda pierwsza metoda:

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

Również to wykonanie jest zaplanowane.

Po uruchomieniu aplikacja dwukrotnie próbowała wykonać to pobieranie:

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 wątki działają w tym samym haczyku i przechowują równolegle - próbując uzyskać insertdane. (tabele są odtwarzane przy każdym starcie).

Wszystkie późniejsze pobrania są w porządku, są wykonywane tylko przez @Sceduledwątek.

Jeśli komentarz @Sceduled- będzie działać dobrze bez żadnych wyjątków.


ROZWIĄZANIE:

Dodano dodatkową właściwość logiczną do klasy usługi:

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

I kontroluj wartość po uruchomieniu aplikacji:

@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 wykorzystuje kombinację pola @version @Id, aby zdecydować, czy scalić, czy wstawić.

  • null @id i null @version oznaczałyby nowy rekord, stąd wstaw
  • jeśli @id jest obecny, pole @version służy do podjęcia decyzji, czy scalić, czy wstawić.
  • Aktualizacja jest wywoływana tylko wtedy, gdy (aktualizacja .... gdzie id = xxx i wersja = 0)

Ponieważ brakuje @id i @version, próbuje on wstawić, ponieważ podstawowy system zdecydował, że jest to nowy rekord i po uruchomieniu sql pojawi się błąd.