Spring JPA / Hibernate 的条件插入

15
我正在一个集群环境下开发项目,其中有许多节点和一个单独的数据库。该项目使用Spring-data-JPA(1.9.0)和Hibernate(5.0.1)。我遇到了如何解决防止重复行问题的困难。
为了举例说明,这里是一个简单的表格。
@Entity
@Table(name = "scheduled_updates")
public class ScheduledUpdateData {
    public enum UpdateType {
        TYPE_A,
        TYPE_B
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private UUID id;

    @Column(name = "type", nullable = false)
    @Enumerated(EnumType.STRING)
    private UpdateType type;

    @Column(name = "source", nullable = false)
    private UUID source;
}

重要的部分是有一个UNIQUE(type, source)约束。

当然,还有匹配的示例存储库:

@Repository
public class ScheduledUpdateRepository implements JpaRepository<ScheduledUpdateData, UUID> {
    ScheduledUpdateData findOneByTypeAndSource(final UpdateType type, final UUID source);

    //...
}
这个例子的想法是,系统的某些部分可以插入行,以便在两次运行之间的任意次数运行。当那个东西实际运行时,它不必担心对同一件事情进行两次操作。
我该如何编写一个服务方法来有条件地插入到这个表中? 我尝试过的几个方法都行不通:
1. 查找->操作 - 服务方法将使用存储库查看条目是否已存在,然后根据需要更新找到的条目或保存新条目。这样做行不通。 2. 尝试插入->如果失败则更新 - 服务方法会尝试插入,捕捉由于唯一约束而引起的异常,然后进行更新。这样做是不行的,因为事务已经处于回滚状态,无法进行进一步的操作。 3. 使用“INSERT INTO ... WHERE NOT EXISTS ...”的本地查询* - 存储库有一个新的本地查询:
@Repository
public class ScheduledUpdateRepository implements JpaRepository<ScheduledUpdateData, UUID> {
    // ...

    @Modifying
    @Query(nativeQuery = true, value = "INSERT INTO scheduled_updates (type, source)" +
                                       " SELECT :type, :src" +
                                       " WHERE NOT EXISTS (SELECT * FROM scheduled_updates WHERE type = :type AND source = :src)")
    void insertUniquely(@Param("type") final String type, @Param("src") final UUID source);
}

很不幸,这也行不通,因为Hibernate似乎会首先执行WHERE子句使用的SELECT - 这意味着最终将尝试多次插入,从而导致唯一约束冲突。

我确实不了解JTA、JPA或Hibernate的很多细节。在多个JVM之间插入具有唯一约束条件(除主键外)的表的任何建议吗?

编辑2016-02-02

使用Postgres(2.3)作为数据库,尝试使用隔离级别SERIALIZABLE - 不幸的是,仅靠它仍然会导致约束冲突异常。


1
重新尝试整个事务(第二次使用更新而不是插入)。 - Dragan Bozanovic
很遗憾,@Version并不能解决问题,因为当两个事务认为该行尚不存在时,就会出现错误。 - mrusinak
如果你的项目架构不能控制事务边界,那我会说它并不算一个“完整的项目”。无论如何,如果这些约束条件的违反很频繁以至于重新尝试事务很昂贵,那么你确实做错了什么。 - Dragan Bozanovic
@mrusinak,但是您可以将插入代码放在自己的事务中:您插入,如果插入失败并回滚,则会获取。 - JB Nizet
1
如果serviceA在一个事务中运行,并调用运行在自己的事务中的serviceB(即使用REQUIRES_NEW),那么您可以在serviceA中捕获由serviceB抛出的异常,而不会回滚serviceA。 - JB Nizet
显示剩余4条评论
4个回答

1
您正在尝试确保每次只有一个节点可以执行此操作。最佳(或至少最兼容于不同数据库)的方法是使用“锁定”表来实现。该表将仅有一行,并将作为信号量以确保串行访问。
请确保将此方法包装在事务中。
// this line will block if any other thread already has a lock
// until that thread's transaction commits
Lock lock = entityManager.find(Lock.class, Lock.ID, LockModeType.PESSIMISTIC_WRITE);

// just some change to the row, it doesn't matter what
lock.setDateUpdated(new Timestamp(System.currentTimeMillis()));  
entityManager.merge(lock);
entityManager.flush();

// find your entity by unique constraint
// if it exists, update it
// if it doesn't, insert it

0

1
虽然理论上这可能回答了问题,但最好在此处包含答案的关键部分,并提供参考链接。 - Tushar

0

这似乎是一个upsert的情况,可以按照这里建议的方式处理。


0

这些都无法解决“如果存在则更新,否则插入”的问题。锁定仅适用于单个记录。如果在初始查询时记录尚不存在,则没有要锁定的内容。可以锁定整个表,但这会导致严重的性能瓶颈。更好的方法是在数据库中创建一个单记录的“锁定”表,并将其用作信号量。有关详细信息,请参见我的回复。 - Alex L.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接