Javaのapache.commons.dbutilsパッケージのO/Rマッパーラッピング


## はじめに

apache.commons.dbutilsパッケージは、Javaでデータベースを扱うときに軽量のO/Rマッパーに近いインタフェースを提供するパッケージです。簡単な使い方は以下のページから見ることができます。

* http://d.hatena.ne.jp/Kishi/20081212/1229076102

普通にJavaのO/Rマッパーを使おうと思うと、かなり容量の大きいJarファイルをインポートする必要があり、かつxmlの設定などが必要だったりといろいろと面倒になります。このdbutilsは比較的薄いラッパーを提供するパッケージなので、容量が少なく、手軽に使えるのが特徴です。

ただ、sql文をそのまま使うためにO/Rマッパーというには少しもの足りない部分があります。今回は、その使い方をもう少しO/Rマッパーに近づけるため、ラッピングクラスを作ったので、自分のリマインダを兼ねてソースをさらしておきます。

注意：Processing用のコードとして使ったので、eclipseで使おうと思うと色々とWarningが出ると思いますが、適当に解決してください。

## インストール／セットアップ

今回のコードサンプルを使うためには、以下のパッケージが必要です。

* sqlite-jdbc-3.7.2.jar
* commons-dbutils-1.5.jar

上の２つは、apache.commons.dbutilsパッケージを使うために必要です。それぞれ以下のサイトからダウンロードすることができます。

* https://bitbucket.org/xerial/sqlite-jdbc/downloads
* http://commons.apache.org/proper/commons-dbutils/download_dbutils.cgi

それ以外にも以下のパッケージを使用しています。

* logback-classic-0.9.30.jar
* logback-core-0.9.30.jar
* slf4j-api-1.6.3.jar 

上記のの3つはログ用に使用しています。以下のサイトから取得できますが、面倒なら、コードの中のlogger.error()という関数をSystem.err.println()に置き換えれば使う必要はありません。

* http://logback.qos.ch/download.html
* http://www.slf4j.org/download.html

※なお、バージョン番号は最新のものになっているはずでずれている可能性があります。

## ラッパークラスの実装

今回実装したのは以下の機能です。

* staticなcreateTableメソッド
* 各種insert, select, update, deleteメソッド

createTableメソッドは、なぜ普通に実装されていないのか謎ですが、クラス定義を与えるとそのpublicなフィールドによって構成されるテーブルを作成するメソッドです。テーブル名は、クラス名になります。insert,select,update,deleteはメジャーな機能をそれぞれsql文なしで実現するという試みです。

