【问题标题】:The statement was aborted because it would have caused a duplicate key该语句被中止,因为它会导致重复键
【发布时间】:2016-09-21 12:08:17
【问题描述】:

我不断收到此错误:

Error code 20000, SQL state 23505
Insert command failed: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL160524112023610' defined on 'TEST'.

当我运行这段代码时:

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.Logger;

public class DerbyBatch {

    private static Connection connection;

    public static void main(String args[]) {
        try {
            createDatabase();
            createTable();
            insertBatch();
        } catch (ClassNotFoundException | SQLException ex) {
            Logger.getLogger(DerbyBatch.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void createDatabase() throws SQLException {
        connection = DriverManager.getConnection("jdbc:derby:" + new File("test").getAbsolutePath() + ";" + "create=true");
        disconnect();
    }

    public static void createTable() throws ClassNotFoundException, SQLException {
        connect();
        String createTable = "CREATE TABLE \"APP\".\"TEST\" (ID INTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY (START WITH 1, INCREMENT BY 1), TEXT VARCHAR (30000) NOT NULL, PRIMARY KEY (ID))";
        PreparedStatement preparedStatement = connection.prepareStatement(createTable);
        preparedStatement.executeUpdate();
        preparedStatement.close();
        connection.commit();
        disconnect();
    }

    public static void insertBatch() throws SQLException, ClassNotFoundException {
        connect();
        String sql = "INSERT INTO TEST (ID, TEXT) VALUES (?,?)";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setInt(1, 1);
        preparedStatement.setString(2, "TEST");
        preparedStatement.addBatch();
        preparedStatement.executeBatch();
        connection.commit();
        disconnect();
        Logger.getLogger(DerbyBatch.class.getName()).log(Level.SEVERE, "All data inserted.");
    }

    public static void connect() throws ClassNotFoundException, SQLException {
        Class.forName("net.sf.log4jdbc.DriverSpy");
        Connection temp = DriverManager.getConnection("jdbc:log4jdbc:derby:" + new File("test").getAbsolutePath());
        connection = new net.sf.log4jdbc.ConnectionSpy(temp);
        connection.setAutoCommit(false);
    }

    public static void disconnect() throws SQLException {
        connection.close();
    }
}

然后尝试在netbeans服务选项卡->数据库中手动插入一条记录:

我确保所有内容都已提交并且连接已正确关闭,所以我不确定为什么 derby 在执行批量插入后不断拾取重复索引?

【问题讨论】:

  • 我认为每次插入相同的 ID,这是您的主键。
  • 无法用给定的代码重现问题。
  • @Moh-Aw:我的德比版本是10.10.1.1,log4jdbc是1.2,slf4j-api是1.7.21,slf4j-log4j12是1.7.21
  • 如果您自己插入 ID,Derby 将使用您插入的内容,它不会生成 ID。这就是GENERATED BY DEFAULT 的意思。如果您希望 Derby 生成 ID,那么您应该使用 GENERATED ALWAYS,并且永远不要自己提供 ID,或者如果您想使用 GENERATED BY DEFAULT,那么您需要NOT 提供 ID如果您希望 Derby 为您生成它。 IOW,GENERATED BY DEFAULT 的意思是:“只有在我自己不插入 ID 时才生成 ID”。
  • 不要在这里发布文字图片,尤其是您已经发布了文字。

标签: java netbeans prepared-statement derby embedded-database


【解决方案1】:

据我所知,您的测试只会成功运行一次。之后,除非您在测试后删除创建的表,否则它将在重复插入、重复表创建或类似操作时失败。

【讨论】:

  • 您认为 derby 是否存在使用批量插入时绕过 START WITH 1, INCREMENT BY 1 的错误?也许这就是为什么在批量插入后手动插入记录时,它会尝试重用 1 作为 ID
  • 我认为这不是问题所在。查看您的表定义,我实际上猜想如果您取消表定义的“身份”属性,它会正常工作。许多数据库不喜欢手动插入标识列。
  • @nerdfest 关于手动插入身份列,GENERATED BY DEFAULTGENERATED ALWAYS 之间存在重要区别。以下是文档:db.apache.org/derby/docs/10.12/ref/rrefsqlj37836.html
【解决方案2】:

关于GENERATED BY DEFAULT,假设自动生成的值应该从最后一个存在的ID开始是错误的;它们将始终从 START WITH 值开始。

因此,当您尝试使用 NetBeans 插入另一行时,将首次触发自动 ID 生成,并尝试插入具有 START WITH 值的 ID;因为那是 1 就像您通过 Java 手动输入的 ID,它会因重复键错误而失败。

official documentation 也涵盖了您的具体情况:

create table greetings 
        (i int generated by default as identity, ch char(50));
-- specify value "1":
insert into greetings values (1, 'hi');
-- use generated default 
insert into greetings values (DEFAULT, 'salut');
-- use generated default 
insert into greetings(ch) values ('bonjour');  

请注意,与 GENERATED ALWAYS 列不同,GENERATED BY DEFAULT 列不保证唯一性。

因此,在上面的示例中, hisalut 行的标识值都是 1,因为 生成的列从 1 开始,用户指定的值也是 1.

为了防止重复,尤其是在加载或导入时 数据,使用对应于的START WITH 值创建表 系统应分配的第一个标识值。

因此,您要么不手动插入 ID,而只使用自动生成,要么将 START WITH 相应地更改为手动插入的 ID,例如 START WITH 2

您也可以稍后使用ALTER TABLERESTART WITH 更改START WITH 的值; documentation of ALTER TABLE 有一个示例可以解决与您类似的情况:

考虑下面的例子,它涉及到一个组合 自动生成数据和手动插入数据:

CREATE TABLE tauto(i INT GENERATED BY DEFAULT AS IDENTITY, k INT)
CREATE UNIQUE INDEX tautoInd ON tauto(i)
INSERT INTO tauto(k) values 1,2

系统 将自动为标识列生成值。但现在 您需要手动将一些数据插入到标识列中:

INSERT INTO tauto VALUES (3,3)
INSERT INTO tauto VALUES (4,4)
INSERT INTO tauto VALUES (5,5)

标识列已使用值 15 at 这点。如果您现在希望系统生成一个值,则系统 将生成一个3,这将导致唯一键异常,因为 3 的值已经被手动插入了。

为了弥补 手动插入,为标识列发出ALTER TABLE 语句 RESTART WITH 6:

ALTER TABLE tauto ALTER COLUMN i RESTART WITH 6

【讨论】:

  • 不知道有没有查询可以在批量插入后更新START WITH
  • @Hooli 是的,您可以将ALTER TABLERESTART WITH 一起使用,我更新了答案并添加了这个。
猜你喜欢
  • 2021-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-05
  • 2022-06-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多