如何在JDBC中获取插入的ID?

433

我想在使用Java的JDBC时,在一个数据库(在我的情况下是Microsoft SQL Server)中插入一条记录。同时,我想要获取插入的ID。如何使用JDBC API实现这一功能?


1
在查询中保留自动生成的idString sql = "INSERT INTO 'yash'.'mytable' ('name') VALUES (?)"; int primkey = 0; PreparedStatement pstmt = con.prepareStatement(sql, new String[] { "id" }/*Statement.RETURN_GENERATED_KEYS*/); pstmt.setString(1, name); if (pstmt.executeUpdate() > 0) { java.sql.ResultSet generatedKeys = pstmt.getGeneratedKeys(); if (generatedKeys.next()) primkey = generatedKeys.getInt(1); } - Yash
提醒大家一下,只有使用AUTO INC类型才能获取生成的键。UUID、char或其他使用默认值的类型在MSSQL中无法使用。 - sproketboy
15个回答

748
如果它是自动生成的键,则可以使用Statement#getGeneratedKeys()。您需要在相同的Statement上调用它,就像用于INSERT的那个一样。您首先需要使用Statement.RETURN_GENERATED_KEYS创建语句,以通知JDBC驱动程序返回键。
下面是一个基本示例:
public void create(User user) throws SQLException {
    try (
        Connection connection = dataSource.getConnection();
        PreparedStatement statement = connection.prepareStatement(SQL_INSERT,
                                      Statement.RETURN_GENERATED_KEYS);
    ) {
        statement.setString(1, user.getName());
        statement.setString(2, user.getPassword());
        statement.setString(3, user.getEmail());
        // ...

        int affectedRows = statement.executeUpdate();

        if (affectedRows == 0) {
            throw new SQLException("Creating user failed, no rows affected.");
        }

        try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
            if (generatedKeys.next()) {
                user.setId(generatedKeys.getLong(1));
            }
            else {
                throw new SQLException("Creating user failed, no ID obtained.");
            }
        }
    }
}

请注意,是否能正常工作取决于JDBC驱动程序。目前,大多数最新版本都可以正常工作,但如果我没记错,Oracle JDBC驱动程序在这方面仍然有些麻烦。MySQL和DB2支持它已经有很长时间了。PostgreSQL不久前开始支持它。至于MSSQL,我从未使用过,无法评论。
对于Oracle,您可以在同一事务中调用带有“RETURNING”子句或“SELECT CURRVAL(sequencename)”(或任何特定于数据库的语法)的“CallableStatement”,以获取最后生成的键。也可以参考此答案

5
在插入数据之前获取序列中的下一个值要比在插入后获取当前值更好,因为在多线程环境(例如任何Web应用程序容器)中,后者可能返回错误的值。 JTDS MSSQL驱动程序支持getGeneratedKeys。 - JeeBee
5
我通常使用Oracle,因此对JDBC驱动程序的功能没有太高期望。 - JeeBee
8
不设置Statement.RETURN_GENERATED_KEYS选项的有趣副作用是错误信息非常难以理解,即“必须在获取任何结果之前执行语句。” - Chris Winters
8
如果数据库返回了生成的键,则 generatedKeys.next() 返回 true。注意,这是一个 ResultSetclose() 方法只是为了释放资源。否则,长时间运行时,您的数据库将耗尽资源,并且应用程序将崩溃。您只需要编写一些实用方法来执行关闭任务。请参见 答案。 - BalusC
5
对于大多数数据库/驱动程序,此方法是正确的。但是对于Oracle不适用。对于Oracle,请更改为:connection.prepareStatement(sql,new String[] {"主键列名称"}); - Darrell Teague
显示剩余18条评论

31
  1. 创建生成列

    String generatedColumns[] = { "ID" };
    
    将生成的列传递给您的语句
  2. PreparedStatement stmtInsert = conn.prepareStatement(insertSQL, generatedColumns);
    
    使用ResultSet对象在语句上获取生成的键。
    ResultSet rs = stmtInsert.getGeneratedKeys();
    
    if (rs.next()) {
        long id = rs.getLong(1);
        System.out.println("Inserted ID -" + id); // display inserted record
    }
    

12

如果在使用 Statement.RETURN_GENERATED_KEYS 时遇到“不支持的功能”错误,请尝试以下方法:

String[] returnId = { "BATCHID" };
String sql = "INSERT INTO BATCH (BATCHNAME) VALUES ('aaaaaaa')";
PreparedStatement statement = connection.prepareStatement(sql, returnId);
int affectedRows = statement.executeUpdate();

if (affectedRows == 0) {
    throw new SQLException("Creating user failed, no rows affected.");
}

try (ResultSet rs = statement.getGeneratedKeys()) {
    if (rs.next()) {
        System.out.println(rs.getInt(1));
    }
    rs.close();
}

