mengapa saveAll () selalu memasukkan data daripada memperbaruinya?

Dec 06 2020

Spring Boot 2.4.0, DB adalah MySql 8.

Data diambil setiap 15 detik dari jarak jauh dengan REST dan menyimpannya ke MySql DB dengan saveAll().

Yang memanggil metode save () untuk semua entitas yang diberikan .

Semua data telah menetapkan ID.
Dan saya mengharapkan bahwa jika tidak ada id seperti itu di DB - itu akan dimasukkan .
Jika ID tersebut sudah disajikan di DB - itu akan diperbarui .

Ini diambil dari konsol:

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

Berikut cara mengambil dan menyimpan tampilan seperti:

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

Metode ini berjalan setiap 15 detik .

Kesatuan:

@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

dan Repositori:

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

Berikut potongan untuk entitas iot dalam 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" : "",
  ...

Jadi ID sudah pasti ditetapkan.

Selain itu, pengelompokan diaktifkan untuk sebuah proyek. Seharusnya tidak berdampak pada tabungan.

Saya tidak mengerti mengapa mencoba memasukkan entitas baru alih-alih memperbarui yang sudah ada?
Mengapa tidak bisa membedakan perbedaan antara entitas lama dan baru?


MEMPERBARUI:

Persistable yang Diimplementasikan untuk Entitas:

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

Namun, gagal dengan pengecualian yang sama - Duplicate entry '1' for key 'iot_entity.PRIMARY'

Jika saya akan menambahkan @GeneratedValueseperti berikut:

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

Itu tidak akan gagal. Namun, itu akan memperbarui nilai ID dengan sendirinya.

Misalnya, itu diambil dengan id = 15:

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

Dan harus disimpan seperti berikut:

Faktanya itu id = 2malah:

Dan itu tidak benar.


Mencoba menambahkan ke layanan penyimpanan:

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

Gagal dengan pengecualian yang sama (dengan atau tanpa menerapkan Persistable). Ia mencoba memasukkan nilai -insert into ... Duplicate entry '15' for key '... .PRIMARY'

Cuplikan dari 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

Di sini Anda bisa melihat konten file pom .

Bagaimana cara memperbaiki masalah ini?

Jawaban

2 crizzis Dec 07 2020 at 00:44

Masalahnya mungkin karena, karena @Idtidak ditandai dengan @GeneratedValue, Spring Data mengasumsikan semua entitas yang terlepas (sementara) yang diteruskan save()/saveAll()seharusnya telah EntityManager.persist()dipanggil.

Cobalah membuat IotEntityimplement Persistabledan kembali falsedari isNew(). Ini akan memberi tahu Spring Data untuk selalu menggunakan EntityManager.merge(), yang seharusnya memiliki efek yang diinginkan (yaitu memasukkan entitas yang tidak ada dan memperbarui yang sudah ada).

catch23 Dec 14 2020 at 19:10

Sepertinya saya menemukan akar dari perilaku ini.

Peluncur Aplikasi Utama terlihat seperti:

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

Ketiga langkah tersebut memiliki urutan eksekusi yang ketat. Dan yang pertama harus mengulang untuk memperbarui data secara lokal jika diperlukan. Dua server lain hanya yang bekerja dengan data yang disimpan saja.

Dimana metode pertama terlihat seperti:

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

Juga, eksekusi ini juga dijadwalkan.

Saat aplikasi dimulai, aplikasi mencoba menjalankan pengambilan ini dua kali:

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 utas berjalan pada tangkapan yang sama dan disimpan secara paralel - mencoba insertdata. (tabel dibuat ulang di setiap awal).

Semua pengambilan nanti baik-baik saja, mereka hanya dijalankan oleh @Sceduledutas.

Jika komentar @Sceduled- itu akan bekerja dengan baik tanpa Pengecualian.


LARUTAN:

Menambahkan properti boolean tambahan ke kelas layanan:

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

Dan kontrol nilai setelah aplikasi dimulai:

@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 menggunakan kombinasi bidang @version @Id untuk memutuskan apakah akan menggabungkan atau menyisipkan.

  • null @id dan null @version akan berarti record baru maka masukkan
  • jika @id ada, bidang @version digunakan untuk memutuskan apakah akan menggabungkan atau menyisipkan.
  • Pembaruan hanya dipanggil ketika (update .... dimana id = xxx dan versi = 0)

Karena Anda memiliki @id dan @version hilang, itu mencoba untuk memasukkan, karena sistem yang mendasari memutuskan ini adalah catatan baru dan ketika dijalankan sql Anda mendapatkan kesalahan.