【问题标题】:JDBC PreparedStatement always returns 1 as auto generated key [duplicate]JDBC PreparedStatement 始终返回 1 作为自动生成的键 [重复]
【发布时间】:2013-02-13 01:13:26
【问题描述】:

我有这段代码试图在数据库中插入一条记录:

try {
 Connection conn = getConnection();

 String sql = 
   "INSERT INTO myTable(userId,content,timestamp) VALUES(?,?,NOW())";
 PreparedStatement st = 
    conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);

 st.setLong(1, userId);
 st.setString(2, content);
 id = st.executeUpdate(); //this is the problem line            
} catch(Exception e) {}

问题是,虽然记录插入正确,但我希望id 包含刚刚插入的记录的主键+ auto_increment id。但是,由于某种原因,它总是返回 '1' 作为 id,可能是因为在插入期间 userId 的值是 1。

我的表是 InnoDB。起初userId 是另一个表的外键,因为我已经删除了外键甚至 userId 列上的索引,但我仍然得到 1 作为返回值。

任何想法我做错了什么?

【问题讨论】:

    标签: java mysql jdbc


    【解决方案1】:

    PreparedStatment.executeUpdate()

    回报:
    (1) SQL 数据操作语言 (DML) 语句的行数或 (2) 0 用于不返回任何内容的 SQL 语句

    您需要改用execute() 并使用getGeneratedKeys() 获取ResultSet;它将包含您想要的数据。

    编辑添加:我阅读了您的问题,因为表中有一个自动递增字段不是 userId

    【讨论】:

    • 那么如果我必须通过获取结果集来获取它,RETURN_GENERATED_KEYS 的意义何在?
    • 嗯?如果你不指定......你根本不会让他们回来。它没有进行 another 查询 - 问题是您正在丢弃返回的数据,因为您使用的是executeUpdate()
    • 我切换到st.executeQuery();,现在我得到:DB Exception occured:java.sql.SQLException: Can not issue data manipulation statements with executeQuery().
    • 啊抱歉,这部分是从记忆中完成的,忘记阅读文档后必须使用execute() :) - 编辑
    • 感谢您的帮助。做ResultSet rs =getGeneratedKeys() 然后rs.next(); id = rs.getLong(1) 为我工作。
    【解决方案2】:

    Brian Roach 的 accepted Answer 是正确的。我正在添加一些想法和一个带有完整代码的示例。

    RETURN_GENERATED_KEYS 不是的意思是“返回生成的密钥”

    原来的发帖人似乎被标志Statement.RETURN_GENERATED_KEYS 的措辞弄糊涂了,这是可以理解的。与直觉相反,传递此标志不会改变PreparedStatement::executeUpdate 方法的行为。该方法总是返回一个int,即受执行的 SQL 影响的行数。 “executeUpdate”方法从不返回生成的密钥。

    int countRowsAffected = pstmt.executeUpdate();  // Always return number of rows affected, *not* the generated keys.
    

    问,你就会收到

    如果你想要生成的密钥,你必须做两个步骤:

    1. 传递标志,
    2. 要求ResultSet 由仅包含生成的键值的行组成。

    这种安排允许您添加取回生成的键的行为,同时保持其他所需的行为,获取受影响的行数。

    示例代码

    这是一个几乎真实的示例,取自 Java 8 应用程序,该应用程序从数据馈送中抓取数据。我认为在这种情况下,一个完整的例子可能比一个最小的例子更有用。

    次要细节…这段代码可能并不完美,无论是语法还是其他方面,因为我复制粘贴修改了真实源代码。我使用UUID data type 而不是整数作为我表的surrogate 主键。 CharHelperDBHelper 类是我自己的,这里的细节不重要。 xy 变量是我自己的应用程序有意义的数据的替代品。我的日志调用是对SLF4J 框架进行的。 UUID hex 字符串是将日志中的报告链接回原始源代码的便捷方式。数据库是Postgres,但是这种代码应该适用于任何支持生成密钥报告的数据库。

    public UUID dbWrite (  String x , String y , DateTime whenRetrievedArg ) {
        if ( whenRetrievedArg == null ) {
            logger.error( "Passed null for whenRetrievedArg. Message # 2112ed1a-4612-4d5d-8cc5-bf27087a350d." );
            return null;
        }
    
        Boolean rowInsertComplete = Boolean.FALSE; // Might be used for debugging or logging or some logic in other copy-pasted methods.
    
        String method = "Method 'dbWrite'";
        String message = "Insert row for some_table_ in " + method + ". Message # edbea872-d3ed-489c-94e8-106a8e3b58f7.";
        this.logger.trace( message );
    
        String tableName = "some_table_";
    
        java.sql.Timestamp tsWhenRetrieved = new java.sql.Timestamp( whenRetrievedArg.getMillis() );  // Convert Joda-Time DatTime object to a java.sql.Timestamp object.
    
        UUID uuidNew = null;
    
        StringBuilder sql = new StringBuilder( AbstractPersister.INITIAL_CAPACITY_OF_SQL_STRING ); // private final static Integer INITIAL_CAPACITY_OF_SQL_STRING = 1024;
        sql.append( "INSERT INTO " ).append( tableName ).append( CharHelper.CHAR.PAREN_OPEN_SPACED ).append( " x_ , y_ " ).append( CharHelper.CHAR.PAREN_CLOSED ).append( DBHelper.SQL_NEWLINE );
        sql.append( "VALUES ( ? , ? , ?  ) " ).append( DBHelper.SQL_NEWLINE );
        sql.append( ";" );
    
        try ( Connection conn = DBHelper.instance().dataSource().getConnection() ;
    

    这里我们执行步骤#1,传递RETURN_GENERATED_KEYS 标志。

                PreparedStatement pstmt = conn.prepareStatement( sql.toString() , Statement.RETURN_GENERATED_KEYS ); ) {
    

    我们继续准备和执行语句。请注意,int countRows = pstmt.executeUpdate(); 返回受影响的行数,而不是生成的键。

            pstmt.setString( 1 , x ); 
            pstmt.setString( 2 , y ); 
            pstmt.setTimestamp( 3 , tsWhenRetrieved );  
            // Execute
            int countRows = pstmt.executeUpdate();  // Always returns an int, a count of affected rows. Does *not* return the generated keys.
            if ( countRows == 0 ) {  // Bad.
                this.logger.error( "Insert into database for new " + tableName + " failed to affect any rows. Message # 67e8de7e-67a5-42a6-a4fc-06929211e6e3." );
            } else if ( countRows == 1 ) {  // Good.
                rowInsertComplete = Boolean.TRUE;
            } else if ( countRows > 1 ) {  // Bad.
                rowInsertComplete = Boolean.TRUE;
                this.logger.error( "Insert into database for new " + tableName + " failed, affecting more than one row. Should not be possible. Message # a366e215-6cf2-4e5c-8443-0b5d537cbd68." );
            } else { // Impossible.
                this.logger.error( "Should never reach this Case-Else with countRows value " + countRows + " Message # 48af80d4-6f50-4c52-8ea8-98856873f3bb." );
            }
    

    这里我们执行第 2 步,请求生成密钥的 ResultSet。在本例中,我们插入了一行并期望返回一个生成的键。

            if ( rowInsertComplete ) {
                // Return new row’s primary key value.
                ResultSet genKeys = pstmt.getGeneratedKeys();
                if ( genKeys.next() ) {
                    uuidNew = ( UUID ) genKeys.getObject( 1 );  // ResultSet should have exactly one column, the primary key of INSERT table.
                } else {
                    logger.error( "Failed to get a generated key returned from database INSERT. Message # 6426843e-30b6-4237-b110-ec93faf7537d." );
                }
            }
    

    剩下的就是错误处理和清理。请注意,我们在此代码的底部返回 UUID,即插入记录的生成主键。

        } catch ( SQLException ex ) {
            // We expect to have occasional violations of unique constraint on this table in this data-scraping app.
            String sqlState = ex.getSQLState();
            if ( sqlState.equals( DBHelper.SQL_STATE.POSTGRES.UNIQUE_CONSTRAINT_VIOLATION ) ) {  // SqlState code '23505' = 'unique_violation'.
                this.logger.trace( "Found existing row when inserting a '" + tableName + "' row for y: " + y + ". Expected to happen on most attempts. Message # 0131e8aa-0bf6-4d19-b1b3-2ed9d333df27." );
                return null; // Bail out.
            } else { // Else any other exception, throw it.
                this.logger.error( "SQLException during: " + method + " for table: " + tableName + ", for y: " + y + ". Message # 67908d00-2a5f-4e4e-815c-5e5a480d614b.\n" + ex );
                return null; // Bail out.
            }
        } catch ( Exception ex ) {
            this.logger.error( "Exception during: " + method + " for table: " + tableName + ", for y: " + y + ". Message # eecc25d8-de38-458a-bb46-bd6f33117969.\n" + ex );
            return null;  // Bail out.
        }
    
        if ( uuidNew == null ) {
            logger.error( "Returning a null uuidNew var. SQL: {} \nMessage # 92e2374b-8095-4557-a4ed-291652c210ae." , sql );
        }
        return uuidNew;
    }
    

    【讨论】:

    • 在你的步骤#2中,在什么情况下你会到达else,其中插入了一行(因为countRows == 1),但getGeneratedKeys()失败的?我会认为这是一个轻浮的if / else(就像最后一个标记为“不可能”的else),只是我当时正试图弄清楚我是如何到达那里的。
    • @Menachem 严格来说,if() 检查似乎是不可能的。但我已经学会了期待不可能的事情。我在前面的if ( countRows… 中的多行不会停止控制流,因此我们最终可能会继续要求生成零插入行的键。即使countRows 检查停止了控制流,这些检查也可能会因为诸如< 而不是> 之类的拼写错误而搞砸。另外,生成的密钥可能会出现问题,所以我正在检查。我练习防御性编码,一路上对我的许多假设进行额外检查,即使理论上是不可能的。
    • 谢谢。我的观点是:countRows ==1genKeys.next() 返回 false 的(看似)不可能的情况——这就是我现在所处的位置,我正试图找出原因。见this question
    • @Menachem Ahh,您问我是否知道可能导致getGeneratedKeys 偶尔失败并产生空结果集的实际问题/问题/情况。我不知道。我确实认为该命令失败,因此添加了我的防御性编码进行检查。但那是由于偏执的担心,而不是对任何真正问题的了解。我还没有出现过这样的问题。 Statement 类文档确实提到了专门抛出 SQLFeatureNotSupportedException 的方法。但这并不能解释你的间歇性失败。
    【解决方案3】:
    String SQLQuery=" ";
    
    String generatedKeys[]= {"column_name"};//'column_name' auto-increment column
    
    prepSt = Connection.prepareStatement(SQLQuery,generatedKeys);
    
    prepSt.setInt(1, 1234); 
    
    .....
    
    .....
    
    ....
    
    
    prepSt.executeUpdate();
    
    ResultSet rs = prepSt.getGeneratedKeys; // used same PreparedStatement object as used   for Insert .
    
    
    if(rs.next()) {
    
    
    int id=rs.getLong("column_name");
    
    
    System.out.println(id);
    
    
    }
    } catch (SQLException e) {
    }
    

    【讨论】:

    • 这里似乎有一个没有尝试的问题,所以有点混乱。请您用文字和代码解释您的答案吗?
    • @Parb 感谢您发布一些示例代码。但请添加一些讨论。 StackOverflow 旨在具有教育意义和启发性,而不仅仅是一个 sn-p 集合。
    【解决方案4】:

    如果您已在数据库中设置userId 具有自动增量,则不应尝试自行添加。你应该插入NULL,它会为你自动递增! (线索就在名字里!)

    另外,您不是在更新您的表格,而是在插入表格。所以你不执行Update()。试试……

    PreparedStatement pst = conn.prepareStatement("INSERT INTO myTable(userId,content,timestamp) VALUES(NULL,?,NOW())");
    pst.setString(1, content);
    pst.executeQuery();
    

    【讨论】:

    • 不,userId 不是 auto_increment,一个单独的字段 id 是 auto_increment
    • 您能描述一下您的桌子,以便我们知道您到底想添加什么吗?
    【解决方案5】:

    您得到的是“插入行”的通知(对于 INSERT 语句)。我们使用这种方法来知道我们的 DML 查询是否成功。以下是使用 [prepareStatement(yourSQL, Statement.RETURN_GENERATED_KEYS)] 获取自动生成 ID 的方法。请注意,此方法仅返回您一个 RowID 参考。获取实际值,请参考方法二。

    (方法一)

    Try{
    String yourSQL="insert into Table1(Id,Col2,Col3) values(SEQ.nextval,?,?)";
    myPrepStatement = <Connection>.prepareStatement(yourSQL, Statement.RETURN_GENERATED_KEYS);
    myPrepStatement.setInt(1, 123); 
    myPrepStatement.setInt(2, 123); 
    
    myPrepStatement.executeUpdate();
    ResultSet rs = getGeneratedKeys;
    if(rs.next()) {
      java.sql.RowId rid=rs.getRowId(1); 
      //what you get is only a RowId ref, try make use of it anyway U could think of
      System.out.println(rid);
    }
    } catch (SQLException e) {
    }
    

    (方法二)

    Try{
    String yourSQL="insert into Table1(Id,Col2,Col3) values(SEQ.nextval,?,?)";
    //IMPORTANT: here's where other threads don tell U, you need to list ALL cols 
    //mentioned in your query in the array
    myPrepStatement = <Connection>.prepareStatement(yourSQL, new String[]{"Id","Col2","Col3"});
    myPrepStatement.setInt(1, 123); 
    myPrepStatement.setInt(2, 123); 
    myPrepStatement.executeUpdate();
    ResultSet rs = getGeneratedKeys;
    if(rs.next()) {
    //In this exp, the autoKey val is in 1st col
      int id=rs.getLong(1);
      //now this's a real value of col Id
      System.out.println(id);
    }
    } catch (SQLException e) {
    }
    

    基本上,如果您只想要 SEQ.Nextval 的值,请尽量不要使用 Method1,b'cse 它只返回 RowID ref,您可能会想方设法使用它,它也不适合所有数据类型您尝试将其投射到!这在 MySQL、DB2 中可能工作正常(返回实际 val),但在 Oracle 中不行。

    重要提示: 在调试时关闭 SQL Developer、Toad 或任何使用相同登录会话执行 INSERT 的客户端。它可能不会每次都影响你(调试调用)......直到你发现你的应用程序无一例外地冻结了一段时间。是的……毫无例外地停止!

    【讨论】:

      猜你喜欢
      • 2012-05-16
      • 2018-07-02
      • 2020-12-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-21
      • 1970-01-01
      相关资源
      最近更新 更多