こんにちは、鈴木です。
Spring4 の JdbcTemplate で INSERT したときに、DB 側で自動生成された値を取得する方法を調べました。
主キーを SERIAL 型にしておいて INSERT 時に値を自動生成させたり、レコードの作成日時を保持するカラムに「DEFAULT CURRENT_TIMESTAMP」などと指定することはあるのではないでしょうか。そのような INSERT 時に自動生成された値を取得したいと思います。
RDBMS ごとにやり方は異なると思いますが、今回は PostgreSQL の場合です。
データベースとテーブルを作る
まずはデータベースを作ります。
1 |
createdb spring4_sample |
データベースができたら psql コマンドで接続できるはずです。
1 |
psql spring4_sample |
次にテーブルを作ります。何でもよいのですが、users テーブルを作ってみます。
1 2 3 4 5 6 7 8 9 10 |
CREATE TABLE users ( -- 主キー. id SERIAL PRIMARY KEY, -- 名前. name VARCHAR(20) NOT NULL, -- 年齢. age INTEGER NOT NULL ); |
id カラムは SERIAL 型にして、値が自動生成されるようにしています。
他のカラムは何でもよいのですが、名前と年齢を保持するカラムを作っておきました。
Maven でプロジェクトを作る
maven でプロジェクトを作ります。
1 |
mvn archetype:create -DgroupId=com.example.spring4.hello -DartifactId=hello |
pom.xml を編集します。「追加 - ここから」「追加 - ここまで」の間が追加した部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.spring4.hello</groupId> <artifactId>hello</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>hello</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> <!-- 追加 - ここから --> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901-1.jdbc4</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.0.0.RELEASE</version> </dependency> <!-- 追加 - ここまで --> </dependencies> </project> |
とりあえず INSERT するコード
本題とは関係無いコードが多くなってしまうので、とりあえず INSERT するプログラムを書き、その後で必要な箇所をピックアップします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
package com.example.spring4.hello; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.Transactional; @Component @Configuration @ComponentScan @EnableTransactionManagement public class App { public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(App.class); context.getBean(App.class).run(); } @Autowired private DataSource dataSource; @Transactional public void run() { final String name = "たろう"; final Integer age = 20; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", name, age); // TODO ここで自動生成された id の値を取得する } @Bean protected DataSource createDataSource() { SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); dataSource.setDriverClass(org.postgresql.Driver.class); dataSource.setUrl("jdbc:postgresql://localhost/spring4_sample"); // 接続ユーザやパスワードを指定する場合は以下のメソッドを使う. // dataSource.setUsername(""); // dataSource.setPassword(""); return dataSource; } @Bean @Autowired protected PlatformTransactionManager createTransactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } } |
App#run メソッドの中が、とりあえず INSERT するコードです。
INSERT するために JdbcTemplate#update メソッドを使っています。JdbcTemplate#update は INSERT や UPDATE, DELETE するときに使える割と汎用的なメソッドで、影響のあったレコード数を返します。つまり、JdbcTemplate#update メソッドでは自動生成された id の値を取得できません。
ということで、どうしましょう・・。
lastval 関数を使う方法
PostgreSQL には lastval という関数があります。これを使うことで現在のセッションで最後に生成された値(正確には nextval で取得された値)を取得することができます。
コードを書くと以下のようになります。
1 2 3 4 5 6 7 8 9 10 |
@Transactional public void run() { final String name = "たろう"; final Integer age = 20; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", name, age); Integer id = jdbcTemplate.queryForObject("SELECT lastval()", Integer.class); System.out.println(id); } |
ちなみに lastval に似ている currval という関数もあります。currval は引数で指定したシーケンスの最後の値を返しますが、lastval は最後に nextval されたシーケンスの最後の値を返します。currval だとシーケンス名を指定する手間があるので、lastval を使うことにしました。
RETURNING 句を使う方法
別の方法として INSERT 文の最後に「RETURNING id」を加えた SQL を実行することで、INSERT しつつ自動生成された id の値を返すことができます。
コードにすると以下のようになります。
1 2 3 4 5 6 7 8 9 |
@Transactional public void run() { final String name = "たろう"; final Integer age = 20; JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); Integer id = jdbcTemplate.queryForObject("INSERT INTO users (name, age) VALUES (?, ?) RETURNING id", Integer.class, name, age); System.out.println(id); } |
lastval を使う方法では INSERT する SQL と lastval を実行する SQL を分けて実行する必要がありましたが、この方法では 1 回の SQL で完結します。
比較する
どちらの方法が良いか比較してみます。
lastval 関数を使う方法
- lastval は PostgreSQL8.1 からサポートされている。
(see 「PostgreSQL 8.1.9文書 - 9.12. シーケンス操作関数」) - SQL の実行回数が増えてしまう。
- カラムの名前を気にしなくて良い。
RETURNING 句を使う方法
- RETURNING 句は PostgreSQL 8.2 からサポートされている。
(see 「PostgreSQL 8.2.6文書 - INSERT」) - SQL の実行回数は増えない。
- カラムの名前を気にする必要がある。(カラム名が piyo なら「RETURNING piyo」)
まとめ
まとめると、
- RETURNING 句を使う方法が良い(実行する SQL が増えないので)。
- PostgreSQL のバージョンが 8.2 未満の場合は lastval を使う方法。
- SQL の実行回数が増えても問題にならない場合は lastval の方法が楽(カラム名を気にしなくて良いので)。
だと思います。