Springでデータベースを使う場合はJdbcTemplateというものが用意されている。当然SpringBoot上からもJdbcTemplateは使える。アプリを作る際に気になるのは、コネクションはどうやってとるのとか、コネクションのコミット、ロールバック、クローズ方法やトランザクションなどだけど以下を確認した。以下自分の備忘録的なログなので別に実行ログとかは添付しない。ご参考程度に
以下まとめ
- JdbcTemplateを使う限り、JDBCのコネクションを意識する必要はない
- autoCommitはデフォルトでtrueがセットされている
- コネクションプーリングはHIKARI-CPが推奨
- autoCommitをfalseにするにはHIKARI-CPの設定
- トランザクションはデフォルトでOFFでJdbcTemplateを使用するメソッドまたはクラスに@Transactional宣言をする
- @Transactionalをつけたメソッドの開始から終了までがトランザクションとなる
- 非検査例外が発生すると自動ロールバック
- 無事メソッドが終了するとコミットされる(余計なことを)
- @Transactionalを付加するとautoCommitはfalse扱い
以下に確認して行った事をソースベースで確認。
pom.xmlにJDBCの利用とJdbcTemplateの利用を宣言(こういう表現でいいのか?)以下ではPostgreSQLを利用。上の依存がJDBC、下の依存はポスグレの使用。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency>
JdbcTemplateの基本的な使い方
まず、JdbcTemplateの基本的な利用方法?JdbcTemplateインスタンス自身は@AutoWiredでインジェクション。各メソッドでJdbcTemplateインスタンス経由でDBにあアクセス。こんな感じのクラスを作成。コードが古いJavaとか言わない!そこの君!!
@Configuration public class DBAccess { @Autowired private JdbcTemplate _jdbcTemplate; public int insert(String table, String[] values) { Log log = LogFactory.getLog(getClass()); String sql = "insert into " + table + " values("; for (int i = 0 ; i < values.length; i++) { sql = sql + " ?,"; } sql = sql.substring(0, sql.length() - 1); sql = sql + ")"; log.debug("SQL: " + sql); return _jdbcTemplate.update(sql, values); } public List<Map<String, Object>> selectAll(String table) { String sql = "select * from " + table; return _jdbcTemplate.queryForList(sql); } }
利用するクラスはこんな感じに
@RestController public class HelloController { @Autowired DBAccess _dbaccess; @RequestMapping(value="/", method=RequestMethod.GET) @Transactional public ModelAndView index(ModelAndView mav, HttpServletRequest req, HttpServletResponse resp) { Log log = LogFactory.getLog(getClass()); List<Map<String, Object>> citylist = _dbaccess.selectAll("city_tbl"); for (Map<String, Object> record : citylist) { log.info("sort_key: " + record.get("sort_key") + " -- name: " + record.get("name")); } _dbaccess.insert("city_tbl", new String[] {"004", "test1"}); }
一応これでSELECTとINSERTができたのは確認できた。JdbcTemplateが使えたことを確認。INSERTなどの更新系のSQLを実行すると自動コミットされる。自動コミットされたのは、Connection#setAutoCommit()がtrueなため。
コネクションプーリングおよび、オートコミットの無効化
データベースを利用する場合、気になるのはコネクションのプーリング。SpringBootではHIKARI-CPというプーリング機構が推奨されているらしい。
また上のリンクに書いている通り、pom.xmlでspring-boot-starter-jdbcを指定していればこのHIKARI-CPのライブラリ一式の依存も解決してくれるみたい。あとはapplication.propertiesにその設定を書いていく。あと、設定最後にあるauto-commitの指定でオートコミットの無効化ができる。しかしオートコミットってプーリングに関するところで設定すんのが普通なんかな?という疑問は浮かぶ。
spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.pool-name=ConnectionPool spring.datasource.hikari.leakDetectionThreshold=5000 spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.auto-commit=false
application.propertiesの設定については以下のリンクが本家
https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
トランザクション
JDBCを使っている場合だと、トランザクションを意識する方法としてリクエスト中Connectionを引き回したりスレッドローカルでリクエスト中、同一のコネクションを使うようにするであったり、そういった仕組みを考える必要がありますが、SpringBoot自身ですでにトランザクションを用意しているみたい。
トランザクションについてはクラスメソッドの都元ダイスケさんという方が書いたこちらの記事が詳しいですが、メソッドに@Transactionalをつけるとそのメソッドの開始から終了までが1トランザクションとなり、終了時に非検査例外が発生していなければ自動的にコミットされます。不思議ですね〜。正直余計なことすんな!って思うのは古い人間なんでしょうか。
先ほど作ったクラスに@Transactionalを入れます。
@RestController public class HelloController { @Autowired DBAccess _dbaccess; @RequestMapping(value="/", method=RequestMethod.GET) @Transactional <-----------------------------------------これを入れる public ModelAndView index(ModelAndView mav, HttpServletRequest req, HttpServletResponse resp) { Log log = LogFactory.getLog(getClass()); 。。。省略
このトランザクションの種類も色々とあるみたいで、こちらのエントリが非常にわかりやすかったです。
pppurple.hatenablog.com
ここで気になるのは異なるJdbcTemplateオブジェクトを介してSQLを発行した際にどうなるのか?トランザクションは一緒になるのか?ということで、こんな実験を。まず、異なるJdbcTemplateインスタンスになるであろうDBAccess2クラスを作る
@Configuration public class DBAccess2 { @Autowired private JdbcTemplate _jdbcTemplate; public int insert(String table, String[] values) { Log log = LogFactory.getLog(getClass()); String sql = "insert into " + table + " values("; for (int i = 0 ; i < values.length; i++) { sql = sql + " ?,"; } sql = sql.substring(0, sql.length() - 1); sql = sql + ")"; log.debug("SQL: " + sql); return _jdbcTemplate.update(sql, values); } }
んで、コントローラでトランザクションを開始し、内部で異なるJdbcTemplateインスタンス経由でSQLを発行します。
@RequestMapping(value="/", method=RequestMethod.GET) @Transactional public ModelAndView index(ModelAndView mav, HttpServletRequest req, HttpServletResponse resp) { Log log = LogFactory.getLog(getClass()); // トランザクションのテスト log.info("dbaccess : " + _dbaccess); log.info("dbaccess2 : " + _dbaccess2); _dbaccess.insert("city_tbl", new String[] {"004", "test1"}); _dbaccess2.insert("city_tbl", new String[] {"005", "test2"}); mav.setViewName("index"); mav.addObject("msg", "input your name :"); return mav; }
実は_dbaccessと_dbaccess2が持つJdbcTemplateインスタンスは同一オブジェクトでした。ログでオブジェクトの持つハッシュコードで確認。不思議なのはコントローラから@Transactionalを取っても同一オブジェクトみたいで、不思議〜。
次に、この状態で一つ目のINSERTの後、非検査例外が起きた場合です。
_dbaccess.insert("city_tbl", new String[] {"004", "test1"}); if(true)throw new RuntimeException("test!!!!"); _dbaccess2.insert("city_tbl", new String[] {"005", "test2"});
こちらの場合は一つ目のSQLは成功しているのにも関わらず、コミットはされていません。
次にこの状態で、spring.datasource.hikari.auto-commit=trueにしてみます。オートコミットがONなら予想では一つ目のSQLはコミットされるはずです。しかし実行してみるとわかりますがいずれのSQLもコミットされません。もしかすると@Transactionalを付加した時点でspring.datasource.hikari.auto-commit=false扱いになるのかもしれません。そこで、メソッドの@Transactionalを外して実行してみます。
実行すると1つ目のSQLは無事?コミットされました、二つ目は実行前に例外が発生しているのでコミットされないのは当たり前ですね。ということでオートコミットとか考える必要もなく、トランザクションを使いたい場合は@Transactionalを使えば良いということがわかりました。便利すぎますね