SimpleJdbc クラスを使用した JDBC 操作の簡素化

SimpleJdbcInsert および SimpleJdbcCall クラスは、JDBC ドライバーを介して取得できるデータベースメタデータを利用することにより、簡素化された構成を提供します。これは、コードのすべての詳細を提供することを希望する場合、メタデータ処理をオーバーライドまたはオフにできますが、事前に構成する必要が少ないことを意味します。

SimpleJdbcInsert を使用したデータの挿入

まず、最小限の構成オプションで SimpleJdbcInsert クラスを調べることから始めます。データアクセスレイヤーの初期化メソッドで SimpleJdbcInsert をインスタンス化する必要があります。この例では、初期化メソッドは setDataSource メソッドです。SimpleJdbcInsert クラスをサブクラス化する必要はありません。代わりに、withTableName メソッドを使用して、新しいインスタンスを作成し、テーブル名を設定できます。このクラスの構成メソッドは、SimpleJdbcInsert のインスタンスを返す fluid スタイルに従います。これにより、すべての構成メソッドをチェーンにできます。次の例では、1 つの構成方法のみを使用します(複数の方法の例を後で示します)。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(3);
		parameters.put("id", actor.getId());
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		insertActor.execute(parameters);
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor")

	fun add(actor: Actor) {
		val parameters = mutableMapOf<String, Any>()
		parameters["id"] = actor.id
		parameters["first_name"] = actor.firstName
		parameters["last_name"] = actor.lastName
		insertActor.execute(parameters)
	}

	// ... additional methods
}

ここで使用される execute メソッドは、その唯一のパラメーターとしてプレーンな java.util.Map を取ります。ここで重要なことは、Map に使用されるキーは、データベースで定義されているテーブルの列名と一致する必要があるということです。これは、メタデータを読み取って実際の挿入ステートメントを作成するためです。

SimpleJdbcInsert を使用して自動生成されたキーを取得する

次の例では、前の例と同じ挿入を使用しますが、id を渡す代わりに、自動生成キーを取得して、新しい Actor オブジェクトに設定します。SimpleJdbcInsert を作成するとき、テーブル名の指定に加えて、usingGeneratedKeyColumns メソッドで生成されたキー列の名前を指定します。次のリストは、その仕組みを示しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor").usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = mapOf(
				"first_name" to actor.firstName,
				"last_name" to actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters);
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

この 2 番目のアプローチを使用して挿入を実行する場合の主な違いは、id を Map に追加せず、executeAndReturnKey メソッドを呼び出すことです。これは、ドメインクラスで使用される数値型のインスタンスを作成できる java.lang.Number オブジェクトを返します。ここで特定の Java クラスを返すためにすべてのデータベースに依存することはできません。java.lang.Number は、信頼できる基本クラスです。複数の自動生成列がある場合、または生成された値が非数値の場合、executeAndReturnKeyHolder メソッドから返される KeyHolder を使用できます。

SimpleJdbcInsert の列の指定

次の例に示すように、usingColumns メソッドを使用して列名のリストを指定することにより、挿入の列を制限できます。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingColumns("first_name", "last_name")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		Map<String, Object> parameters = new HashMap<>(2);
		parameters.put("first_name", actor.getFirstName());
		parameters.put("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingColumns("first_name", "last_name")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = mapOf(
				"first_name" to actor.firstName,
				"last_name" to actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters);
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

挿入の実行は、使用する列を決定するためにメタデータに依存していた場合と同じです。

SqlParameterSource を使用してパラメーター値を提供する

Map を使用してパラメーター値を提供することは正常に機能しますが、使用するのに最も便利なクラスではありません。Spring は、代わりに使用できる SqlParameterSource インターフェースの実装をいくつか提供します。最初のものは BeanPropertySqlParameterSource です。これは、値を含む JavaBean 準拠のクラスがある場合に非常に便利なクラスです。対応する getter メソッドを使用して、パラメーター値を抽出します。次の例は、BeanPropertySqlParameterSource の使用方法を示しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = BeanPropertySqlParameterSource(actor)
		val newId = insertActor.executeAndReturnKey(parameters)
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

