LoginSignup
2
2

More than 5 years have passed since last update.

SQLを食べてBeanを返すJDBC接続マッパー

Last updated at Posted at 2019-03-22

はじめに

まだまだレガシーなJDBC接続を実現せざるを得ないJavaソースは世に数多く存在し
プログラマの悩みの種の一つであったりします。
保守だけならまだしも、新規に作らなくてはいけないことも。

レガシーという意味ですぐに思いつくケースとしては
「DBからSelectで引いた情報をBeanとしてマッピングする」という形ですが
その処理プロセスのまだるっこしさたるや…

「クエリの結果をResultSetで一回預かる」
「ResultSetのカーソルを回してBeanに詰める」
「Beanをループさせてビジネスロジックを実装」
みたいな感じです。

SQLとBeanの設計・定義は正しいのに、そのまだるっこしい過程の中で
ケアレスミスをしたばっかりに全体が動かなくなってしまう、という顛末も往々にしてあります。

もっと直感的に、SQLとBeanだけで完結しないの?という事で本稿です。

実現したいこと

掲題通りです。
厳密には「返ってくるBeanの型」はあらかじめ指定する必要があるので
スタバのマイボトルに近いでしょうか。
注文(SQL)して、マイボトル(Bean)を店員に渡すと
マイボトルがコーヒーで満たされて返ってくる、的な。

実現に当たって捨てたもの

型安全性

この後出てくるコードをご覧になって頂ければわかりますが
Javaの大きな特徴の一つである「型安全性」を完全に無視している部分があります。
個人的には「別に」ですが、引っかかる人は引っかかるかもしれません。
何卒、ご容赦ください。

既存ORM技術の深堀り

ORMは全然詳しくありません。
なので、既存のミドルウェアやライブラリでもっと優れた機能性を以て
私がやりたい事を実現しているリソースもありそうではありますが
ORM自体、賛否両論どころか否定的な意見が多い、という見切りもあり
早計も早計ではありますが、無理に首を突っ込むのはやめておきました。
(個人的に、ORMは広義な言葉と認識していますので、
 全部が全部ダメという事ではないとは思うのですが
 言葉狩りのきらいがあるのも事実ですのであえて避けました)

参考にさせて頂いた記事

やりたい事に一番近い、と感じたのが以下の記事です。
基盤部分のソースはほぼそのまま転用させて頂いております。
【Java】DBアクセスライブラリを作ってみよう!

Javaのバージョン

Java6以降
※Java5でも動かない事はないと思いますが、未検証です。

登場人物

JDBCManagerクラス

DB接続基盤クラス兼マッピングを司るクラスです。

SqlParameterクラス

SQLへのパラメータとなるプレースホルダを管理するクラスです。
型安全ならぬ型危険なのがこいつです。

SQL(クエリ文字列)

実行したいSQL文です。
CRUDを問わず、特にファイルやDBレコードとして永続化する必要はありません。

Selectに応じたBeanクラス

Selectの抽出カラムに対応したBeanクラスです。
1つのSelect文に対して1つ作成します。

I/O

最低限の動作に必要なI/Oは以下の通りです。

インプット

