Webエンジニア susumuis の技術ブログ

このブログの内容は個人の見解であり、所属する組織の公式見解ではありません

WicketでJDBCでSQLExceptionが面倒な件

オープンソース化の作業を進めつつ、しこしこコード修正しています。

独自実装のJDBCファサードがありまして...

僕は、ずっとORマッパやDIコンテナを使わずに、ひたすらJDBCをラップしたクラスを使って
MapをListにしたようなデータ構造に値を流し込むプログラミングをしていました。

こんな感じです

DBManager dbManager = null;
try {
  // このメソッドでコネクションプールからコネクション取ってくる。
  dbManager = getDBManager(); 

  // PreparedStatementを簡単化したクラス
  ISimpleStatement stmt = newSimpleStatement(dbManager);
  stmt.append("select * from item;");
  ResultSet rs = stmt.executeQuery();
  // rs をいじくりまくる。
} finally {
  if (dbManager != null) {
    // StatementやResultSetなど使った資源をここで解放、コネクションをプールに戻す。
    dbManager.freeResource();
  }
}

try 〜 finally部分は不慣れな人にお願いすると、かなりの確率で書き間違えます。
なので、実際はコンテナ側で自動化して、各ロジッククラスには、DBManagerをパラメータにした
メソッドを固めます。

// このメソッドで取得すると、リクエストが終わったときに自動的にフレームワークが解放してくれる。
DBManager dbManager = getAutoDBManager(); 
hogehogeBean.hogehoge(dbManager);
class HogehogeBean() {
  public void hogehoge(DBManager dbManager) {
    // hogehoge実装
  }
}


オープンソースプロジェクトをやるときに、このままでいいのかわかりませんが、これはこれで使いやすいんですよね…


で、このクラスをWicketで使おうとすると、

罠がありました。

その罠とは SQLException です。

  • ResultSetを直接操作しているので、すべての操作は SQLException をthrowします。
  • SQLExceptionはチェックされる例外なので、メソッドにthrowsを書くか、tryでcatchしなければいけません。
  • ところが、例えばWicketのButtonクラスのonSubmitは
final Button button = new Button("button") {
  public void onSubmit() { //<------------- throws SQLExceptionが書けない
  }
}

親クラスのButtonがonSubmitでExceptionをthrowsしていないため、すべてのチェック例外を自分でtry〜catchしなければなりません。

それは良いことなのは分かっていますが、毎回

final Button button = new Button("button") {
  public void onSubmit() { //<------------- throws SQLExceptionが書けない
    try {
      // 処理
    } catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }
}

のように書くのはあまりに冗長です。
さっきのtry 〜 finallyとあわせると、たった1行のSQLのために雑音がすごいことになります。

例:

try {
  DBManager dbManager = null;
  try {
    dbManager = getDBManager();
    ISimpleStatement stmt = newSimpleStatement(dbManager);
    stmt.append("select * from item;"); // <-- オリジナルな記述はここだけ
    ResultSet rs = stmt.executeQuery();
    // rs をいじくりまくる。             // <-- 大抵はListにしてReturnするだけ
  } finally {
    if (dbManager != null) {
      dbManager.freeResource();
    }
  }
} catch (SQLException e) {
  throw new RuntimeException(e);
}

フレームワークを作るか?

この問題を解決する方法を考えていくと、これ
http://commons.apache.org/dbutils/
の、出来の悪い再開発みたいなものができてしまいました。

でも、それはちょっとないなぁと。

やっぱり、むやみにフレームワークを増やしたくはありません。
そこで、とりあえずIDataProviderをカスタマイズして以下のものを使ってみました。
DB処理はDataViewで表示するためのリストの取得が多いので、これを使うようにすれば、とりあえずの問題は解決です。
依然、ボタンのイベントに関しては自分でやらなくちゃいけないのがネックですが、それはそれで考えます。

/**
 * DBManagerの取得解放SQLException処理を自動化でやってくれるDataProviderです。
 * @author s-ishigami
 */
public abstract class DBDataProvider implements IDataProvider {

	/**
	 * {@link #iterator(int, int)}の代わりにここを実装してデータを取得するSQL処理を書いてください。
	 * 
	 * @see IDataProvider#iterator(int, int)
	 */
	protected abstract Iterator<?> iteratorBySql(DBManager dbManager, int first, int count) throws SQLException;
	
	/**
	 * {@link #size()}の代わりにここを実装して件数を取得するSQL処理を書いてください。
	 * 
	 * @see IDataProvider#size()
	 */
	protected abstract int sizeBySql(DBManager dbManager) throws SQLException;
	
	/**
	 * {@link #iteratorBySql(DBManager, int, int)}処理中に{@link SQLException}が発生した時、呼ばれるイベントです。
	 * 
	 * <p>通常はRuntimeExceptionを投げますが、オーバーライドすることにより代替値を返したり、エラーメッセージをコンポーネントに入れたりすることができます。</p>
	 * 
	 * @param e 発生したException
	 * @return 代替値
	 * @throws RuntimeException 例外が処理しきれなかったとき
	 */
	protected Iterator<?> onIteratorSqlException(SQLException e) {
		throw new RuntimeException(e);
	}
	
	/**
	 * {@link #sizeBySql(DBManager)}処理中に{@link SQLException}が発生した時、呼ばれるイベントです。
	 * 
	 * <p>通常はRuntimeExceptionを投げますが、オーバーライドすることにより代替値を返したり、エラーメッセージをコンポーネントに入れたりすることができます。</p>
	 * 
	 * @param e 発生したException
	 * @return 代替値
	 * @throws RuntimeException 例外が処理しきれなかったとき
	 */
	private int onSizeSqlException(SQLException e) {
		throw new RuntimeException(e);
	}
	
	/**
	 * @see IDataProvider#iterator(int, int)
	 */
	public Iterator<?> iterator(int first, int count) {
		try {
			DBManager dbManager = null;
			try {
				return iteratorBySql(dbManager, first, count);
			} finally {
				if (dbManager != null) {
					dbManager.freeResource();
				}
			}
		} catch (SQLException e) {
			return onIteratorSqlException(e);
		}
	}
	
	/**
	 * @see IDataProvider#model(Object)
	 */
	public IModel model(Object object) {
		return new Model((Serializable) object);
	}

	/**
	 * @see IDataProvider#size()
	 */
	public int size() {
		try {
			DBManager dbManager = null;
			try {
				return sizeBySql(dbManager);
			} finally {
				if (dbManager != null) {
					dbManager.freeResource();
				}
			}
		} catch (SQLException e) {
			return onSizeSqlException(e);
		}
	}

	
	public void detach() {
	}
}

使い方

final DataView view = new DataView("list", new DBDataProvider() {
	protected Iterator<?> iteratorBySql(DBManager dbManager, int first, int count) throws SQLException {
		return bean.getRanking(dbManager).iterator();
	}
	protected int sizeBySql(DBManager dbManager) throws SQLException {
		return bean.getRankingSize(dbManager);
	}
}, 10) {
	protected void populateItem(Item item) {
		// ここで行毎のadd処理
	}
};

DataProviderはInnerClassにした方がいいかもしれないですね。
その場合はstaticやTopLevelクラスにするのではなく、非スタティック内部クラスにするのがいい感じだと思っています。
そうすれば、errorとか、ページクラスのメソッドが直接使えます。