別のオプションは、Map に似ていますが、連鎖できるより便利な addValue メソッドを提供する MapSqlParameterSource です。次の例は、その使用方法を示しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcInsert insertActor;

	public void setDataSource(DataSource dataSource) {
		this.insertActor = new SimpleJdbcInsert(dataSource)
				.withTableName("t_actor")
				.usingGeneratedKeyColumns("id");
	}

	public void add(Actor actor) {
		SqlParameterSource parameters = new MapSqlParameterSource()
				.addValue("first_name", actor.getFirstName())
				.addValue("last_name", actor.getLastName());
		Number newId = insertActor.executeAndReturnKey(parameters);
		actor.setId(newId.longValue());
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val insertActor = SimpleJdbcInsert(dataSource)
			.withTableName("t_actor")
			.usingGeneratedKeyColumns("id")

	fun add(actor: Actor): Actor {
		val parameters = MapSqlParameterSource()
					.addValue("first_name", actor.firstName)
					.addValue("last_name", actor.lastName)
		val newId = insertActor.executeAndReturnKey(parameters)
		return actor.copy(id = newId.toLong())
	}

	// ... additional methods
}

ご覧のとおり、構成は同じです。これらの代替入力クラスを使用するには、実行中のコードのみを変更する必要があります。

SimpleJdbcCall を使用したストアドプロシージャの呼び出し

SimpleJdbcCall クラスは、データベース内のメタデータを使用して in および out パラメーターの名前を検索するため、明示的に宣言する必要はありません。パラメーターを宣言したい場合、または Java クラスへの自動マッピングを持たないパラメーター(ARRAY や STRUCT など)がある場合は、パラメーターを宣言できます。最初の例は、MySQL データベースから VARCHAR および DATE 形式のスカラー値のみを返す簡単な手順を示しています。サンプルプロシージャは、指定されたアクターエントリを読み取り、out パラメーターの形式で first_namelast_namebirth_date 列を返します。次のリストは、最初の例を示しています。

CREATE PROCEDURE read_actor (
	IN in_id INTEGER,
	OUT out_first_name VARCHAR(100),
	OUT out_last_name VARCHAR(100),
	OUT out_birth_date DATE)
BEGIN
	SELECT first_name, last_name, birth_date
	INTO out_first_name, out_last_name, out_birth_date
	FROM t_actor where id = in_id;
END;

in_id パラメーターには、検索しているアクターの id が含まれています。out パラメーターは、テーブルから読み取ったデータを返します。

SimpleJdbcInsert の宣言と同様の方法で SimpleJdbcCall を宣言できます。データアクセスレイヤーの初期化メソッドでクラスをインスタンス化して構成する必要があります。StoredProcedure クラスと比較して、サブクラスを作成する必要はなく、データベースメタデータで検索できるパラメーターを宣言する必要もありません。次の SimpleJdbcCall 構成の例では、前述のストアドプロシージャを使用しています(DataSource に加えて唯一の構成オプションは、ストアドプロシージャの名前です)。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		this.procReadActor = new SimpleJdbcCall(dataSource)
				.withProcedureName("read_actor");
	}

	public Actor readActor(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		Map out = procReadActor.execute(in);
		Actor actor = new Actor();
		actor.setId(id);
		actor.setFirstName((String) out.get("out_first_name"));
		actor.setLastName((String) out.get("out_last_name"));
		actor.setBirthDate((Date) out.get("out_birth_date"));
		return actor;
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val procReadActor = SimpleJdbcCall(dataSource)
			.withProcedureName("read_actor")


	fun readActor(id: Long): Actor {
		val source = MapSqlParameterSource().addValue("in_id", id)
		val output = procReadActor.execute(source)
		return Actor(
				id,
				output["out_first_name"] as String,
				output["out_last_name"] as String,
				output["out_birth_date"] as Date)
	}

		// ... additional methods
}

呼び出しの実行用に記述するコードには、IN パラメーターを含む SqlParameterSource の作成が含まれます。入力値に指定された名前を、ストアドプロシージャで宣言されたパラメーター名と一致させる必要があります。メタデータを使用して、ストアドプロシージャでデータベースオブジェクトを参照する方法を決定するため、ケースを一致させる必要はありません。ストアドプロシージャのソースで指定されていることは、必ずしもデータベースに格納されている方法とは限りません。名前をすべて大文字に変換するデータベースもあれば、小文字を使用するか、指定されたとおりに大文字を使用するデータベースもあります。

execute メソッドは、IN パラメーターを受け取り、ストアードプロシージャーで指定されているように、名前でキー設定された out パラメーターを含む Map を返します。この場合、out_first_nameout_last_nameout_birth_date です。

execute メソッドの最後の部分では、取得したデータを返すために使用する Actor インスタンスを作成します。ここでも、out パラメーターの名前をストアドプロシージャで宣言されているとおりに使用することが重要です。また、結果マップに格納された out パラメーターの名前の大文字小文字は、データベース内の out パラメーター名の大文字小文字と一致しますが、これはデータベース間で異なる場合があります。コードの移植性を高めるには、大文字小文字を区別しないルックアップを行うか、Spring に LinkedCaseInsensitiveMap を使用するように指示する必要があります。後者を実行するには、独自の JdbcTemplate を作成し、setResultsMapCaseInsensitive プロパティを true に設定します。その後、このカスタマイズされた JdbcTemplate インスタンスを SimpleJdbcCall のコンストラクターに渡すことができます。次の例は、この設定を示しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor");
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
		isResultsMapCaseInsensitive = true
	}).withProcedureName("read_actor")

	// ... additional methods
}