```DBConnection.java
import java.sql.Connection;
import java.sql.Statement;
import java.sql.DriverManager;
import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;

import java.lang.Class;
import java.lang.reflect.Field;

public class DBConnection {
	protected static Logger logger = LoggerFactory.getLogger(DBConnection.class);

	public static DBConnection connection = null;

	public static boolean init (String dbpath) {
		try {
			if (DBConnection.connection == null) {
				DBConnection.connection = new DBConnection (dbpath);
			}
			return true;
		} catch (Exception e) {
			return false;
		}
	}

	protected static Map<String, String> fieldTypeMap = new HashMap <String, String> () {{
		put ("int", "INT");
		put ("long", "INT");
		put ("double", "DOUBLE");
		put ("float", "FLOAT");
		put ("boolean", "BOOLEAN");	
	
		put ("java.lang.Integer", "INT");
		put ("java.lang.Long", "INT");
		put ("java.lang.Double", "DOUBLE");
		put ("java.lang.Float", "FLOAT");		
		put ("java.lang.Boolean", "BOOLEAN");

		put ("java.lang.String", "varchar(255)");
	}};

	protected static String dropTableStatement = "drop table %s;";
	protected static String createTableStatement = "create TABLE %s (%s);";

	public static boolean createTable(Class<?> clazz, boolean forceDrop) {
		if (forceDrop) {
			try {
			    Statement statement = connection.con.createStatement();
				statement.setQueryTimeout(30);
				statement.executeUpdate(String.format(dropTableStatement, clazz.getName().toLowerCase()));
				statement.close();
			} catch (Exception e) {
				logger.error("Error: " + e.getMessage());
			}
		}
		return createTable(clazz);
	}

	public static boolean createTable (Class<?> clazz) {
		try {
			Field[] fields = clazz.getFields();
			// StringBuffer buf = new StringBuffer();
			List<String> declarations = new ArrayList<String>();
			for (Field f : fields) {
				String fieldType = fieldTypeMap.get(f.getType().getName());
				if (fieldType == null) continue;
				declarations.add(f.getName() + " " + fieldType);
			}
			String fieldDeclaration = concatWithCommas(declarations);
			//logger.debug(fieldDeclaration);
			String query = String.format(
				createTableStatement, 
				clazz.getName().toLowerCase(), 
				fieldDeclaration
				);
			//logger.debug("query: {}", query);
			DBConnection connection = getInstance();

		    Statement statement = connection.con.createStatement();
    		statement.setQueryTimeout(30);
    		statement.executeUpdate(query);

    		statement.close();

			return true;
		} catch (Exception e) {
			logger.error(e.getMessage());
			return false;
		}
	}

	public static DBConnection getInstance () {
		return DBConnection.connection;
	}

    public Connection con = null;
    public String dbpath = null;
	public QueryRunner qr = null;
	protected Map<Class<?>, Map<String, String>> queryStatementMap = null;


	public DBConnection (String dbpath) throws Exception {
		this.dbpath = dbpath;
        this.qr = new QueryRunner();
        this.queryStatementMap = new HashMap<Class<?>, Map<String, String>>();
		if (!this.connect()) {
			throw new Exception("failed to connect db.");
		}
	}

	public boolean registerTable (Class<?> clazz) {
		try {
			Field[] fields = clazz.getFields();
			List<String> fieldNames = new ArrayList<String>();
			List<String> insertions = new ArrayList<String>();
			List<String> fieldEquations = new ArrayList<String>();

			for (Field f: fields) {
				fieldNames.add(f.getName());
				insertions.add("?");
				fieldEquations.add(String.format("%s = ?", f.getName()));
			}

			String insertStatement = String.format(
				"insert into %s(%s) values(%s)",
				clazz.getName().toLowerCase(),
				concatWithCommas(fieldNames),
				concatWithCommas(insertions)
				);

			String selectStatement = String.format(
				"select * from %s",
				clazz.getName().toLowerCase()
				);

			String updateStatement = String.format(
				"update %s set %s",
				clazz.getName().toLowerCase(),
				concatWithCommas(fieldEquations)
				);

			String deleteStatement = String.format(
				"delete from %s",
				clazz.getName().toLowerCase()
				);

			Map<String, String> queryStatements = new HashMap<String, String> ();
			queryStatements.put ("insert", insertStatement);
			queryStatements.put ("select", selectStatement);
			queryStatements.put ("update", updateStatement);
			queryStatements.put ("delete", deleteStatement);
			this.queryStatementMap.put (clazz, queryStatements);
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	public synchronized boolean connect () {
		try {
	        Class.forName("org.sqlite.JDBC");
	     	this.close();
	        con = DriverManager.getConnection(this.dbpath);
	        return true;
	    } catch (ClassNotFoundException e) {
	    	logger.error("class not found: " + e.getMessage());
	    	return false;
	    } catch (SQLException e) {
	    	logger.error("SQL Exception: " + e.getMessage());
	    	return false;
	    } catch (Exception e) {
	    	logger.error("Exception: " + e.getMessage());
	    	return false;
	    } 
	}

	public synchronized void close () {
		try {
	        if (this.con != null) {
	        	this.con.close();
	        	this.con = null;
			}
		} catch (SQLException e) {
	    	logger.error("SQL Exception: " + e.getMessage());
			return;
		} catch (Exception e) {
	    	logger.error("Exception: " + e.getMessage());			
			return;
		}
	}

	public boolean insert (Object obj) {
		try {
			Class<?> clazz = obj.getClass();
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			String insertStatement = queryStatements.get("insert");
			List<Object> fieldValues = new ArrayList<Object>();
			for (Field f: clazz.getFields()) fieldValues.add(f.get(obj));
			this.qr.update(this.con, insertStatement, fieldValues.toArray());
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	public List select (Class<?> clazz, String whereStatement, Object param) {
		try {
			ResultSetHandler h = new BeanListHandler(clazz);
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			String selectStatement = queryStatements.get("select") + " " + whereStatement;
			return (List)this.qr.query(this.con, selectStatement, param, h);
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return null;
		}
	}

	public List select (Class<?> clazz, String whereStatement, Object[] params) {
		try {
			ResultSetHandler h = new BeanListHandler(clazz);
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			String selectStatement = queryStatements.get("select") + " " + whereStatement;
			return (List)this.qr.query(this.con, selectStatement, params, h);
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return null;
		}
	}

	public List all (Class<?> clazz) {
		try {
			ResultSetHandler h = new BeanListHandler(clazz);
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			String selectStatement = queryStatements.get("select");
			return (List)this.qr.query(this.con, selectStatement, h);
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return null;
		}
	}

	public boolean update (Object obj, String whereStatement, Object param) {
		try {
			Class<?> clazz = obj.getClass();
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			List<Object> params = new ArrayList<Object>();
			for (Field f: clazz.getFields()) params.add(f.get(obj));
			params.add(param);
			String updateStatement = queryStatements.get("update") + " " + whereStatement;
			this.qr.update(this.con, updateStatement, params.toArray());
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	public boolean update (Object obj, String whereStatement, Object[] params) {
		try {
			Class<?> clazz = obj.getClass();
			Map<String, String> queryStatements = this.queryStatementMap.get(clazz);
			List<Object> params_ = new ArrayList<Object>();
			for (Field f: clazz.getFields()) params_.add(f.get(obj));
			params_.addAll(Arrays.asList(params));
			String updateStatement = queryStatements.get("update") + " " + whereStatement;
			this.qr.update(this.con, updateStatement, params_.toArray());
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	public boolean delete (Class<?> clazz, String whereStatement, Object param) {
		try {
			Map<String,String> queryStatements = this.queryStatementMap.get(clazz);
			String deleteStatement = queryStatements.get("delete") + " " + whereStatement;
			this.qr.update(this.con, deleteStatement, param);
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	public boolean delete (Class<?> clazz, String whereStatement, Object[] params) {
		try {
			Map<String,String> queryStatements = this.queryStatementMap.get(clazz);
			String deleteStatement = queryStatements.get("delete") + " " + whereStatement;
			this.qr.update(this.con, deleteStatement, params);
			return true;
		} catch (Exception e) {
			logger.error("Error: {}", e.getMessage());
			return false;
		}
	}

	protected static String concatWithCommas(List<String> words) {
 	   StringBuffer wordList = new StringBuffer();
    	for (String word : words) wordList.append(word + ",");
    	return new String(wordList.deleteCharAt(wordList.length() - 1));
	}
}
```

