为什么 saveAll() 总是插入数据而不是更新数据?
why saveAll() always inserts data instead of update it?
Spring 启动2.4.0
, DB 是MySql 8
.
每 15 秒使用 REST 从远程获取数据并将其存储到 MySql 数据库 saveAll()
。
.
所有数据都有设置ID。
我希望如果数据库中没有这样的 ID - 它将 inserted.
如果此类 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:
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 file content.
如何解决这个问题?
问题很可能是,由于 @Id
未标记为 @GeneratedValue
,Spring 数据假定传递给 save()/saveAll()
的所有分离(瞬态)实体应该具有EntityManager.persist()
对它们调用。
尝试让 IotEntity
实现 Persistable
并从 isNew()
返回 false
。这将告诉 Spring 数据始终使用 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();
}
这3个步骤都有严格的执行顺序。如果需要,第一个必须重复以在本地更新数据。另外两个仅处理存储数据的服务器。
第一个方法看起来像:
@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 个线程 运行 并行捕获和存储 - 尝试 insert
数据。 (每次开始都会重新创建表格)。
所有以后的提取都很好,它们仅由 @Sceduled
个线程执行。
如果评论 @Sceduled
- 它将正常工作,没有任何异常。
解决方案:
向服务 class 添加了额外的布尔值 属性:
@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);
}
Spring Data JPA使用@version @Id字段的组合来决定是否合并或插入。
- null @id 和 null @version 表示新记录因此插入
- 如果@id存在,@version字段用于决定是否合并或插入。
- 仅在(更新....其中 id = xxx 和版本 = 0)时调用更新
因为你缺少@id 和@version,它试图插入,因为底层系统认为这是新记录,当运行 sql 你得到错误。
你能试试 @GeneratedValue(strategy = GenerationType.AUTO)
这对我有用。
Spring 启动2.4.0
, DB 是MySql 8
.
每 15 秒使用 REST 从远程获取数据并将其存储到 MySql 数据库 saveAll()
。
所有数据都有设置ID。
我希望如果数据库中没有这样的 ID - 它将 inserted.
如果此类 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:
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 file content.
如何解决这个问题?
问题很可能是,由于 @Id
未标记为 @GeneratedValue
,Spring 数据假定传递给 save()/saveAll()
的所有分离(瞬态)实体应该具有EntityManager.persist()
对它们调用。
尝试让 IotEntity
实现 Persistable
并从 isNew()
返回 false
。这将告诉 Spring 数据始终使用 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();
}
这3个步骤都有严格的执行顺序。如果需要,第一个必须重复以在本地更新数据。另外两个仅处理存储数据的服务器。
第一个方法看起来像:
@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 个线程 运行 并行捕获和存储 - 尝试 insert
数据。 (每次开始都会重新创建表格)。
所有以后的提取都很好,它们仅由 @Sceduled
个线程执行。
如果评论 @Sceduled
- 它将正常工作,没有任何异常。
解决方案:
向服务 class 添加了额外的布尔值 属性:
@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);
}
Spring Data JPA使用@version @Id字段的组合来决定是否合并或插入。
- null @id 和 null @version 表示新记录因此插入
- 如果@id存在,@version字段用于决定是否合并或插入。
- 仅在(更新....其中 id = xxx 和版本 = 0)时调用更新
因为你缺少@id 和@version,它试图插入,因为底层系统认为这是新记录,当运行 sql 你得到错误。
你能试试 @GeneratedValue(strategy = GenerationType.AUTO)
这对我有用。