このアクションを実行することにより、返される out パラメーターの名前に使用される大文字小文字の競合を回避できます。

SimpleJdbcCall に使用するパラメーターを明示的に宣言する

この章の前半で、パラメーターがメタデータからどのように推測されるかを説明しましたが、必要に応じて明示的に宣言できます。これを行うには、declareParameters メソッドを使用して SimpleJdbcCall を作成および構成します。declareParameters メソッドは、可変数の SqlParameter オブジェクトを入力として受け取ります。SqlParameter を定義する方法の詳細については、次のセクションを参照してください。

使用するデータベースが Spring でサポートされているデータベースでない場合は、明示的な宣言が必要です。現在、Spring は、Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle、Sybase のデータベースのストアドプロシージャ呼び出しのメタデータ検索をサポートしています。また、MySQL、Microsoft SQL Server、および Oracle のストアド関数のメタデータ検索もサポートしています。

1 つ、いくつか、すべてのパラメーターを明示的に宣言することを選択できます。パラメーターメタデータは、パラメーターを明示的に宣言しない場合でも引き続き使用されます。潜在的なパラメーターのメタデータ検索のすべての処理をバイパスし、宣言されたパラメーターのみを使用するには、宣言の一部としてメソッド withoutProcedureColumnMetaDataAccess を呼び出すことができます。データベース関数に対して 2 つ以上の異なる呼び出しシグネチャーが宣言されているとします。この場合、useInParameterNames を呼び出して、特定の署名に含める IN パラメーター名のリストを指定します。

次の例は、完全に宣言されたプロシージャ呼び出しを示し、前述の例の情報を使用しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadActor;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_actor")
				.withoutProcedureColumnMetaDataAccess()
				.useInParameterNames("in_id")
				.declareParameters(
						new SqlParameter("in_id", Types.NUMERIC),
						new SqlOutParameter("out_first_name", Types.VARCHAR),
						new SqlOutParameter("out_last_name", Types.VARCHAR),
						new SqlOutParameter("out_birth_date", Types.DATE)
				);
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

		private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
			isResultsMapCaseInsensitive = true
		}).withProcedureName("read_actor")
				.withoutProcedureColumnMetaDataAccess()
				.useInParameterNames("in_id")
				.declareParameters(
						SqlParameter("in_id", Types.NUMERIC),
						SqlOutParameter("out_first_name", Types.VARCHAR),
						SqlOutParameter("out_last_name", Types.VARCHAR),
						SqlOutParameter("out_birth_date", Types.DATE)
	)

		// ... additional methods
}

2 つの例の実行結果と最終結果は同じです。2 番目の例では、メタデータに依存するのではなく、すべての詳細を明示的に指定します。

SqlParameters を定義する方法

SimpleJdbc クラスおよび RDBMS 操作クラス(JDBC 操作を Java オブジェクトとしてモデル化するでカバー)のパラメーターを定義するには、SqlParameter またはそのサブクラスの 1 つを使用できます。そのためには、通常、コンストラクターでパラメーター名と SQL 型を指定します。SQL 型は、java.sql.Types 定数を使用して指定されます。この章の前半で、次のような宣言を見ました。

  • Java

  • Kotlin

new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SqlParameter("in_id", Types.NUMERIC),
SqlOutParameter("out_first_name", Types.VARCHAR),

SqlParameter のある最初の行は、IN パラメーターを宣言しています。SqlQuery とそのサブクラス(SqlQuery を理解するでカバー)を使用して、ストアドプロシージャコールとクエリの両方に IN パラメーターを使用できます。

2 行目(SqlOutParameter を使用)は、ストアドプロシージャコールで使用される out パラメーターを宣言しています。InOut パラメーター(プロシージャーに IN 値を提供し、値も返すパラメーター)用の SqlInOutParameter もあります。

SqlParameter および SqlInOutParameter として宣言されたパラメーターのみが、入力値を提供するために使用されます。これは、StoredProcedure クラスとは異なります。StoredProcedure クラスは(後方互換性の理由で) SqlOutParameter として宣言されたパラメーターに入力値を提供します。

