【Java】DBアクセスライブラリを作ってみよう！

# 対象読者
ライブラリ化したいけど、どうすればいいかわからない…。
そんな悩みを持った人のためにDAO (Data Access Object)を例に説明していきます。

※抽象(abstract)クラスやインタフェース(interface)といった
Javaの初歩の説明記事ではありません。
ライブラリを作るための説明となります。

# ライブラリを作成するための極意
それはズバリ、いきなり中身のロジックを考えないことです。
外側から物事を考えることです。
これはオブジェクト指向にも通じることですね～。

具体的には、ライブラリの利用者側の視点で以下を考えてみることだと思います。

* オブジェクト生成のタイミング
* メソッドの引数

# 実際にやってみよう

### ライブラリ作成対象のテーマ
DAO (Data Access Object)をテーマにしたいと思います。

まずは以下のコードを見ていただき、
それから、どういうコンストラクタ―、メソッドであれば使いやすいか考えていきましょう。

```Java:DBアクセスするコード
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

/** ユーザマスタDAO */
public class UserMstDao{
    private Connection connection;
    public UserMstDao(Connection connection){
        this.connection = connection;
    }

    /** ユーザレコード1件取得します */
     * @throws SQLException */
    public UserMst selectUser(String userId) throws SQLException{
        PreparedStatement ps = connection.prepareStatement("select * from UserMst where userId = ?");
        ps.setObject(1, userId);
        ResultSet rs = ps.executeQuery();
        UserMst bean = null;
        if (rs.next()) {
            bean= new UserMst();
            bean.userId = (Integer)rs.getObject("userId");
            bean.userName = (String)rs.getObject("userName");
        }
        return bean;
    }

    /** ユーザレコード全件取得します */
     * @throws SQLException */
    public List<UserMst> selectUserList() throws SQLException{
    	PreparedStatement ps = connection.prepareStatement("select * from UserMst");
    	ResultSet rs = ps.executeQuery();
        List<UserMst> list = new ArrayList<>();
        while (rs.next()) {
            UserMst bean = new UserMst();
            bean.userId = (Integer)rs.getObject("userId");
            bean.userName = (String)rs.getObject("userName");
            list.add(bean);
        }
        return list;
    }
    
    /** ユーザマスタ */
    public static class UserMst{
        //ゲッターセッターは手抜き実装のため、無しですｗ
        public Integer userId;
        public String userName;
    }
}
```

