第4回 Spring環境におけるDBアクセス(1) 〜 JdbcTemplate篇

【求人のご案内】Javaアプリ開発エンジニア募集

よく訓練されたアップル信者、都元です。では今回は、前回の予告どおり、Spring環境からのDBアクセスについて見て行こうと思います。

DB環境整備(MySQL)

というわけで、今回はDBを使いますので、ローカルにMySQLをインストールしておいてください。筆者の検証環境におけるMySQLのバージョンは5.6系(Server version: 5.6.13-log Source distributionという奴)ですが、まぁまだ基本的なことしかしませんので、最新でなくても良いと思います。また、localhostからはパスワード無しでrootユーザで接続できるような環境を前提としています。適宜そのように調整するか、パスワードが必要な環境を前提とするのであれば、適宜読み替えをおこなってください。

さて、ではMySQLに接続して、とりあえずDBとスキーマを作り、適当なデータを投入しておきましょう。

CREATE DATABASE berserker CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
USE berserker;
CREATE TABLE users (
    username VARCHAR(32) PRIMARY KEY,
    password CHAR(60) NOT NULL
);
 
CREATE TABLE events (
    event_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_name VARCHAR(128) NOT NULL,
    event_owner VARCHAR(32),
    FOREIGN KEY (event_owner) REFERENCES users (username)
);
 
CREATE TABLE participations (
    username VARCHAR(32),
    event_id BIGINT,
    PRIMARY KEY (username, event_id),
    FOREIGN KEY (username) REFERENCES users (username),
    FOREIGN KEY (event_id) REFERENCES events (event_id)
);
 
CREATE TABLE pictures (
    picture_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    location VARCHAR(255) NOT NULL,
    event_id BIGINT NOT NULL,
    FOREIGN KEY (event_id) REFERENCES events (event_id)
);
 
INSERT INTO users
    (username, password)
VALUES
    ('miyamoto', '$2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy'),
    ('yokota',   '$2a$10$nkvNPCb3Y1z/GSearD7s7OBdS9BoLBss3D4enbFQIgNJDvr0Xincm');

ちなみに、DBに生パスワードを保存するという設計は、やってはいけません。上記はエンコード(BCrypt)によって加工したパスワードを記述しています。

依存ライブラリ定義

前回、せっかくGradleを導入しましたので、そのファイルに修正を入れます。まずライブラリのバージョンは、このようにgradle.properties内で変数として参照できるようように定義しておくと、バージョンアップや他の依存ライブラリ追加の際に楽ができます。

springVersion = 4.1.5.RELEASE
mysqlVersion = 5.1.36

Springの追加ライブラリとしては2つです。DBのトランザクション制御を担うspring-tx、JDBCをラップする各種クラスを提供するspring-jdbcです。これの他に、MySQLを使いますので、MySQLのドライバを追加しておきましょう。

dependencies {
  // spring
  compile "org.springframework:spring-context-support:$springVersion"
  compile "org.springframework:spring-tx:$springVersion" // 追加1
  compile "org.springframework:spring-jdbc:$springVersion" // 追加2

  // other
  // ...
  compile "mysql:mysql-connector-java:$mysqlVersion" // 追加3
}

Springの設定とJava側の準備

データベース設定: DataSourceConfiguration

SpringでDBを扱う場合、大抵はjavax.sql.DataSourceの実装を使うのが基本になります。今回は最もシンプルな実装として、DriverManagerDataSourceというのを使います。

DataSourceConfigurationというクラスを作成し、以下のような内容を記述します。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class DataSourceConfiguration {
  
  @Bean
  public DriverManagerDataSource dataSource() {
    DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
    driverManagerDataSource.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
    driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/berserker");
    driverManagerDataSource.setUsername("root");
    driverManagerDataSource.setPassword("");
    return driverManagerDataSource;
  }
  
  @Bean
  public DataSourceTransactionManager transactionManager() {
    return new DataSourceTransactionManager(dataSource());
  }
  
  @Bean
  public JdbcTemplate jdbcTemplate() {
    return new JdbcTemplate(dataSource());
  }
}

DriverManagerDataSourceに対してドライバやURL等、接続に必要な情報を与えます。続いて、トランザクションの制御を行うtransactionManagerを定義 *1し、アノテーションによる「宣言的トランザクション制御」を有効にします。最後に、jdbcTemplateというbeanを定義していますが、クライアントのプログラムからはこのbeanを利用します。

今回のプログラム側の基本骨格

以上を作成の上、メインのクラスを書いていきましょう。今回はDBへのアクセスを確認するだけの目的ですので、単純なmainメソッドを起点とするコンソールプログラムです。以前にもご紹介した手法ですが、DataAccessSampleをbeanとして扱い、このインスタンスにDIが可能な状況を作り出しています。

@Component
@ComponentScan
public class DataAccessSample {
  
  public static void main(String[] args) {
    try (ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(DataAccessSample.class)) {
      DataAccessSample das = context.getBean(DataAccessSample.class);
      das.execute();
    }
  }
  
  
  @Autowired
  JdbcTemplate jdbcTemplate;
  
  
  @Transactional
  public void execute() {
    // do some work
  }
}

ポイントは2つ。1つ目はDataDourceConfigurationで定義したjdbcTemplateをDIしていること。もう1つはexecuteメソッドに@Transactionalというアノテーションを付けていることです。