IN パラメーターの場合、名前と SQL 型に加えて、数値データのスケールまたはカスタムデータベース型の型名を指定できます。out パラメーターの場合、RowMapper を提供して、REF カーソルから返された行のマッピングを処理できます。もう 1 つのオプションは、戻り値のカスタマイズされた処理を定義する機会を提供する SqlReturnType を指定することです。

SimpleJdbcCall を使用してストアド関数を呼び出す

ストアドプロシージャは、プロシージャ名ではなく関数名を指定することを除いて、ストアドプロシージャを呼び出す場合とほぼ同じメソッドで呼び出すことができます。withFunctionName メソッドを構成の一部として使用して、関数を呼び出すことを示し、関数呼び出しに対応する文字列が生成されます。特殊な呼び出し(executeFunction)を使用して関数を実行し、関数の戻り値を指定した型のオブジェクトとして返します。つまり、結果マップから戻り値を取得する必要はありません。同様の便利なメソッド(executeObject という名前)は、out パラメーターが 1 つだけのストアドプロシージャでも使用できます。次の例(MySQL の場合)は、アクターのフルネームを返す get_actor_name という名前のストアド関数に基づいています。

CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
	DECLARE out_name VARCHAR(200);
	SELECT concat(first_name, ' ', last_name)
		INTO out_name
		FROM t_actor where id = in_id;
	RETURN out_name;
END;

この関数を呼び出すには、次の例に示すように、初期化メソッドで再度 SimpleJdbcCall を作成します。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall funcGetActorName;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
				.withFunctionName("get_actor_name");
	}

	public String getActorName(Long id) {
		SqlParameterSource in = new MapSqlParameterSource()
				.addValue("in_id", id);
		String name = funcGetActorName.executeFunction(String.class, in);
		return name;
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

	private val jdbcTemplate = JdbcTemplate(dataSource).apply {
		isResultsMapCaseInsensitive = true
	}
	private val funcGetActorName = SimpleJdbcCall(jdbcTemplate)
			.withFunctionName("get_actor_name")

	fun getActorName(id: Long): String {
		val source = MapSqlParameterSource().addValue("in_id", id)
		return funcGetActorName.executeFunction(String::class.java, source)
	}

	// ... additional methods
}

使用される executeFunction メソッドは、関数呼び出しからの戻り値を含む String を返します。

SimpleJdbcCall から ResultSet または REF カーソルを返す

結果セットを返すストアドプロシージャまたは関数の呼び出しは、少し注意が必要です。JDBC 結果処理中に結果セットを返すデータベースもあれば、特定の型の明示的に登録された out パラメーターを必要とするデータベースもあります。どちらのアプローチでも、結果セットをループして返された行を処理するために追加の処理が必要です。SimpleJdbcCall では、returningResultSet メソッドを使用して、特定のパラメーターに使用する RowMapper 実装を宣言できます。結果処理中に結果セットが返される場合、名前は定義されていないため、返される結果は、RowMapper 実装を宣言する順序と一致する必要があります。指定された名前は、execute ステートメントから返される結果マップに結果の処理済みリストを保存するために引き続き使用されます。

次の例(MySQL の場合)では、IN パラメーターを受け取らず、t_actor テーブルからすべての行を返すストアドプロシージャを使用しています。

CREATE PROCEDURE read_all_actors()
BEGIN
 SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;

このプロシージャを呼び出すには、RowMapper を宣言できます。マッピングするクラスは JavaBean ルールに従うため、newInstance メソッドでマッピングするために必要なクラスを渡すことにより作成される BeanPropertyRowMapper を使用できます。次の例は、その方法を示しています。

  • Java

  • Kotlin

public class JdbcActorDao implements ActorDao {

	private SimpleJdbcCall procReadAllActors;

	public void setDataSource(DataSource dataSource) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		jdbcTemplate.setResultsMapCaseInsensitive(true);
		this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
				.withProcedureName("read_all_actors")
				.returningResultSet("actors",
				BeanPropertyRowMapper.newInstance(Actor.class));
	}

	public List getActorsList() {
		Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
		return (List) m.get("actors");
	}

	// ... additional methods
}
class JdbcActorDao(dataSource: DataSource) : ActorDao {

		private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply {
			isResultsMapCaseInsensitive = true
		}).withProcedureName("read_all_actors")
				.returningResultSet("actors",
						BeanPropertyRowMapper.newInstance(Actor::class.java))

	fun getActorsList(): List<Actor> {
		val m = procReadAllActors.execute(mapOf<String, Any>())
		return m["actors"] as List<Actor>
	}

	// ... additional methods
}

execute 呼び出しは、空の Map を渡します。これは、この呼び出しがパラメーターを受け取らないためです。次に、アクターのリストが結果マップから取得され、呼び出し元に返されます。