Javaでデータベースアプリケーションを学び始めたころ、私自身が「なぜ、こんな書き方をするのだろう?」と悩んだコードの一つが、JDBCのgetGeneratedKeys()でした。
INSERTそのものは比較的分かりやすいと思います。
pstmt.executeUpdate();
これでレコードを追加できます。
ところが、自動採番される主キーを取得しようとすると、急に次のようなコードが登場します。
PreparedStatement pstmt = conn.prepareStatement(
INSERT_SQL,
Statement.RETURN_GENERATED_KEYS);
さらに、INSERTを実行した後で、
ResultSet rs = pstmt.getGeneratedKeys();
そして、
if (rs.next()) {
return rs.getInt(1);
}
と続きます。
初めて見たとき、この数行のコードだけでいくつもの疑問を持ちました。
-
INSERTなのに、なぜResultSetが出てくるのか - IDを1個取得したいだけなのに、なぜ
rs.next()が必要なのか -
getInt(1)の1は何を意味するのか -
PreparedStatementを使っているのに、なぜStatement.RETURN_GENERATED_KEYSなのか - そもそも
getGeneratedKeys()は、どこからIDを取得しているのか
この記事では、こうした疑問を一つずつ整理しながら、
JDBCでINSERTした直後に、データベースが自動採番したIDをJava側で取得する仕組み
を見ていきます。
今回使うテーブル
例として、PostgreSQLに次のcategoriesテーブルがあるものとします。
CREATE TABLE categories (
category_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
category_name VARCHAR(10) NOT NULL
);
category_idはGENERATED ALWAYS AS IDENTITYになっているため、レコードを追加するとPostgreSQLがIDを自動的に割り当てます。
例えば、分類名として「交際費」を追加するとします。
SQLは次のようになります。
INSERT INTO categories (category_name)
VALUES (?);
category_idはSQLに指定していません。
PostgreSQL側で自動採番されるからです。
INSERTはできた。でもIDは何番?
Java側では、例えば次のように実行できます。
private static final String INSERT_SQL =
"INSERT INTO categories (category_name) VALUES (?)";
PreparedStatement pstmt =
conn.prepareStatement(INSERT_SQL);
pstmt.setString(1, "交際費");
pstmt.executeUpdate();
これで登録自体はできます。
しかし、ここで一つ問題があります。
PostgreSQLでは、
category_id = 自動採番された値
category_name = 交際費
というレコードが作られていますが、Java側はまだ、
「今追加されたレコードのcategory_idが何番なのか」
を知りません。
例えば登録後に、
分類が追加されました。(新しいID: 9)
のように表示したい場合、このIDをJava側へ返してもらう必要があります。
処理の流れで考えると、こうなります。
Java
│
│ INSERT「交際費」
↓
PostgreSQL
│
│ category_idを自動採番
↓
レコードを登録
│
│ 自動生成したIDを返す
↓
Java
この最後の、
「データベースが生成したIDをJava側で受け取る」
ために使うのがgetGeneratedKeys()です。
解決方法の全体像
細かなコードを見る前に、処理全体を把握しておきましょう。
必要なのは次の4段階です。
① 自動生成されたキーを取得したい、と指定する
↓
② INSERTを実行する
↓
③ 生成されたキーを取得する
↓
④ ResultSetからIDを読み取る
JDBCのコードに対応させると、
Statement.RETURN_GENERATED_KEYS
↓
executeUpdate()
↓
getGeneratedKeys()
↓
rs.next()
↓
rs.getInt(1)
となります。
完成形を先に見ると、次のようになります。
public int insertCategory(CategoryDTO category)
throws SQLException {
try (Connection conn =
DatabaseConnection.getConnection();
PreparedStatement pstmt =
conn.prepareStatement(
INSERT_SQL,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(
1, category.getCategoryName());
pstmt.executeUpdate();
try (ResultSet rs =
pstmt.getGeneratedKeys()) {
if (rs.next()) {
return rs.getInt(1);
} else {
throw new SQLException(
"分類の登録は実行されましたが、"
+ "自動生成されたIDを取得できませんでした。");
}
}
}
}
初めて見ると少し複雑です。
しかし、一つずつ分解すると、それぞれの役割はかなり明確です。
1. RETURN_GENERATED_KEYSを指定する
最初のポイントはここです。
PreparedStatement pstmt =
conn.prepareStatement(
INSERT_SQL,
Statement.RETURN_GENERATED_KEYS);
通常なら、
conn.prepareStatement(INSERT_SQL);
だけでもINSERTは実行できます。
今回は第2引数として、
Statement.RETURN_GENERATED_KEYS
を指定しています。
これは、
「このSQLによって自動生成されたキーを、あとで取得したい」
という指定です。
つまり、getGeneratedKeys()をあとで呼び出すための準備を、PreparedStatementを作る段階で行っています。
PreparedStatementなのに、なぜStatement.RETURN_GENERATED_KEYS?
ここも、私が最初に少し引っかかったところです。
使っているオブジェクトは、
PreparedStatement
です。
それなのに指定する定数は、
Statement.RETURN_GENERATED_KEYS
となっています。
理由は単純で、RETURN_GENERATED_KEYSという定数がjava.sql.Statementに定義されているからです。
そのため、次のimportが必要になります。
import java.sql.Statement;
EclipseなどのIDEで自動importする場合は、同名の別クラスではなく、
java.sql.Statement
であることを確認してください。
ここでは、
PreparedStatementを使っている
↓
でもRETURN_GENERATED_KEYSという定数は
Statementに定義されている
↓
Statement.RETURN_GENERATED_KEYS
と考えればよいでしょう。
2. executeUpdate()でINSERTする
次は通常のINSERTと同じです。
pstmt.setString(
1, category.getCategoryName());
pstmt.executeUpdate();
ここでPostgreSQLへINSERT文が送られます。
例えば、
categoryName = "交際費"
なら、PostgreSQL側で新しいレコードが作られ、その際にcategory_idも自動生成されます。
ここで注意したいのは、
getGeneratedKeys()がINSERTを行うわけではない
ということです。
順序は、
まずINSERTする
↓
そのINSERTで生成されたキーを取得する
です。
したがって、
pstmt.executeUpdate();
の後でgetGeneratedKeys()を呼び出します。
executeUpdate()の戻り値がIDなのでは?
ここも混同しやすいところです。
executeUpdate()には戻り値があります。
例えば、
int rows = pstmt.executeUpdate();
と書けます。
しかし、このrowsは自動採番されたIDではありません。
INSERT、UPDATE、DELETEなどによって処理された行数を表します。
例えば1件のレコードを追加すれば、通常は、
rows = 1
となります。
しかし、
ID = 9
という意味ではありません。
したがって、
int newId = pstmt.executeUpdate();
として自動採番IDを取得することはできません。
生成されたキーは、別に取得する必要があります。
そこで登場するのが、
pstmt.getGeneratedKeys()
です。
3. getGeneratedKeys()で生成されたキーを取得する
INSERT実行後に、次のコードを書きます。
ResultSet rs =
pstmt.getGeneratedKeys();
実際には、ResultSetもクローズする必要があるため、try-with-resourcesを使って、
try (ResultSet rs =
pstmt.getGeneratedKeys()) {
// 自動生成されたIDを取得
}
とするのが分かりやすいでしょう。
ここで、初学者にとって少し意外なものが登場します。
ResultSetです。
INSERTなのに、なぜResultSetが出てくるのか?
JDBCを学び始めると、まずSELECTでResultSetを使うことが多いと思います。
ResultSet rs =
pstmt.executeQuery();
そして、
while (rs.next()) {
...
}
と書きます。
そのため、
「ResultSetはSELECTの検索結果を扱うもの」
という印象を持ちやすいかもしれません。
しかし、ここで扱っているResultSetは、categoriesテーブルをSELECTした検索結果ではありません。
pstmt.getGeneratedKeys()
によって得られる、
「今回のSQL実行によって生成されたキーの取得結果」
です。
イメージとしては、
INSERT実行
↓
PostgreSQLがIDを生成
↓
getGeneratedKeys()
↓
生成されたキーのResultSet
です。
今回、仮にcategory_id = 9が生成されたとします。処理を理解しやすくするため、取得したいIDの列だけを示すと、概念的には次のようになります。
生成されたキーの結果
1列目
-----
9
getGeneratedKeys()から、このような結果を受け取っていると考えると分かりやすくなります。
4. なぜrs.next()が必要なのか?
次に出てくるのが、
if (rs.next()) {
return rs.getInt(1);
}
です。
ここも、私自身が最初に疑問を持ったところです。
欲しいものはID一つだけです。
それなのに、なぜ、
rs.next()
するのでしょうか。
理由は、getGeneratedKeys()が返すものもResultSetだからです。
ResultSetを取得した直後のカーソルは、最初の行を指しているわけではありません。
取得直後
カーソル
↓
[最初の行より前]
1行目 9
そこで、
rs.next();
を実行します。
すると、
rs.next()後
カーソル
↓
1行目 9
となります。
この状態になって初めて、
rs.getInt(1)
で値を取得できます。
「INSERTした行の位置」へ移動しているわけではない
ここは特に重要です。
rs.next()と聞くと、
「categoriesテーブルの中で、今INSERTしたレコードの行まで移動しているのか?」
と思うかもしれません。
そうではありません。
今回操作しているのはcategoriesテーブルそのものではなく、
pstmt.getGeneratedKeys()
が返した生成キー用のResultSetです。
つまり、
categoriesテーブル
1 給与
2 配当金
3 雑収入
...
9 交際費
の中をrs.next()で移動しているわけではありません。
そうではなく、
getGeneratedKeys()の結果
1行目 9
という別の結果に対して、
rs.next();
しているのです。
今回のように1件のレコードをINSERTし、その1件について生成されたIDを取得する場合は、
getGeneratedKeys()
↓
生成されたキーの結果
↓
rs.next()
↓
1行目
↓
rs.getInt(1)
という流れになります。
この違いが分かると、rs.next()が必要な理由も理解しやすくなります。
rs.getInt(1)の1は何?
続いて、
return rs.getInt(1);
です。
この1は、自動採番されたIDの値ではありません。
ResultSetの1列目
という意味です。
例えば取得結果が、
1列目
-----
9
なら、
rs.getInt(1)
によって、
9
を取得します。
したがって、
if (rs.next()) {
return rs.getInt(1);
}
は、
生成されたキーの結果に1行目があるか確認する
↓
あれば、その1列目をintとして取得する
↓
そのIDをreturnする
という処理です。
なぜifなのか? whileではないのか?
SELECTで複数件を取得する場合は、
while (rs.next()) {
...
}
と書くことが多いでしょう。
しかし今回は、1件の分類を登録し、その1件について生成されたIDを一つ取得したいだけです。
そのため、
if (rs.next()) {
return rs.getInt(1);
}
で十分です。
そして、もし生成されたキーを取得できなかった場合は、
else {
throw new SQLException(
"分類の登録は実行されましたが、"
+ "自動生成されたIDを取得できませんでした。");
}
として異常な状態を呼び出し元へ伝えています。
完成コードをもう一度見る
ここまで理解したところで、最初のコードをもう一度見てみます。
public int insertCategory(CategoryDTO category)
throws SQLException {
try (Connection conn =
DatabaseConnection.getConnection();
PreparedStatement pstmt =
conn.prepareStatement(
INSERT_SQL,
Statement.RETURN_GENERATED_KEYS)) {
pstmt.setString(
1, category.getCategoryName());
pstmt.executeUpdate();
try (ResultSet rs =
pstmt.getGeneratedKeys()) {
if (rs.next()) {
return rs.getInt(1);
} else {
throw new SQLException(
"分類の登録は実行されましたが、"
+ "自動生成されたIDを取得できませんでした。");
}
}
}
}
それぞれの役割を書き加えると、次のようになります。
Statement.RETURN_GENERATED_KEYS
↓
「生成されたキーを後で取得したい」と指定
executeUpdate()
↓
INSERTを実行
getGeneratedKeys()
↓
INSERTによって生成されたキーを取得
rs.next()
↓
生成キーの結果の1行目へ移動
rs.getInt(1)
↓
1列目のIDをintとして取得
最初は少し複雑に見えたコードも、処理の流れが分かれば、それぞれの行が必要な理由を説明できるようになります。
呼び出し側では、取得したIDを使える
insertCategory()は、取得したIDをintで返しています。
そのため呼び出し側では、
CategoryDTO newCategory =
new CategoryDTO(0, name);
int newId =
categoryDAO.insertCategory(newCategory);
System.out.println(
"分類が追加されました。(新しいID: "
+ newId + ")");
のように使えます。
登録前は、
Java
categoryName = "交際費"
categoryId = まだ分からない
でした。
それが、
Java
│
│ INSERT
↓
PostgreSQL
│
│ IDを自動生成
↓
getGeneratedKeys()
│
↓
Java
newId = 自動生成されたID
となります。
これで冒頭の、
「INSERTは成功した。でも、今追加されたレコードのIDは何番なのか?」
という問題が解決しました。
まとめ
JDBCでINSERT後の自動採番IDを取得する基本的な流れは、次のようになります。
① 自動生成キーを取得することを指定する
Statement.RETURN_GENERATED_KEYS
↓
② INSERTする
executeUpdate()
↓
③ 生成されたキーを取得する
getGeneratedKeys()
↓
④ ResultSetの1行目へ移動する
rs.next()
↓
⑤ 1列目のIDを取得する
rs.getInt(1)
特に、最初は次の点が分かりにくいかもしれません。
-
executeUpdate()の戻り値は生成されたIDではなく、処理された行数 -
getGeneratedKeys()が返すのはResultSet - その
ResultSetはテーブルそのものではなく、生成されたキーの取得結果 -
rs.next()は、INSERTしたテーブル上の行へ移動しているのではなく、生成キーのResultSetの最初の行へ移動している -
getInt(1)の1は、ResultSetの1列目という意味
私自身、JDBCを学び始めたころは、これらが一つの処理としてつながって見えていませんでした。
getGeneratedKeys()というメソッド名だけを覚えるよりも、
「INSERT時にデータベースが生成したキーを、Java側へ受け取る一連の処理」
として理解すると、かなり分かりやすくなると思います。
サンプルコードを試してみたい方へ
この記事で使用したcategoriesテーブルを含むPostgreSQLの学習環境とJavaのサンプルコードは、次のサポートサイトで公開しています。
create_rakuraku.sqlを利用すると、この記事の題材になっている学習用データベースrakurakuを作成できます。
JDBCをコードを書きながら学びたい方へ
この記事のコードは、Kindle書籍 『実践Javaデータベースプログラミング: JDBC・PostgreSQLで学ぶ』 で扱っている題材をもとにしています。
書籍では、JDBCによる最小限のデータベース接続から始め、DTO、DAO、CRUDへと段階的にコードを発展させながら、PreparedStatement、ResultSet、executeUpdate()、getGeneratedKeys()などを実際に動かして確認していきます。
完成したコードだけを見るのではなく、「なぜ、このコードが必要になるのか」を手を動かしながら確認したい方には、続きとして読んでいただければと思います。