其中BATCHID是自动生成的标识符。


你是指 BATCHID 吗? - moolsbytheway

9

我正在使用单线程的基于JDBC的应用程序访问Microsoft SQL Server 2008 R2,并在不使用RETURN_GENERATED_KEYS属性或任何PreparedStatement的情况下检索最后一个ID。 大概是这样的:

private int insertQueryReturnInt(String SQLQy) {
    ResultSet generatedKeys = null;
    int generatedKey = -1;

    try {
        Statement statement = conn.createStatement();
        statement.execute(SQLQy);
    } catch (Exception e) {
        errorDescription = "Failed to insert SQL query: " + SQLQy + "( " + e.toString() + ")";
        return -1;
    }

    try {
        generatedKey = Integer.parseInt(readOneValue("SELECT @@IDENTITY"));
    } catch (Exception e) {
        errorDescription = "Failed to get ID of just-inserted SQL query: " + SQLQy + "( " + e.toString() + ")";
        return -1;
    }

    return generatedKey;
} 

这篇博客很好地介绍了SQL Server的三个“最后ID”选项:http://msjawahar.wordpress.com/2008/01/25/how-to-find-the-last-identity-value-inserted-in-the-sql-server/ - 我还没有用到另外两个选项。

6
该应用程序只有一个线程并不意味着不存在竞态条件:如果两个客户端使用您的方法插入一行并检索ID,则可能会失败。 - 11684
你为什么要这样做呢?我只是庆幸自己不是那个可怜的家伙,在允许多线程时需要调试你的代码! - mjaggard
@11684 是的,你说得对。有些驱动程序不会通过 statement.getGeneratedKeys() 提供ID,这使得这种尝试“可以理解”。然而,在 prepareStatement 期间提供ID(s) 可以解决这个问题(例如 preapareStatement(query, new String[] {insertIdColumnName}))。请参阅 @Yash 的略微被低估的答案以获取更多详细信息。 - Levite

7

我不想发表评论,而只是想回答帖子。


接口 java.sql.PreparedStatement

  1. columnIndexes « You can use prepareStatement function that accepts columnIndexes and SQL statement. Where columnIndexes allowed constant flags are Statement.RETURN_GENERATED_KEYS1 or Statement.NO_GENERATED_KEYS[2], SQL statement that may contain one or more '?' IN parameter placeholders.

    SYNTAX «

    Connection.prepareStatement(String sql, int autoGeneratedKeys)
    Connection.prepareStatement(String sql, int[] columnIndexes)
    

    Example:

    PreparedStatement pstmt = 
        conn.prepareStatement( insertSQL, Statement.RETURN_GENERATED_KEYS );
    

  1. columnNames « List out the columnNames like 'id', 'uniqueID', .... in the target table that contain the auto-generated keys that should be returned. The driver will ignore them if the SQL statement is not an INSERT statement.

    SYNTAX «

    Connection.prepareStatement(String sql, String[] columnNames)
    

    Example:

    String columnNames[] = new String[] { "id" };
    PreparedStatement pstmt = conn.prepareStatement( insertSQL, columnNames );
    

完整示例:

public static void insertAutoIncrement_SQL(String UserName, String Language, String Message) {
    String DB_URL = "jdbc:mysql://localhost:3306/test", DB_User = "root", DB_Password = "";

    String insertSQL = "INSERT INTO `unicodeinfo`( `UserName`, `Language`, `Message`) VALUES (?,?,?)";
            //"INSERT INTO `unicodeinfo`(`id`, `UserName`, `Language`, `Message`) VALUES (?,?,?,?)";
    int primkey = 0 ;
    try {
        Class.forName("com.mysql.jdbc.Driver").newInstance();
        Connection conn = DriverManager.getConnection(DB_URL, DB_User, DB_Password);

        String columnNames[] = new String[] { "id" };

        PreparedStatement pstmt = conn.prepareStatement( insertSQL, columnNames );
        pstmt.setString(1, UserName );
        pstmt.setString(2, Language );
        pstmt.setString(3, Message );

        if (pstmt.executeUpdate() > 0) {
            // Retrieves any auto-generated keys created as a result of executing this Statement object
            java.sql.ResultSet generatedKeys = pstmt.getGeneratedKeys();
            if ( generatedKeys.next() ) {
                primkey = generatedKeys.getInt(1);
            }
        }
        System.out.println("Record updated with id = "+primkey);
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    }
}

1
在多线程运行时环境中使用这个解决方案是否安全? - The Prototype
这篇文章值得更多的点赞!!它解决了即使对于旧的驱动程序也可以返回ID的问题 - 不需要使用@@IDENTIY(当提供请求ID的String Array时)。 - Levite

