【问题标题】:DELETE AND INSERT in same transaction throws Duplicate entry exception in JDBC同一事务中的 DELETE 和 INSERT 在 JDBC 中引发 Duplicate entry 异常
【发布时间】:2021-03-18 04:33:44
【问题描述】:

我有一个表 inbox_participants,其中 inbox_id 作为外键。当收件箱更新时,可以添加或删除参与者。我正在尝试删除给定 inbox_id 的所有收件箱参与者,并重新插入具有相同参与者 ID 的更新参与者。它在我的本地机器上正常运行。但在服务器中,它为 inbox_participant_id 提供了一个 Duplicate Entry 异常,该异常应该已被删除。

更新收件箱

logger.info("Update inbox. Inbox id : {}", id);
final String query = "UPDATE inbox SET subject=?, updated_at=?, type=? WHERE inbox_id=?";
Connection conn = null;
PreparedStatement pst = null;
try {
    conn = dataSource.getConnection();
    conn.setAutoCommit(false);
    pst = dataSource.prepareStatement(query, conn);
    logger.debug(debug, QUERY, query);
    pst.setString(1, inbox.getSubject());
    pst.setLong(2, TreeleafDate.timestamp());
    pst.setInt(3, inbox.getTypeValue());
    pst.setString(4, id);
    if (pst.executeUpdate() != 1) {
        rollback(conn);
        return false;
    }
    if (!removeParticipants(conn, id, debug)) {
        rollback(conn);
        return false;
    }
    if (!updateParticipants(conn, id, accountIdParticipantMap, debug)) {
        rollback(conn);
        return false;
    }
    commit(conn);
    return true;
} catch (SQLException | JDBCException e) {
    rollback(conn);
    logger.error(debug, "Error while updating inbox", e);
    return false;
} finally {
    close(pst);
    close(conn);
}

删除参与者

private boolean removeParticipants(final Connection conn,
                                   final String id,
                                   final TreeleafProto.Debug debug) {
    logger.info(debug, "Deleting inbox participants. Inbox id : {}", id);
    final String query = "DELETE FROM inbox_participant WHERE inbox_id=?";

    try (PreparedStatement pst = dataSource.prepareStatement(query, conn)) {
        logger.debug(debug, QUERY,
                query);
        pst.setString(1, id);
        final var i = pst.executeUpdate();
        logger.debug(debug, "Delete query rows updated : {}", i);
        return i >= 0;
    } catch (JDBCException | SQLException e) {
        logger.error(debug, "Error while removing participants.", e);
        return false;
    }
}

插入更新的参与者

private boolean updateParticipants(final Connection conn,
                                   final String id,
                                   final Map<String, InboxProto.InboxParticipant> participants,
                                   final TreeleafProto.Debug debug) {
    logger.info(debug, "Updating inbox participants");
    final String query = "INSERT INTO inbox_participant (inbox_participant_id, inbox_id, account_id, `role`, created_at, updated_at, notification_type, `left`) VALUES(?, ?, ?, ?, ?, ?, ?, ?)";

    try (PreparedStatement pst = dataSource.prepareStatement(query, conn)) {
        logger.debug(debug, QUERY,
                query);
        for (Map.Entry<String, InboxProto.InboxParticipant> entry : participants.entrySet()) {
            final var participant = entry.getValue();
            pst.setString(1, getId(participant));
            pst.setString(2, id);
            pst.setString(3, entry.getKey());
            pst.setInt(4, participant.getRoleValue());
            pst.setLong(5, TreeleafDate.timestamp());
            pst.setLong(6, TreeleafDate.timestamp());
            pst.setInt(7, participant.getNotificationTypeValue());
            pst.setInt(8, participant.getParticipantStatusValue());
            pst.addBatch();
        }
        int[] ints = pst.executeBatch();
        return ints.length == participants.size() &&
                Arrays.stream(ints).allMatch(value -> value == 1);
    } catch (JDBCException | SQLException e) {
        logger.error(debug, "Error while updating participants.", e);
        return false;
    }
}

【问题讨论】:

  • 请添加 Queries.INSERT_INBOX_PARTICIPANT 以供审核。

标签: java mysql jdbc


【解决方案1】:

如果我理解这里的一般逻辑,看起来上面的代码正在尝试添加 (INSERT) OR UPDATE by-way-of-first-deleting-and-then-inserting based关于参与者是否已经在场。

如果这是您想要做的事情的要点,您应该考虑使用 UPSERT 语法。这是一种将这种类型的“INSERT 或 UPDATE if-it-exists-already”逻辑推送到 DML 中的方法,这通常更容易用代码编写(更少的代码)并且更容易在 SQL 控制台之外进行测试代码也是。

这是一个讨论 Upserts 的示例参考指南。 https://blog.usejournal.com/update-insert-upsert-multiple-records-in-different-db-types-63aa44191884

因此,在您的代码中,此策略将允许您将“removeParticipants”作为一种方法删除,然后将 INSERT_INBOX_PARTICIPANT 替换为直接 INSERT,例如 INSERT INTO inbox_participant,您将拥有类似...

MERGE INTO INBOX_PARTICIPANT T USING 
(VALUES
    (?, ?, ?)
) S (INBOX_ID, PARTICIPANT_ID, SOME_DATA) 
ON T.INBOX_ID = S.INBOX_ID and T.PARTICIPANT_ID= S.PARTICIPANT_ID
WHEN MATCHED THEN UPDATE SET SOME_DATA = S.SOME_DATA
WHEN NOT MATCHED THEN INSERT (INBOX_ID, PARTICIPANT_ID, SOME_DATA) 
VALUES (S.INBOX_ID, S.PARTICIPANT_ID, S.SOME_DATA);

**注意:确切的语法因您的底层数据库而异!详情见页面! **

这可能会间接解决您的问题,但也会使代码更容易维护。

至于如果你想继续你原来的问题,我会调查你的 DELETE 是否在不经意间被回滚。

或者可能因为您在批处理模式下插入但作为直接执行更新删除,“批处理”模式插入可能会获得与删除不同的事务上下文,因为“批处理模式”可能会尝试启动单独的事务来管理批次。为了测试这个理论,也许可以尝试让删除和添加在相同的批处理上下文中运行。

此外,如果 removeParticipants 没有要删除的现有参与者,您似乎调用了回滚,因为 i==0。这是你想要的吗?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-09-05
    • 2020-05-20
    • 2011-05-01
    • 1970-01-01
    • 2021-05-30
    • 2022-11-03
    • 1970-01-01
    • 2011-06-29
    相关资源
    最近更新 更多