(2012) テストデータ投入用のオレオレsql builder(Java)

テストデータっていうかユニットテスト用の fixture ってやつでしょうか。
ほんとは

table: foo
data:
  id   : 1
  name : test tarou
  age  : 20
  created_at: "2012-08-26 12:34"

みたいな yaml をテストケース内に書けるといいなーと思いつつできないので次善の策として類似ライブラリなど調べずに適当に書いてみた。

なぜ普通のinsert文ではなくyamlみたいな書式で書きたいかというと、insert文では見た目的に key と value の対応が分かりにくくデバッグがめんどくさいから。


こんな感じで使う。

String sql = new InsertSqlBuilder()
  .insertInto("foo")
  ._(  "id"        , 1)
  ._(  "name"      , "test tarou")
  ._(  "age"       , 20)
  .raw("created_at", "now()") // mysql
  .toString();

これでも () と "" が邪魔くさくはあるけどとりあえずは良しということで……

String sql = new InsertSqlBuilder()
  .insertInto("foo"
  )._(   "id"        , 1
  )._(   "name"      , "test tarou"
  )._(   "age"       , 20
  ).raw( "created_at", "now()" // mysql
  ).toString();

とためしに括弧をずらして書いてみたり……うーん。

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class InsertSqlBuilder {
	private String tableName;

	private List<KV> kvList = new ArrayList<KV>();

	public InsertSqlBuilder insertInto(String tableName){
		this.tableName = tableName;
		return this;
	}
	
	public InsertSqlBuilder _(String key, Object val){
		kvList.add(new KV(key, val, false));
		return this;
	}
	
	/**
	 * 文字列リテラルとしてクォートされるのを抑制したい場合はこっちを使う。
	 */
	public InsertSqlBuilder raw(String key, Object val){
		kvList.add(new KV(key, val, true));
		return this;
	}
	
	public String toString(){
		String result = "";
		result += "insert into "
			+ tableName.toUpperCase()
			+ "\n(\t" + joinKeys(", ") + "\t) values"
			+ "\n(\t" + joinValues(",\t") + "\t);";
		return result;
	}
	
	private String joinKeys(String sep){
		String result = "";

		for(int i=0; i<kvList.size(); i++){
			if(i >= 1){ result += sep; }
			result += kvList.get(i).key;
		}
		
		return result;
	}
	
	private String joinValues(String sep){
		String result = "";

		for(int i=0; i<kvList.size(); i++){
			if(i>=1){ result += sep; }
			
			KV kv = kvList.get(i);
			System.err.println("" + i + ": " + kv.value.getClass());
			result += "/*" + kv.key + "*/"
				+ kv.toSqlToken();
		}
		
		return result;
	}
	
	public InsertSqlBuilder print(){
		System.out.println(this.toString());
		return this;
	}
}


class KV<T> {
	public String key;
	public T value;
	public Boolean isRaw;
	
	KV(String key, T value, Boolean isRaw){
		this.key = key;
		this.value = value;
		this.isRaw = isRaw;
	}
	
	public String toSqlToken(){
		Class _class = this.value.getClass();
		if (Arrays.asList(String.class, Timestamp.class).contains(_class)
				&& !isRaw) {
			// TODO エスケープ処理(必要なら)
			return String.format("'%s'", this.value);
		}else{
			return "" + this.value;
		}
	}
	
	public String toString(){
		return String.format("{k=%s, v=%s}", key, value);
	}
}

ちなみに、値の前に /*label*/ でラベルを付けるスタイルを最初に見たのは Seasar2 の 2Way SQL でしたが、先日リーダブルコードを読んでいたら関数の呼び出しで

Connect(/* timeout_ms = */ 10, /* user_encryption = */ false);

みたいに書く例が載っていて(p77, 「名前付き引数」コメント)、あ、これは「insert文で key と value の対応が分かりにくい問題」への対処として使えるかも、と思って試してみた次第。