永続化FWを自作する(Java)

テンプレートエンジンを駆使すれば、
簡単にMyBatisやDomaみたいな、SQLを外部ファイルに持たせた永続化ＦＷを作れるのでは？
と考え作ってみました。

以下のようなコードだけで`user_mst`のデータを取得できるようになりました。

```sql:selectById.sql
select *
from user_mst
where id = {{ p.bind(id) }}
```

```java:user_mst取得するJavaコード
SqlTemplate sqlTemplate = new SqlTemplate();
DbAccess dbAccess = new DbAccess(connection);//java.sql.Connectionです

Map<String, Object> context = new HashMap<>();
context.put("id", id);
QueryBuilder query = sqlTemplate.evaluate( "selectById.sql", context);
UserMst userMst = dbAccess.selectOne( query, UserMst.class );
```

テンプレートエンジンは今回は[Pebble](http://www.mitchellbosecke.com/pebble/documentation)を使います。
が、`Apache Velocity`でも`FreeMarker`でも`Mustache.java`でも多分できると思うので、
好きなテンプレートエンジンでいいと思います。
要はテンプレートの中でメソッドを呼び出せればいいです。
JDBCを直接扱うのは面倒なので、[Apache Commons DBUtils](https://commons.apache.org/proper/commons-dbutils/)を使います。
値のセットはリフレクションを駆使して、自動的にセットします。

```groovy:build.groovy
// https://mvnrepository.com/artifact/com.mitchellbosecke/pebble
compile group: 'com.mitchellbosecke', name: 'pebble', version: '2.4.0'
// https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils
compile group: 'commons-dbutils', name: 'commons-dbutils', version: '1.7'
```

```java:SQL用テンプレートエンジン
/** SQL用テンプレートエンジン */
import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.template.PebbleTemplate;
import java.io.StringWriter;

public class SqlTemplate{
	private static PebbleEngine engine;
	/** テンプレートエンジンを取得します */
	private PebbleEngine getEngine(){
		if( null == engine){
			engine = new PebbleEngine.Builder()
					.autoEscaping(false)//htmlエスケープは余計なので外す
					.strictVariables(true)//contextにない変数使おうとすると例外発生させる設定
					.build();
		}
		return engine;
	}

	/** sqlテンプレート名、テンプレート用の値を元にクエリを取得 */
	public QueryBuilder evaluate(String templateFileName, Map<String, Object> context) throws Exception{
		SqlParametors sqlParametors = new SqlParametors();
		context.put( "p", sqlParametors );
		String result = simpleEvaluateTemplate(templateFileName, context );
		QueryBuilder query = new QueryBuilder( result, sqlParametors.getParams() );
		return query;
	}

	/** テンプレートを評価して、結果の文字列返すだけのメソッド */
	private String simpleEvaluateTemplate(String templateFileName, Map<String, Object> context) throws Exception{
		StringWriter writer = new StringWriter();
		PebbleTemplate template = getEngine().getTemplate(templateFileName);
		template.evaluate(writer, context);
		return writer.toString();
	}
}
```

```java:sqlパラメータ
import java.util.List;

/** sqlパラメータ格納用クラス.テンプレートエンジン内でアクセスします。 */
public class SqlParametors {
	/** パラメーター */
	private List<Object> params = new ArrayList<>();
	/** パラメーターをバインドします */
	public String bind(Object param){
		params.add(param);
		return "?";
	}
	/** パラメーター取得 */
	public List<Object> getParams(){
		return params;
	}
}
```

```java:クエリ格納用クラス
import java.util.List;
import java.util.Arrays;

/** クエリ格納用クラス */
public class QueryBuilder {
	/** sql文 */
	private StringBuilder sql = new StringBuilder();
	/** パラメーター */
	private List<Object> params = new ArrayList<>();

	public QueryBuilder(){
	}

	public QueryBuilder(CharSequence sql, List<Object>params){
		this.sql.append(sql);
		this.params.addAll(params);
	}

	public StringBuilder getSql() {
		return sql;
	}

	public void setSql(CharSequence sql) {
		this.sql = new StringBuilder( sql.toString() );
	}

	public Object[] getParams() {
		return params.toArray();
	}

	public void setParams(Object[] params) {
		this.params = Arrays.asList(params);
	}


}
```

```java:DBアクセス
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.MapHandler;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/** DBアクセスクラス。QueryBuilderオブジェクトを引数にDB操作する */
class DbAccess{
	/** クエリ実行(dbutils) */
	private QueryRunner queryRunner = new QueryRunner();

	/** コネクション */
	private java.sql.Connection conn;

	/** コンストラクタ */
	public DbAccess(java.sql.Connection conn){
		this.conn = conn;
	}

	/** insert文やupdate文用 */
	public int update(QueryBuilder query ) throws Exception{
		int rows = queryRunner.update(conn, query.getSql().toString(), query.getParams() );
		return rows;
	}

	/** selectしてその結果をリストで取得したいとき用 */
	public <T> List<T>  selectList(QueryBuilder query, Class<T> clazz) throws Exception{
		ResultSetHandler<List<Map<String, Object>>> h = new MapListHandler();
		List<Map<String, Object>> mapList = queryRunner.query(conn, query.getSql().toString(), h, query.getParams() );
		List<T> beanList = convMapList2BeanList(mapList, clazz);
		return beanList;
	}

	/** selectしてその結果を1行だけ取得したいとき用 */
	public <T> T selectOne(QueryBuilder query, Class<T> clazz) throws Exception{
		ResultSetHandler<Map<String, Object>> h = new MapHandler();
		Map<String, Object> map = queryRunner.query(conn, query.getSql().toString(), h ,query.getParams());
		if( null == map )
			return null;
		T bean = convMap2Bean(map, clazz );
		return bean;
	}

	/** selectしてその結果を1列だけ取得したいとき用 */
	public <T> T selectOneColumn(QueryBuilder query, Class<T> clazz) throws Exception{
		Object value = queryRunner.query(conn, query.getSql().toString(), new ScalarHandler<T>(), query.getParams() );
		return clazz.cast(value);
	}
	
	/**
	 * MapオブジェクトリストをBeanリストへセットする.
	 * @param mapList Mapオブジェクトのリスト
	 * @param clazz セット先Beanクラス
	 * @throws Exception
	 */
	private <T> List<T> convMapList2BeanList(List<Map<String, Object>> mapList, Class<T> clazz) throws Exception{
		List<T> list = new ArrayList<>();
		for( int i = 0; i < mapList.size(); i++ ){
			list.add( convMap2Bean(mapList.get(i), clazz) );
		}
		return list;
	}

	/**
	 * MapオブジェクトをBeanへセットする.
	 * @param map Mapオブジェクト
	 * @param clazz セット先Beanクラス
	 * @throws Exception
	 */
	private <T> T convMap2Bean(Map<String, ?> map, Class<T> clazz) throws Exception{
		T bean = clazz.newInstance();
		//フィールド一覧取得
		Field[] fields = clazz.getDeclaredFields();

		for( Field f : fields ){
			f.setAccessible(true);
			f.set(bean, map.get(f.getName()) );//TODO スネークケース＝＞キャメルへの変換処理入れてもいいですね。
		}
		return bean;
	}
}

```