mengapa saveAll () selalu memasukkan data daripada memperbaruinya?
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 @GeneratedValue
seperti 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 = 2
malah:

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
Masalahnya mungkin karena, karena @Id
tidak ditandai dengan @GeneratedValue
, Spring Data mengasumsikan semua entitas yang terlepas (sementara) yang diteruskan save()/saveAll()
seharusnya telah EntityManager.persist()
dipanggil.
Cobalah membuat IotEntity
implement Persistable
dan kembali false
dari 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).
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 insert
data. (tabel dibuat ulang di setiap awal).
Semua pengambilan nanti baik-baik saja, mereka hanya dijalankan oleh @Sceduled
utas.
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);
}
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.