@Transactioanlアノテーションは、トランザクション制御を宣言的に行う手法です。詳しくは次回以降にお話しますが、executeの実行直前にトランザクションを開始(begin)させ、実行直後にコミットします。executeが正常に終了せず、例外によって終了した場合はロールバックします。つまり、executeメソッド内のDB処理がアトミックになるわけです。

ということを、「手続きの記述」 *2ではなく、メソッドにアノテーションを付与するという「宣言の記述」で表現しようとする試み *3です。

あとは、usersテーブルに対応するJava beansを作っておきましょう。

@ToString
public class User {
  @Getter @Setter
  private String username;
  @Getter @Setter
  private String password;
}

いよいよDBアクセス

あとはexecuteの中からjdbcTemplateのメソッドを呼び出すだけです。まずは簡単なところから。usersテーブルの全件数を取ってみましょう。第1引数がクエリで、第2引数は戻り値の型となります。結果的に2が返るはずです。

Long allUsersCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", Long.class);
System.out.println("allUsersCount = " + allUsersCount);

つづいて、miyamotoユーザのパスワードを取得。第2引数には、クエリ内に埋め込んだプレースホルダ?に割り当てるパラメータを配列で渡せます。結果は$2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy

String password = jdbcTemplate.queryForObject("SELECT password FROM users WHERE username = ?", new Object[] {
  "miyamoto"
}, String.class);
System.out.println("password = " + password);

ここまでは1カラムから構成される単純な値(いわゆるスカラー値)を取得するだけでした。複数のカラムがある場合は、RowMapperというのを使います。

User user = jdbcTemplate.queryForObject("SELECT * FROM users WHERE username = ?", new Object[] {
  "miyamoto"
}, new RowMapper<User>() {
  
  @Override
  public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    User user = new User();
    user.setUsername(rs.getString("username"));
    user.setPassword(rs.getString("password"));
    return user;
  }
});
System.out.println("user = " + user);

これで、ResultSetから取得できる各カラムの値をUserクラスのフィールドにマッピングできました。

サンプルプロジェクト berserker v4.0

2015-07-13追記:このコードを、GitHubに上げておきました。ご興味のある方は、下記のように実行してみてください。初回実行時には色々表示も異なり、実行開始まで時間が掛かります。execute1タスクが、Mainクラスの実行、execute2タスクがSpringMainクラスの実行です。ログ出力は少々異なりますが、どちらもfooという出力がされていますね。

$ git clone https://github.com/classmethod-sandbox/berserker.git
$ cd berserker
$ git checkout 4.0
$ ./gradlew execute
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:execute
2016/04/08 18:43:46.773 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext:578 - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@511baa65: startup date [Fri Apr 08 18:43:46 JST 2016]; root of context hierarchy
2016/04/08 18:43:47.253 [main] INFO  o.s.j.d.DriverManagerDataSource:133 - Loaded JDBC driver: com.mysql.jdbc.Driver
2016/04/08 18:43:47.697 [main] INFO  j.c.e.berserker.DataAccessSample:56 - allUsersCount = 2
2016/04/08 18:43:47.719 [main] INFO  j.c.e.berserker.DataAccessSample:61 - password = $2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy
2016/04/08 18:43:47.723 [main] INFO  j.c.e.berserker.DataAccessSample:75 - user = User(username=miyamoto, password=$2a$10$cPnF0sq.bCPHeGuzVagOgOmbe2spT1Uh1k9LyuS0jzb5F3Lm.9kEy)
2016/04/08 18:43:47.728 [main] INFO  o.s.c.a.AnnotationConfigApplicationContext:960 - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@511baa65: startup date [Fri Apr 08 18:43:46 JST 2016]; root of context hierarchy

BUILD SUCCESSFUL

Total time: 2.674 secs

まとめ

と、いうのがSpringが標準で用意しているデータアクセスの基本です。もう少し使いやすい工夫がされたMappingSqlQuery等の仕組みもありますが、もう少しコードの記述がしやすくなるだけで、基本的な考え方は変わりません。ここまで見て、少々原始的(低級)な印象が強いのではないでしょうか。生のJDBCを操作するよりだいぶましですが、Javaコードの中にSQLを記述すること自体、あまりモダンな感じがしません。

しかし、これ以上の高級なAPIを利用したい場合は、Spring単独ではなく、外部のライブラリと連携する必要があります。代表的な選択肢としては、HibernateJDO、JPA、iBatis等があります。Springはいずれの選択肢についても、インテグレーションのサポートを提供しています。

さらに、Spring DataというSpringのサブプロジェクトを利用するという選択肢もあります。筆者は個人的にこちらの方が好みなので、次回は Spring Data についてのお話をしようと思います。

という訳で、次回は「第5回 Spring環境におけるDBアクセス(2) 〜 Spring Data篇」です。実は、データアクセスの仕組みはこっちが本命。正直JdbcTemplateとか、私はあんま使わないですよwww(ごめんなさいごめんなさいwww *4

脚注

  1. トランザクションについては、次回以降(予定では第6回)に詳しく扱おうと思っています。
  2. よくあるtryを使った制御。
  3. 本連載の初回でも、JavaとXMLを対比して、手続き型と宣言型について考えてみたことを思い出してください。
  4. その上で、なぜこれを説明したのかと言いますと、Springが裏側で何をやっているか想像しやすいからです。この方式は黒魔術的要素が少なく、型変換等を自分で記述できるので、いざという時に対処しやすいという特徴があります。