spring bootの組み込みh2dbにjdbcTemplateでアクセスする

### 1. はじめに

今回はspring-jdbcのjdbcTemplateを利用して、spring bootの組み込みh2dbにDBアクセスする方法について説明したいと思います。といってもh2dbに関する話はほとんどなく、単純にjdbcTemplateの使い方になります。
組み込みh2dbの準備については「[spring bootで組み込みh2dbを利用する方法](https://qiita.com/5zm/items/4916d517b45ca9dc5a4b)」を参照ねがいます。

jdbcTemplateではなく他のORMapperでDBアクセスをする場合は以下の記事を参照ねがいます。

* MyBatisを利用する場合
  * [絶対分かるMyBatis！MyBatisで覚えるべきチェックルール25（前半）](https://qiita.com/5zm/items/e1faac61a1d00998fb1f)
  * [絶対分かるMyBatis！MyBatisで覚えるべきチェックルール25（中編）](https://qiita.com/5zm/items/0864d6641c65f976d415)
* Sql2oを利用する場合
  * [JavaのSql2oで簡単データベースアクセス](https://qiita.com/5zm/items/e1f615e1729b3a9f70ba)

### 2. JdbcTemplateによるDBアクセスのポイント

`JdbcTemplate`によるDBアクセスをする際のポイントについて以下に示します。
基本的なCRUDであれば、ここで示す内容で十分かと思います。

* パラメータ部分を`?`で定義する`JdbcTemplate`と`:パラメータ名`で定義する`NamedParameterJdbcTemplate`がある
  * 可読性が高い`NamedParameterJdbcTemplate`の利用を推奨
* DBアクセスするクラスはリポジトリと呼び、`@Repository`クラスアノテーションを付与する
* `NamedParameterJdbcTemplate`を`@Autowired`等でリポジトリクラスにインジェクションする
* SQL文は文字列型として定数や変数として定義する
  * `@Value`で外出しするのも可能
  * パラメータ部分は`:パラメータ名`で定義する
* パラメータが多い場合は`BeanPropertySqlParameterSource`のコンストラクタを利用して定義するのが便利
* パラメータが少ない場合は`MapSqlParameterSource`のビルダーパターンを利用して定義するのが便利
* `INSERT`、`DELETE`、`UPDATE`のSQLは`jdbcTemplate`の`update`メソッドを利用する
  * 戻り値の`int`は更新結果の行数である
* `SELECT`のSQL文の場合、結果を特定のクラスにマッピングするため`RowMapper`を利用する
* １行を取得する`SELECT`のSQLは`jdbcTemplate`の`queryForObject`メソッドを利用する
* 複数行を取得する`SELECT`のSQLは`jdbcTemplate`の`query`メソッドを利用する

### 3. サンプル

#### 3.1. テーブルの定義

spring bootではクラスパス直下の`schema.sql`ファイルを起動時に自動で実行してくれます。
このファイルに今回利用するテーブルの`CREATE TABLE`文を記述します。
h2dbのデータ型については以下の公式ガイドラインを参照ください。
http://www.h2database.com/html/datatypes.html

```sql:src/main/resources/schema.sql
-- file_info table
CREATE TABLE IF NOT EXISTS file_info (
    file_id varchar(100) PRIMARY KEY,
    file_type varchar(100) NOT NULL,
    file_name varchar(200) NOT NULL,
    file_path varchar(200) NOT NULL,
    content_type varchar(100) NOT NULL,
    content_length bigint NOT NULL,
    registered_date timestamp NOT NULL
);
```

#### 3.2. マッピング対象のクラス

テーブルに対応するマッピング対象のクラスを定義します。
といっても普通のPOJOのクラスです。

```java:FileInfo.java
package com.example.demo.domain.model;

import java.io.Serializable;
import java.sql.Timestamp;

public class FileInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    private String contentType;
    private long contentLength;
    private String fileName;
    private String fileType;
    private String fileId;
    private String filePath;
    private Timestamp registeredDate;

    // constructor,setter,getter omitted
}
```

#### 3.3. リポジトリのクラス

今回の記事のメインとなるリポジトリのクラスを定義します。
ポイントは前述した「[2. JdbcTemplateによるDBアクセスのポイント](#2-jdbctemplateによるdbアクセスのポイント)」の通りです。

**実装上の注意としては検索結果が0件の場合、`org.springframework.dao.EmptyResultDataAccessException`が発生するということです。今回は他のORMapperと同じく、0件の場合は`null`を返すようにしました。**

なお`DataAccessException`を`DemoSystemException`にラップしているのは当方の都合です。jdbcTemplateの使い方とは直接的には関係ありません。

```java:FileInfoRepository.java
package com.example.demo.domain.repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Repository;

import com.example.demo.domain.common.DemoSystemException;
import com.example.demo.domain.model.FileInfo;

@Repository
public class FileInfoRepository {

    private static final Logger LOGGER = LoggerFactory.getLogger(FileInfoRepository.class);

    private static final String INSERT_SQL = "INSERT INTO file_info ("
            + "file_id, file_type, file_name, file_path, content_type, content_length, registered_date) values ("
            + ":fileId, :fileType, :fileName, :filePath, :contentType, :contentLength, :registeredDate)";

    private static final String DELETE_BY_KEY_SQL = "DELETE FROM file_info WHERE file_id = :fileId";

    private static final String UPDATE_BY_KEY_SQL = "UPDATE file_info SET file_type = :fileType, file_name = :fileName, "
            + "file_path = :filePath, content_type = :contentType, content_length = :contentLength, registered_date = :registeredDate "
            + "WHERE file_id = :fileId";

    private static final String FIND_ONE_SQL = "SELECT file_id, file_type, file_name, file_path, content_type, content_length, registered_date "
            + "FROM file_info WHERE file_id = :fileId";

    private static final String FIND_ALL_SQL = "SELECT file_id, file_type, file_name, file_path, content_type, content_length, registered_date "
            + "FROM file_info ORDER BY file_type, registered_date";

    @Autowired
    private NamedParameterJdbcTemplate jdbcTemplate;

    public int insert(FileInfo fileInfo) {
        try {
            SqlParameterSource param = new BeanPropertySqlParameterSource(fileInfo);
            return jdbcTemplate.update(INSERT_SQL, param);
        } catch(DataAccessException e) {
            LOGGER.error("DataAccessError : INSERT_SQL param:{}, error:{}", fileInfo, e);
            throw new DemoSystemException("DataAccessError : FileInfoRepository INSERT_SQL", e);
        }
    }

    public int deleteByKey(String fileId) {
        try {
            SqlParameterSource param = new MapSqlParameterSource().addValue("fileId", fileId);
            return jdbcTemplate.update(DELETE_BY_KEY_SQL, param);
        } catch(DataAccessException e) {
            LOGGER.error("DataAccessError : DELETE_BY_KEY_SQL param:{}, error:{}", fileId, e);
            throw new DemoSystemException("DataAccessError : FileInfoRepository DELETE_BY_KEY_SQL", e);
        }
    }

    public int updateByKey(FileInfo fileInfo) {
        try {
            SqlParameterSource param = new BeanPropertySqlParameterSource(fileInfo);
            return jdbcTemplate.update(UPDATE_BY_KEY_SQL, param);
        } catch(DataAccessException e) {
            LOGGER.error("DataAccessError : UPDATE_BY_KEY_SQL param:{}, error:{}", fileInfo, e);
            throw new DemoSystemException("DataAccessError : FileInfoRepository UPDATE_BY_KEY_SQL", e);
        }
    }

    public FileInfo fineOne(String fileId) {
        try {
            SqlParameterSource param = new MapSqlParameterSource().addValue("fileId", fileId);
            return jdbcTemplate.queryForObject(FIND_ONE_SQL, param, fileInfoRowMapper());
        } catch(EmptyResultDataAccessException e) {
            return null;
        } catch(DataAccessException e) {
            LOGGER.error("DataAccessError : FIND_ONE_SQL param:{}, error:{}", fileId, e);
            throw new DemoSystemException("DataAccessError : FileInfoRepository FIND_ONE_SQL", e);
        }
    }

    public List<FileInfo> findAll() {
        try {
            return jdbcTemplate.query(FIND_ALL_SQL, fileInfoRowMapper());
        } catch(EmptyResultDataAccessException e) {
            return null;
        } catch(DataAccessException e) {
            LOGGER.error("DataAccessError : FIND_ALL_SQL error:{}", e);
            throw new DemoSystemException("DataAccessError : FileInfoRepository FIND_ALL_SQL", e);
        }
    }

    private RowMapper<FileInfo> fileInfoRowMapper() {

        return new RowMapper<FileInfo>() {
            @Override
            public FileInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
                
                FileInfo fileInfo = new FileInfo();
                fileInfo.setContentLength(rs.getLong("content_length"));
                fileInfo.setContentType(rs.getString("content_type"));
                fileInfo.setFileId(rs.getString("file_id"));
                fileInfo.setFileName(rs.getString("file_name"));
                fileInfo.setFilePath(rs.getString("file_path"));
                fileInfo.setFileType(rs.getString("file_type"));
                fileInfo.setRegisteredDate(rs.getTimestamp("registered_date"));
                return fileInfo;
            }
        };
    }
}
```

### 4. さいごに

今回はspring-jdbcのjdbcTemplateを利用して、spring bootの組み込みh2dbにDBアクセスする方法について説明しました。
簡単なDBアクセスであればORMapperを導入しなくてもjdbcTemplateで十分かと思います。