Я использую Spring Boot Scheduler для ежедневного выполнения запроса к БД, чтобы найти некоторые записи на основе условия и обновить возвращенные записи. Извлечение записей с помощью JPA работает нормально, но когда я перебираю их, обновляю и пытаюсь сохранить каждую обновленную запись, я получаю следующую ошибку: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction
Caused by: javax.persistence.RollbackException: Error while committing the transaction at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:81) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562) ... 30 more Caused by: java.lang.NullPointerException at com.xxx.yyy.config.JpaAuditingConfiguration.auditorProvider$lambda-0(JpaAuditingConfiguration.kt:15) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208) at com.sun.proxy.$Proxy168.getCurrentAuditor(Unknown Source) at java.base/java.util.Optional.map(Optional.java:265) at org.springframework.data.auditing.AuditingHandler.getAuditor(AuditingHandler.java:109) at org.springframework.data.auditing.AuditingHandler.markModified(AuditingHandler.java:104) at org.springframework.data.jpa.domain.support.AuditingEntityListener.touchForUpdate(AuditingEntityListener.java:112).
Вот код планировщика, который у меня есть. Если я запускаю тот же код внутри своей службы и вызываю его с помощью конечной точки, все работает нормально:
@Component
class Scheduler(
private val repository: Repository
) {
@Scheduled(cron = "0 0 2 * * *")
fun expire() {
val records = repository.findRecords()
for (record in records) {
try {
// Call some external API using record.id but this part is commented out for now until the saving works
record.active = false
repository.save(record)
} catch (ex: Exception) {
logger.error("Error expiring record " + record.id)
logger.error("Exception: ${ex.printStackTrace()}")
continue
}
}
}
}
Исключение нулевого указателя происходит в конфигурации JpaAuditingConfiguration, которую я использую для хранения дат created_at и last_modified_at. Вот код, который у меня есть для этого класса:
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class JpaAuditingConfiguration {
@Bean
fun auditorProvider(): AuditorAware<String> {
return AuditorAware { Optional.of(SecurityContextHolder.getContext().authentication.name) }
}
}
Зачем делать обновление в коде, просто напишите оператор обновления вместо выбора + извлечения + изменения + сохранения. 1 запрос тоже может это сделать. Также ваш код неверен, поскольку поиск и сохранение являются отдельными транзакциями. Этот код медленный, так как каждое сохранение представляет собой отдельную транзакцию (или 1 большую транзакцию, и тогда это неправильно, так как вы выполняете перехват + продолжение).
@M.Deinum M.Deinum Я просматриваю записи, потому что мне нужно вызвать какой-то внешний API, используя идентификатор записи, прежде чем обновлять запись и сохранять. Эта часть кода пока закомментирована, потому что проблема не в ней. Я обновил свой код выше и добавил комментарий, где должен работать внешний API.
@voidvoid это происходит в JpaAuditingConfiguration классе. Я добавил строки, которые показывают это в ошибке, и добавил код, который я использую в этом классе.
@Simo, когда ваш планировщик выполняется, у вас нет принципала в контексте безопасности, однако, когда это делает запрос, у вас есть принципал из сеанса. Вот почему вы получаете ошибку. Пожалуйста, подтвердите это, чтобы я мог опубликовать ответ :)
@voidvoid, возможно, вы правы, но я не понимаю, почему субъект безопасности не применяется к планировщику
@Simo, откуда он узнает, кого аутентифицировать? Вам нужно вручную аутентифицировать кого-то, обычно для этого вы создаете отдельного пользователя app.
Ваш JpaAuditingConfiguration требует, чтобы контекст безопасности был ненулевым, когда вы вносите изменения. Когда вы запускаете свою задачу в планировщике, нет активного запроса, поэтому нет активного сеанса, и поэтому ваша аутентификация недействительна. Обычно это решается путем создания специального пользователя app и его аутентификации вручную в запланированном задании.
Спасибо. Я смог заставить его работать благодаря вашему ответу и ответу, представленному в этом комментарии.
Можете ли вы включить соответствующую строку для Caused by: java.lang.NullPointerException?