首页 > 解决方案 > Kotlin JPA 一对多 @ElementCollection 尝试保存重复项导致违反约束

问题描述

我定义了以下实体(简化)...

@Entity(name = "metrics")
data class MetricsEntity(
    val name: String,
    // ... other properties omitted for clarity
    @ElementCollection(fetch = FetchType.EAGER)
    @MapKeyColumn(name = "event_name")
    @Column(name = "event_count")
    @CollectionTable(name = "metric_event", joinColumns = [JoinColumn(name = "metrics_id")])
    val events: MutableMap<String, Int>,
)

这里的想法是,对于指标表中的每个条目,我们记录事件计数,最终得到一个包含这样条目的表......

 metrics_id |            event_name             | event_count 
------------+-----------------------------------+-------------
   15647624 | Launched                          |           1
   15647624 | Registration_successful           |          10
   15647624 | Registration_failed               |           2
   15647624 | History_viewed                    |           1

在代码中,我们使用类似这样的方式加载指标实体......

val metrics = metricsRepository.findByProperties(properties)

...获得一个指标。此处的选择标准已被简化,但足以说明我们从该查询中获得了一个指标实例。这里的存储库定义为...

interface MetricsRepository : CrudRepository<MetricsEntity, Long> {
   ...
}

现在我们要么更新事件映射以使用以下代码添加一个新计数,即增加一个现有计数...

metrics.events[eventName] = (it.events[eventName] ?: 0) + 1
metricsRepository.save(it)

这在绝大多数情况下都有效,但保存调用时不时会在上表中引发称为 metric_event_constraint 的约束冲突,该约束被定义为......

ALTER TABLE metric_event ADD CONSTRAINT metric_event_constraint UNIQUE (metrics_id, name);

这似乎表明当行已经存在时,保存操作正在保存新行。查看日志表明我在尝试修改相同计数的多个线程之间发生冲突......

08:58:18.466  INFO 3 --- [TaskExecutor-11] incr count for event Launched, count 1
08:58:18.487  INFO 3 --- [cTaskExecutor-4] incr count for event Launched, count 1
08:58:18.618 ERROR 3 --- [cTaskExecutor-4] incr count failed for request
08:58:24.623  INFO 3 --- [TaskExecutor-94] incr count for event Launched, count 2
08:59:14.951  INFO 3 --- [askExecutor-126] incr count for event Launched, count 3

...这里第一个事件起作用并增加计数,第二个事件失败并且没有(发现约束违规),第三个和第四个工作正常。当我们想要 4 个时,总数为 3。在我看来,第一个和第二个事件发生了冲突。

所以问题首先是你认为这个总结是否正确?其次,我如何使它工作;)?我的假设是我需要锁定度量实体,那么我将如何使用使用的 crud 存储库和实体类的框架来解决这个问题?

这背后的数据库是 Postgres。

问候,

标记

标签: postgresqlspring-bootjpakotlin

解决方案


所以问题首先是你认为这个总结是否正确?

这是很有可能的。

我如何使它工作;)?

您可以在获取时锁定实体(使用EntityManager.find()if 直接交互的第二个参数EntityManger,或@LockSpring Data 存储库方法上的注释。对于您的场景,我猜您需要乐观锁定,这意味着您需要添加实体的一个@Version字段(如果乐观锁失败,您可能还想用悲观锁重试,但如果冲突很少,纯粹的乐观锁可能是要走的路)。

当然,如果您希望两次修改尝试都成功,则需要通过重试操作来捕获和处理锁定异常(我相信 Spring repos 会将其包装成 a ObjectOptimisticLockingFailureException,但您必须检查)。


推荐阅读