このクラスはProcessingで使うために作ったので、以下にProcessingのサンプルコードを添付しておきます。もちろん、pure Javaでも使えると思いますが、パッケージ名の指定などが必要だと思われます。

```DBUtil.pde
import java.util.List;

DBConnection connection = null;

public void setup () {
	String dbpath = "jdbc:sqlite://" + sketchPath + "/data.db";
	print ("dbpath: " + dbpath);
	connection.init(dbpath);

	connection = DBConnection.getInstance();
	try {
		test_run();
		} catch (Exception e) {
			System.out.println("test failed: " + e.getMessage());
		}
}

public void draw () {
}

public void test_run () throws Exception {
	DBConnection.createTable(User.class, true);
	DBConnection db = DBConnection.getInstance();
	db.registerTable(User.class);

	//挿入
	println ("#############################################################");
	println ("insertion");
	println ("#############################################################");

	println ("");
	println ("inserting (1, Kishi) ...");
	User user = new User();
	user.setId(1);
	user.setName("Kishi");
	db.insert(user);

	println ("");
	println ("inserting (2, Kawa) ...");
	User user1 = new User();
	user1.setId(2);
	user1.setName("Kawa");
	db.insert(user1);

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : (List<User>)db.all(User.class)) println (u.getId() + ":" + u.getName());

	println ("");
	println ("#############################################################");
	println ("selection");
	println ("#############################################################");
	
	//検索
	println("selecting");
	println ("select with name = Kishi ...");
	List<User> users = db.select(User.class, "where name=?", "Kishi");

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : users) println (u.getId() + ":" + u.getName());

	println ("select with name = Kishi and id = 1 ...");
	users = db.select(User.class, "where name=? and id=?", new Object [] {"Kishi", 1});

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : users) println (u.getId() + ":" + u.getName());


	println ("select all ...");
	users = db.all(User.class);
	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : users) println (u.getId() + ":" + u.getName());

	println ("");
	println ("#############################################################");
	println ("update");
	println ("#############################################################");

	user = users.get(0);

	println("updating the row with id = 1");
	User user_ = new User();
	user_.setName("Shiki");
	user_.setId(2);
	db.update(user_, "where id = ?", user.getId());

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : (List<User>)db.all(User.class)) System.out.println(u.getId() + ":" + u.getName());

	println("updating the row with id = 2 and name = Shiki");
	User user__ = new User();
	user__.setName("Hoge");
	user__.setId(10);
	db.update(user__, "where id = ? and name = ?", new Object [] {user_.getId(), user_.getName()});

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : (List<User>)db.all(User.class)) System.out.println(u.getId() + ":" + u.getName());

	println ("");
	println ("#############################################################");
	println ("delete");
	println ("#############################################################");

	println ("delete with id = 10 ...");
	db.delete(User.class, "where id=?", 10);

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : (List<User>)db.all(User.class)) System.out.println(u.getId() + ":" + u.getName());

	println ("");
	println ("delete with id = 2 and name = Kawa ...");
	db.delete(User.class, "where id=? and name=?", new Object [] {2, "Kawa"});

	println ("");
	println ("confirmation");
	println ("--------------");
	for(User u : (List<User>)db.all(User.class)) System.out.println(u.getId() + ":" + u.getName());

	println ("");
	println ("#############################################################");
	println ("");
	println ("all process completed ...");

	db.close();
}
```
大抵が、printlnですが、流れを追うことができると思います。

なお、今回使用しているUserクラスの定義は以下のとおりです。ラッパークラスを使うためにはフィールド値はpublicにする必要があるので、注意してください。

```java:User.java
public class User {
	private static final long serialVersionUID = 1L;
	public int id;
	public String name;

	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}
```