ライブラリ使わないで書くとだいたい誰が書いてもこんな感じのコードになるでしょうね。
ResultSet#getIntを使わないでResultSet#getObjectを使っているのは
getIntの戻り値はプリミティブ型なので、DBの値がnullだと0になるというトラップがあるため、その防止です。
詳しくは以下
[ResultSetのgetXxxxがプリミティブ型なせいでハマった件(Java) - Qiita](https://qiita.com/momosetkn/items/9c3d42a1457cacc0f0ec)

### どんなAPIにするか？
みなさんも一緒に「ライブラリを作成するための極意」で言ったこと踏まえて
以下2点をどうするか考えてみましょう。

* オブジェクト生成のタイミング
* メソッドの引数

おそらく全員が違う回答になると思いますが、ライブラリってそういうもんなんで気にしないで進みましょう。

### ライブラリを実際にイメージ！
ぼくはまずはこういうコードを思い浮かべました。

```java
Jdbc jdbc = new Jdbc(connection);

List<UserMst> userList = jdbc.selectList("select * from UserMst", UserMst.class);

UserMst user = jdbc.selectOne("select * from UserMst where userId = ?", UserMst.class, 0);
```

メソッドにSQLとクラスの情報とSQLのパラメータだけ渡せば結果が返ってくる！
クラスの情報をもとにして、そのクラスのオブジェクトを生成してフィールド名とDB項目名が一致していたら、そのフィールドにセットする！
先ほどのコードと比べるとかなり少ないコードになりましたね！

テンプレートエンジンを駆使して外部SQLファイル化したり、SQL文のなかに変数を記述できたりしますが、
規模がでかくなるので、今回はそこまではやらないでおきましょう。

### それをコーディング！

```java
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * Jdbcを簡単に扱えるようにします.
 */
public class Jdbc {
    private Connection conn;

    public Jdbc(Connection conn) {
        this.conn = conn;
    }

    /**
     * PreparedStatementへパラメーターをセット.
     * @param statement プリぺあーどステートメント
     * @param params パラメータ
     * @throws SQLException
     */
    private void setParams(PreparedStatement statement, Object... params) throws SQLException {
        int paramNo = 1;
        for( Object param : params ) {
            statement.setObject(paramNo++, param);
        }
    }


    /**
     * DBデータから指定されたクラスのオブジェクトへ変換する.
     * clazzにjavaパッケージ配下のクラスを指定した場合は、1項目のみ設定する
     * @param rs リザルトセット
     * @param clazz ビーンクラス
     * @return ビーン
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws SQLException
     */
    private <E> E toObject(ResultSet rs, Class<E> clazz) throws InstantiationException, IllegalAccessException, SQLException {
        E bean = null;
        if( rs.next() ) {
            Field[] fields = clazz.getFields();
            bean = clazz.newInstance();
            for( Field f: fields ) {
                Object val = rs.getObject( f.getName() );
                f.set(bean, val);
            }
        }
        return bean;
    }

    /**
     * DBデータから指定されたクラスのオブジェクトのリストへ変換する.
     * @param rs リザルトセット
     * @param clazz ビーンクラス
     * @return ビーンリスト
     * @throws SQLException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    private <E> List<E> toObjectList(ResultSet rs, Class<E> clazz) throws SQLException, InstantiationException, IllegalAccessException  {
        List<E> rsList = new ArrayList<>();
        while (rs.next()) {
            Field[] fields = clazz.getFields();
            E bean = clazz.newInstance();
            for( Field f: fields ) {
                Object val = rs.getObject( f.getName() );
                f.set(bean, val);
            }
            rsList.add(bean);
        }
        return rsList;
    }

    /**
     * SQLエラー処理
     * @param e 例外
     * @param sql SQL文
     * @return 例外
     */
    private SQLException sqlErr(Throwable e, CharSequence sql) {
        return new SQLException("sql error!\nerror occurred sql="+sql, e);
    }

    /**
     * insertやupdate等を実行します.
     * @param sql
     * @param params
     * @return
     * @throws SQLException
     */
    public int update(String sql, Object...params) throws SQLException {
        try (PreparedStatement statement = conn.prepareStatement(sql.toString())){
            setParams(statement,
                    params);

            return statement.executeUpdate();
        }catch (SQLException e){
            throw sqlErr(e, sql);
        }
    }

    /**
     * 1レコード又は、1カラムのみ取得.
     * @param sql
     * @param clazz
     * @param params
     * @return
     * @throws SQLException
     */
    public <E> E selectOne(String sql, Class<E> clazz, Object...params) throws SQLException {
        ResultSet rs = null;
        try (PreparedStatement statement = conn.prepareStatement(sql.toString())){
            setParams(statement,
                    params);

            rs = statement.executeQuery();

            E val = toObject(rs, clazz);
            return val;
        }catch(SQLException | InstantiationException | IllegalAccessException e) {
            throw sqlErr(e, sql);
        }finally {
            try {
                if( rs != null ) {
                    rs.close();
                }
            } catch (SQLException e) {
                throw sqlErr(e, sql);
            }
        }
    }

    /**
     * 複数レコードを取得します.
     * @param sql
     * @param clazz
     * @param params
     * @return
     * @throws SQLException
     */
    public <E> List<E> selectList(String sql, Class<E> clazz, Object...params) throws SQLException {
        ResultSet rs = null;
        try (PreparedStatement statement = conn.prepareStatement(sql.toString())){
            setParams(statement,
                    params);

            rs = statement.executeQuery();
            List<E> rsList = toObjectList(rs, clazz);
            return rsList;
        }catch(SQLException | InstantiationException | IllegalAccessException e) {
            throw sqlErr(e, sql);
        }finally {
            try {
                if( rs != null ) {
                    rs.close();
                }
            } catch (SQLException e) {
                throw sqlErr(e, sql);
            }
        }
    }

}
```

このコードのポイントは
Classから`java.lang.reflect.Field`の配列を取得して回しているところですね。
リフレクションと言います。
多くのFW/ライブラリで使われています。