・JDBCドライバクラスのフルパス(oracle.jdbc.driver.OracleDriverなど)
・JDBC接続子(jdbc:oracle:thin:@//localhost:1521/xeなど)
・JDBC接続ユーザ
・JDBC接続パスワード
・SQL
・SQLのパラメータ(ただしプレースホルダ未使用の場合は不要)

アウトプット

Bean
※ただしInsert、Update、Deleteの場合は更新件数のみをintで返却。

基盤ソース

JDBCManagerクラスとSqlParameterクラスが基盤となります。

JDBCManagerクラス

JDBCManagerクラス
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class JDBCManager {
    private Connection conn;

    @SuppressWarnings("unused")
    private JDBCManager() throws Throwable{}

    public JDBCManager(String driver,String url,String user,String password) throws ClassNotFoundException,SQLException{
        Class.forName(driver);
        this.conn = DriverManager.getConnection(url, user, password);
        this.conn.setAutoCommit(false);
    }

    private void setParams(PreparedStatement statement, SqlParameter sqlParameter) throws SQLException {
        if(sqlParameter == null || !sqlParameter.hasParameters()) {
            return;
        }
        int paramNo = 1;
        for( Object param : sqlParameter.toParameter() ) {
            statement.setObject(paramNo++, param);
        }
    }

    public void commit() throws SQLException{
        this.conn.commit();
    }

    public void rollback() throws SQLException{
        this.conn.rollback();
    }

    public void close() throws SQLException{
        this.conn.close();
    }

    private <E> E toObject(ResultSet rs, Class<E> clazz) throws InstantiationException, IllegalAccessException, SQLException {
        E bean = null;
        if( rs.next() ) {
            Field[] fields = clazz.getDeclaredFields();
            bean = clazz.newInstance();
            for (Field f : fields) {
                f.setAccessible(true);
                Object val = rs.getObject(f.getName());
                f.set(bean, val);
            }
        }
        return bean;
    }

    private <E> List<E> toObjectList(ResultSet rs, Class<E> clazz) throws SQLException, InstantiationException, IllegalAccessException  {
        List<E> rsList = new ArrayList<E>();
        while (rs.next()) {
            Field[] fields = clazz.getDeclaredFields();
            E bean = clazz.newInstance();
            for( Field f: fields ) {
                f.setAccessible(true);
                Object val = rs.getObject( f.getName() );
                f.set(bean, val);
            }
            rsList.add(bean);
        }
        return rsList;
    }

    private SQLException sqlErr(Throwable e, CharSequence sql) {
        return new SQLException("sql error!\nerror occurred sql="+sql, e);
    }


    public int insert(String sql, SqlParameter sqlParameter) throws SQLException {
        return exert(sql,sqlParameter);
    }

    public int update(String sql, SqlParameter sqlParameter) throws SQLException {
        return exert(sql,sqlParameter);
    }

    public int delete(String sql, SqlParameter sqlParameter) throws SQLException {
        return exert(sql,sqlParameter);
    }

    private int exert(String sql, SqlParameter sqlParameter) throws SQLException {
        PreparedStatement statement = null;
        try {
            statement = conn.prepareStatement(sql.toString());
            setParams(statement,sqlParameter);
            int ret = statement.executeUpdate();
            return ret;
        }catch (SQLException e){
            throw sqlErr(e, sql);
        }finally {
            try {
                if(statement != null){
                    statement.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public <E> E selectOne(String sql, Class<E> clazz) throws SQLException {
        return selectOne(sql,clazz,null);
    }

    public <E> E selectOne(String sql, Class<E> clazz, SqlParameter sqlParameter) throws SQLException {
        ResultSet rs = null;
        PreparedStatement statement = null;
        try{
            statement = conn.prepareStatement(sql.toString());
            setParams(statement,sqlParameter);
            rs = statement.executeQuery();

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

    public <E> List<E> selectList(String sql, Class<E> clazz) throws SQLException {
        return selectList(sql,clazz,null);
    }

    public <E> List<E> selectList(String sql, Class<E> clazz, SqlParameter sqlParameter) throws SQLException {
        ResultSet rs = null;
        PreparedStatement statement = null;
        try {
            statement = conn.prepareStatement(sql.toString());
            setParams(statement,sqlParameter);
            rs = statement.executeQuery();
            List<E> rsList = toObjectList(rs, clazz);
            return rsList;
        }catch(Exception e) {
            throw sqlErr(e, sql);
        }finally {
            try {
                if( rs != null ) {
                    rs.close();
                }
            }catch(SQLException e) {
                e.printStackTrace();
            }finally {
                try {
                    if(statement != null){
                        statement.close();
                    }
                }catch(SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

SqlParameterクラス

SqlParameterクラス
import java.util.ArrayList;
import java.util.List;

public class SqlParameter {
    @SuppressWarnings("rawtypes")
    private List list = null;

    @SuppressWarnings("rawtypes")
    public SqlParameter(int paramNumber){
        list = new ArrayList(paramNumber);
    }

    @SuppressWarnings("rawtypes")
    public SqlParameter(){
        list = new ArrayList();
    }

    @SuppressWarnings("unchecked")
    public <E> void addParameter(E e){
        list.add(e);
    }

    public boolean hasParameters() {
        return list.size()==0 ? false : true;
    }

    public Object[] toParameter(){
        return list.toArray();
    }
}

動作サンプル

4つのケースで動かしていきます。
対象のDBは「Oracle(ExpressEdition)」とします。
スキーマにデフォルトで存在する「EMP」表と「DEPT」表を操作対象としています。

①Select:WHERE句なし(プレースホルダ不要)の複数件取得SQL
②Select:WHERE句あり(プレースホルダあり)の複数件取得SQL
③Select:単一取得SQL
④更新(insert,update,delete)

それぞれSQLに応じたBeanを作成していきます。
ポイントはSelectのエイリアス(AS)とBeanのフィールド名を同じにする事です。

また、Beanのフィールド型はJDBC仕様に応じたクラスで宣言しておきます。

本サンプルのSelectでは、単一のテーブルに対しての操作をしておりますが
結合を使用した複数テーブルを対象としたSelectでも同様の処理が可能です。

要は「SQLとBeanが合っていればいい」のです。

① Select:WHERE句なし(プレースホルダ不要)の複数件取得SQL

Select_WHERE句なし(プレースホルダ不要)の複数件取得SQL
SELECT 
    --VARCHAR2
    DNAME AS dname 
FROM 
    DEPT
DeptBeanクラス
public class DeptBean {
    private String dname = null;

    public String getDname() {
        return dname;
    }

    public void setDname(String dname) {
        this.dname = dname;
    }
}

② Select:WHERE句あり(プレースホルダあり)の複数件取得SQL

Select_WHERE句あり(プレースホルダあり)の複数件取得SQL
SELECT 
     --NUMBER
     EMPNO AS empNo, 
     --VARCHAR2
     ENAME AS ename, 
     --DATE
     HIREDATE AS hireDate 
 FROM 
     EMP 
 WHERE 
     DEPTNO = ? 
     AND 
     JOB = ? 
EmpBeanクラス
import java.math.BigDecimal;
import java.sql.Timestamp;

public class EmpBean {
    private BigDecimal empNo = null;
    private String ename = null;
    private Timestamp hireDate = null;

    public BigDecimal getEmpNo() {
        return empNo;
    }
    public void setEmpNo(BigDecimal empNo) {
        this.empNo = empNo;
    }
    public String getEname() {
        return ename;
    }
    public void setEname(String ename) {
        this.ename = ename;
    }
    public Timestamp getHireDate() {
        return hireDate;
    }
    public void setHireDate(Timestamp hireDate) {
        this.hireDate = hireDate;
    }
}

③ Select:単一取得SQL

Select_単一取得SQL
SELECT 
     --NUMBER
     EMPNO AS empNo, 
     --VARCHAR2
     ENAME AS ename, 
     --DATE
     HIREDATE AS hireDate 
 FROM 
     EMP 
 WHERE 
    //PK
     EMPNO = ?
EmpBeanクラス
上と同じなので割愛

④ 更新(insert,update,delete)

insertをサンプルとします。
結果はintで受け取る為、更新用のBeanは作らなくて大丈夫です。

insert
INSERT INTO DEPT 
     --NUMBER,VARCHAR2,VARCHAR2
     (DEPTNO, DNAME, LOC) 
     values 
     (?, ?, ?) 

テストクラス

上記4つのケースを全て動かしていきます。

テストクラス
import java.math.BigDecimal;
import java.util.List;

public class JDBCTest {
    public static void main(String[] args) throws Throwable{
        JDBCTest t = new JDBCTest();
        t.execute();
    }

    public void execute() throws Throwable{
        //JDBC接続情報
        String driver = "oracle.jdbc.driver.OracleDriver";
        String url = "jdbc:oracle:thin:@//localhost:1521/xe";
        String user = "ouser";
        String password = "ouser";
        //JDBCManagerを初期化
        JDBCManager jdbc = new JDBCManager(driver,url,user,password);

        //パターン1.WHERE句なし(プレースホルダ不要)の複数件取得SQL
        String sql = "SELECT "
                        + "DNAME AS dname "
                    + "FROM "
                        + "DEPT ";
        //Listを取得
        List<DeptBean> dlist = jdbc.selectList(sql, DeptBean.class);

        System.out.println("―――パターン1―――");
        for(DeptBean d : dlist) {
            System.out.println(d.getDname());
        }

        //パターン2.WHERE句あり(プレースホルダあり)の複数件取得SQL
        //別名でBeanのフィールドに合わせる
        sql =       "SELECT "
                        + "EMPNO AS empNo, "
                        + "ENAME AS ename, "
                        + "HIREDATE AS hireDate "
                    + "FROM "
                        + "EMP "
                    + "WHERE "
                        + "DEPTNO = ? "
                        + "AND "
                        + "JOB = ? ";

        //プレースホルダは2つ
        SqlParameter p = new SqlParameter(2);
        //より先頭から順番にバインド
        p.addParameter(new BigDecimal(30));
        p.addParameter("SALESMAN");

        //Listを取得
        List<EmpBean> elist = jdbc.selectList(sql, EmpBean.class, p);

        System.out.println("―――パターン2―――");
        for(EmpBean e : elist) {
            System.out.println(e.getEmpNo() + "\t" + e.getEname() + "\t" + e.getHireDate());
        }

        //パターン3.単一取得SQL
        sql =       "SELECT "
                        + "EMPNO AS empNo, "
                        + "ENAME AS ename, "
                        + "HIREDATE AS hireDate "
                    + "FROM "
                        + "EMP "
                    + "WHERE "
                        //PK
                        + "EMPNO = ? ";
        p = new SqlParameter(1);
        p.addParameter(new BigDecimal(7369));

        //Beanを取得
        EmpBean emp = jdbc.selectOne(sql, EmpBean.class, p);
        System.out.println("―――パターン3―――");
        System.out.println(emp.getEname());

        //パターン4.更新(insert,update,delete)
        sql =       "INSERT INTO DEPT "
                        + "(DEPTNO, DNAME, LOC) "
                        + "values "
                        + "(?, ?, ?) ";

        p = new SqlParameter(3);
        p.addParameter(new BigDecimal(99));
        p.addParameter("SPECIAL");
        p.addParameter("JAPAN");

        int i = jdbc.insert(sql, p);
        System.out.println("―――パターン4―――");
        System.out.println("Insert件数は"+i+"件です");
        jdbc.commit();
        //実際はエラートラップして、catchでjdbc.rollback()を実行

        jdbc.close();
        //実際はエラートラップして、finallyでjdbc.close()を実行
    }
}

実行結果は以下の通りです。

実行結果(標準出力)
―――パターン1―――
ACCOUNTING
RESEARCH
SALES
OPERATIONS
―――パターン2―――
7499    ALLEN   1981-02-20 00:00:00.0
7521    WARD    1981-02-22 00:00:00.0
7654    MARTIN  1981-09-28 00:00:00.0
7844    TURNER  1981-09-08 00:00:00.0
―――パターン3―――
SMITH
―――パターン4―――
Insert件数は1件です

まとめ

参考にさせて頂いた記事から比較しても
目新しい内容はほぼ皆無ですし、
型安全でないばかりに実行時エラーを引き起こすリスクは上がっているのですが
個人的には、扱う対象がSQLパラメータBeanの多くても3つなので
Resultsetとかでごちゃごちゃやって時間を食うくらいであれば
型安全をトレードオフにする価値はあるだろうと思っている次第です。

また、諸事情でJavaをバージョンアップできない環境であったり、
ORMを始めとしたDB基盤系のミドルウェアやライブラリを導入できない環境であったり、
と環境面・運用面での制限があったとしても、
古いJavaVMのみで動作可能という所がお気に入りです。

今回はデータベースとしてOracleをターゲットとしましたが
JDBCで接続可能であれば、多少の方言的な挙動差異はあれど
どのデータベースでもいけるんではないかなぁと楽観的に思ってたりします。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2