3
我正在使用SQLServer 2008,但是我有一个开发限制:我不能使用新的驱动程序,我必须使用 "com.microsoft.jdbc.sqlserver.SQLServerDriver"(我不能使用 "com.microsoft.sqlserver.jdbc.SQLServerDriver")。
这就是为什么解决方案conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)对我抛出了一个java.lang.AbstractMethodError
在这种情况下,我找到的一个可能的解决方案是微软建议的旧方法:如何使用JDBC检索@@IDENTITY值
import java.sql.*; 
import java.io.*; 

public class IdentitySample
{
    public static void main(String args[])
    {
        try
        {
            String URL = "jdbc:microsoft:sqlserver://yourServer:1433;databasename=pubs";
            String userName = "yourUser";
            String password = "yourPassword";

            System.out.println( "Trying to connect to: " + URL); 

            //Register JDBC Driver
            Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver").newInstance();

            //Connect to SQL Server
            Connection con = null;
            con = DriverManager.getConnection(URL,userName,password);
            System.out.println("Successfully connected to server"); 

            //Create statement and Execute using either a stored procecure or batch statement
            CallableStatement callstmt = null;

            callstmt = con.prepareCall("INSERT INTO myIdentTable (col2) VALUES (?);SELECT @@IDENTITY");
            callstmt.setString(1, "testInputBatch");
            System.out.println("Batch statement successfully executed"); 
            callstmt.execute();

            int iUpdCount = callstmt.getUpdateCount();
            boolean bMoreResults = true;
            ResultSet rs = null;
            int myIdentVal = -1; //to store the @@IDENTITY

            //While there are still more results or update counts
            //available, continue processing resultsets
            while (bMoreResults || iUpdCount!=-1)
            {           
                //NOTE: in order for output parameters to be available,
                //all resultsets must be processed

                rs = callstmt.getResultSet();                   

                //if rs is not null, we know we can get the results from the SELECT @@IDENTITY
                if (rs != null)
                {
                    rs.next();
                    myIdentVal = rs.getInt(1);
                }                   

                //Do something with the results here (not shown)

                //get the next resultset, if there is one
                //this call also implicitly closes the previously obtained ResultSet
                bMoreResults = callstmt.getMoreResults();
                iUpdCount = callstmt.getUpdateCount();
            }

            System.out.println( "@@IDENTITY is: " + myIdentVal);        

            //Close statement and connection 
            callstmt.close();
            con.close();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }

        try
        {
            System.out.println("Press any key to quit...");
            System.in.read();
        }
        catch (Exception e)
        {
        }
    }
}

这个解决方案对我很有效!

希望这能帮到你!


尝试提供你想要的String[] ID名称数组,而不是使用RETURN_GENERATED_KEYS。这样就可以突然获得有效的ResultSet和其中的ID,通过getInt(1)获取。 - Levite

3
您可以使用以下Java代码获取新插入的ID。
ps = con.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
ps.setInt(1, quizid);
ps.setInt(2, userid);
ps.executeUpdate();

ResultSet rs = ps.getGeneratedKeys();
if (rs.next()) {
    lastInsertId = rs.getInt(1);
}

2

它可以与普通的Statement一起使用(不仅仅是PreparedStatement

Statement statement = conn.createStatement();
int updateCount = statement.executeUpdate("insert into x...)", Statement.RETURN_GENERATED_KEYS);
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
  if (generatedKeys.next()) {
    return generatedKeys.getLong(1);
  }
  else {
    throw new SQLException("Creating failed, no ID obtained.");
  }
}

那对我很有帮助。 - Thilina Sampath

1
使用 Hibernate 的 NativeQuery 时,需要返回 ResultList 而不是 SingleResult,因为 Hibernate 修改了本地查询。
INSERT INTO bla (a,b) VALUES (2,3) RETURNING id

喜欢

INSERT INTO bla (a,b) VALUES (2,3) RETURNING id LIMIT 1

如果你试图获取单个结果,这会导致大多数数据库(至少是PostgreSQL)抛出语法错误。之后,你可以从列表中获取结果id(通常只包含一个项目)。


1

大多数人建议使用JDBC API来完成这个任务,但是我个人认为使用大多数驱动程序会非常痛苦。实际上,你可以使用本地的T-SQL特性OUTPUT子句

try (
    Statement s = c.createStatement();
    ResultSet rs = s.executeQuery(
        """
        INSERT INTO t (a, b)
        OUTPUT id
        VALUES (1, 2)
        """
    );
) {
    while (rs.next())
        System.out.println("ID = " + rs.getLong(1));
}

这是SQL Server以及其他几种SQL方言(例如Firebird、MariaDB、PostgreSQL,使用 RETURNING 而不是 OUTPUT )的最简单解决方案。

我在这里更详细地介绍了这个主题


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