JDBC 操作を Java オブジェクトとしてモデル化する

org.springframework.jdbc.object パッケージには、よりオブジェクト指向の方法でデータベースにアクセスできるクラスが含まれています。例として、クエリを実行して、ビジネスオブジェクトのプロパティにマッピングされたリレーショナル列データを含むビジネスオブジェクトを含むリストとして結果を取得できます。ストアドプロシージャを実行し、更新、削除、挿入ステートメントを実行することもできます。

多くの Spring 開発者は、以下で説明するさまざまな RDBMS 操作クラス(StoredProcedure クラスを除く)は、多くの場合、ストレート JdbcTemplate 呼び出しで置き換えることができると考えています。多くの場合、JdbcTemplate のメソッドを直接呼び出す DAO メソッドを記述する方が簡単です(クエリを本格的なクラスとしてカプセル化するのとは対照的です)。

ただし、RDBMS 操作クラスを使用して測定可能な値を取得している場合は、これらのクラスを引き続き使用する必要があります。

SqlQuery を理解する

SqlQuery は、SQL クエリをカプセル化する再利用可能なスレッドセーフクラスです。サブクラスは、newRowMapper(..) メソッドを実装して、クエリの実行中に作成された ResultSet を反復処理して取得した行ごとに 1 つのオブジェクトを作成できる RowMapper インスタンスを提供する必要があります。MappingSqlQuery サブクラスは、行を Java クラスにマッピングするためのはるかに便利な実装を提供するため、SqlQuery クラスが直接使用されることはほとんどありません。SqlQuery を継承する他の実装は、MappingSqlQueryWithParameters および UpdatableSqlQuery です。

MappingSqlQuery を使用する

MappingSqlQuery は再利用可能なクエリで、具象サブクラスは抽象 mapRow(..) メソッドを実装して、指定された ResultSet の各行を指定された型のオブジェクトに変換する必要があります。次の例は、t_actor リレーションのデータを Actor クラスのインスタンスにマッピングするカスタムクエリを示しています。

  • Java

  • Kotlin

public class ActorMappingQuery extends MappingSqlQuery<Actor> {

	public ActorMappingQuery(DataSource ds) {
		super(ds, "select id, first_name, last_name from t_actor where id = ?");
		declareParameter(new SqlParameter("id", Types.INTEGER));
		compile();
	}

	@Override
	protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
		Actor actor = new Actor();
		actor.setId(rs.getLong("id"));
		actor.setFirstName(rs.getString("first_name"));
		actor.setLastName(rs.getString("last_name"));
		return actor;
	}
}
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(ds, "select id, first_name, last_name from t_actor where id = ?") {

	init {
		declareParameter(SqlParameter("id", Types.INTEGER))
		compile()
	}

	override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
			rs.getLong("id"),
			rs.getString("first_name"),
			rs.getString("last_name")
	)
}

このクラスは、Actor 型でパラメーター化された MappingSqlQuery を継承します。この顧客クエリのコンストラクターは、DataSource を唯一のパラメーターとして受け取ります。このコンストラクターでは、DataSource と、このクエリの行を取得するために実行する必要がある SQL を使用して、スーパークラスのコンストラクターを呼び出すことができます。この SQL は PreparedStatement を作成するために使用されるため、実行中に渡されるパラメーターのプレースホルダーを含めることができます。SqlParameter に渡す declareParameter メソッドを使用して、各パラメーターを宣言する必要があります。SqlParameter は、java.sql.Types で定義されている名前と JDBC 型を受け取ります。すべてのパラメーターを定義した後、ステートメントを準備して後で実行できるように、compile() メソッドを呼び出すことができます。このクラスはコンパイル後にスレッドセーフであるため、DAO の初期化時にこれらのインスタンスが作成される限り、インスタンス変数として保持して再利用できます。次の例は、そのようなクラスを定義する方法を示しています。

  • Java

  • Kotlin

private ActorMappingQuery actorMappingQuery;

@Autowired
public void setDataSource(DataSource dataSource) {
	this.actorMappingQuery = new ActorMappingQuery(dataSource);
}

public Actor getActor(Long id) {
	return actorMappingQuery.findObject(id);
}
private val actorMappingQuery = ActorMappingQuery(dataSource)

fun getActor(id: Long) = actorMappingQuery.findObject(id)

前述の例のメソッドは、唯一のパラメーターとして渡された id を使用してアクターを取得します。返されるオブジェクトを 1 つだけに限定したいため、パラメーターとして id を使用して findObject コンビニエンスメソッドを呼び出します。代わりに、オブジェクトのリストを返し、追加のパラメーターを取るクエリがある場合は、可変引数として渡されたパラメーター値の配列を取る execute メソッドの 1 つを使用します。次の例は、このようなメソッドを示しています。

  • Java

  • Kotlin

public List<Actor> searchForActors(int age, String namePattern) {
	return actorSearchMappingQuery.execute(age, namePattern);
}
fun searchForActors(age: Int, namePattern: String) =
			actorSearchMappingQuery.execute(age, namePattern)

SqlUpdate を使用する

SqlUpdate クラスは、SQL 更新をカプセル化します。クエリと同様に、更新オブジェクトは再利用可能であり、すべての RdbmsOperation クラスと同様に、更新にはパラメーターを含めることができ、SQL で定義されます。このクラスは、クエリオブジェクトの execute(..) メソッドに類似した多くの update(..) メソッドを提供します。SqlUpdate クラスは具体的です。たとえば、カスタム更新メソッドを追加するために、サブクラス化できます。ただし、SqlUpdate クラスをサブクラス化する必要はありません。これは、SQL の設定とパラメーターの宣言によって簡単にパラメーター化できるためです。次の例では、execute という名前のカスタム更新メソッドを作成します。

  • Java

  • Kotlin

import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;

public class UpdateCreditRating extends SqlUpdate {

	public UpdateCreditRating(DataSource ds) {
		setDataSource(ds);
		setSql("update customer set credit_rating = ? where id = ?");
		declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
		declareParameter(new SqlParameter("id", Types.NUMERIC));
		compile();
	}

	/**
	 * @param id for the Customer to be updated
	 * @param rating the new value for credit rating
	 * @return number of rows updated
	 */
	public int execute(int id, int rating) {
		return update(rating, id);
	}
}
import java.sql.Types
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.SqlUpdate

class UpdateCreditRating(ds: DataSource) : SqlUpdate() {

	init {
		setDataSource(ds)
		sql = "update customer set credit_rating = ? where id = ?"
		declareParameter(SqlParameter("creditRating", Types.NUMERIC))
		declareParameter(SqlParameter("id", Types.NUMERIC))
		compile()
	}

	/**
	 * @param id for the Customer to be updated
	 * @param rating the new value for credit rating
	 * @return number of rows updated
	 */
	fun execute(id: Int, rating: Int): Int {
		return update(rating, id)
	}
}

StoredProcedure を使用する

StoredProcedure クラスは、RDBMS ストアードプロシージャーのオブジェクト抽象化のための abstract スーパークラスです。

継承された sql プロパティは、RDBMS のストアドプロシージャの名前です。

StoredProcedure クラスのパラメーターを定義するには、SqlParameter またはそのサブクラスの 1 つを使用できます。次のコードスニペットに示すように、コンストラクターでパラメーター名と SQL 型を指定する必要があります。

  • 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),

SQL 型は、java.sql.Types 定数を使用して指定されます。

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

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

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

次の単純な DAO の例では、StoredProcedure を使用して、Oracle データベースに付属している関数(sysdate())を呼び出します。ストアドプロシージャ機能を使用するには、StoredProcedure を継承するクラスを作成する必要があります。この例では、StoredProcedure クラスは内部クラスです。ただし、StoredProcedure を再利用する必要がある場合は、最上位クラスとして宣言できます。この例には入力パラメーターはありませんが、SqlOutParameter クラスを使用して出力パラメーターが日付型として宣言されています。execute() メソッドはプロシージャを実行し、結果 Map から返された日付を抽出します。結果 Map には、パラメーター名をキーとして使用して、宣言された各出力パラメーター(この場合は 1 つのみ)のエントリがあります。次のリストは、カスタム StoredProcedure クラスを示しています。

  • Java

  • Kotlin

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class StoredProcedureDao {

	private GetSysdateProcedure getSysdate;

	@Autowired
	public void init(DataSource dataSource) {
		this.getSysdate = new GetSysdateProcedure(dataSource);
	}

	public Date getSysdate() {
		return getSysdate.execute();
	}

	private class GetSysdateProcedure extends StoredProcedure {

		private static final String SQL = "sysdate";

		public GetSysdateProcedure(DataSource dataSource) {
			setDataSource(dataSource);
			setFunction(true);
			setSql(SQL);
			declareParameter(new SqlOutParameter("date", Types.DATE));
			compile();
		}

		public Date execute() {
			// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
			Map<String, Object> results = execute(new HashMap<String, Object>());
			Date sysdate = (Date) results.get("date");
			return sysdate;
		}
	}

}
import java.sql.Types
import java.util.Date
import java.util.Map
import javax.sql.DataSource
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class StoredProcedureDao(dataSource: DataSource) {

	private val SQL = "sysdate"

	private val getSysdate = GetSysdateProcedure(dataSource)

	val sysdate: Date
		get() = getSysdate.execute()

	private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {

		init {
			setDataSource(dataSource)
			isFunction = true
			sql = SQL
			declareParameter(SqlOutParameter("date", Types.DATE))
			compile()
		}

		fun execute(): Date {
			// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
			val results = execute(mutableMapOf<String, Any>())
			return results["date"] as Date
		}
	}
}

次の StoredProcedure の例には、2 つの出力パラメーターがあります(この場合、Oracle REF カーソル)。

  • Java

  • Kotlin

import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAndGenresStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "AllTitlesAndGenres";

	public TitlesAndGenresStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
		compile();
	}

	public Map<String, Object> execute() {
		// again, this sproc has no input parameters, so an empty Map is supplied
		return super.execute(new HashMap<String, Object>());
	}
}
import java.util.HashMap
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

	companion object {
		private const val SPROC_NAME = "AllTitlesAndGenres"
	}

	init {
		declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
		declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper()))
		compile()
	}

	fun execute(): Map<String, Any> {
		// again, this sproc has no input parameters, so an empty Map is supplied
		return super.execute(HashMap<String, Any>())
	}
}

TitlesAndGenresStoredProcedure コンストラクターで使用されていた declareParameter(..) メソッドのオーバーロードされたバリアントが RowMapper 実装インスタンスに渡される方法に注意してください。これは、既存の機能を再利用するための非常に便利で強力な方法です。次の 2 つの例は、2 つの RowMapper 実装のコードを提供します。

TitleMapper クラスは、次のように、指定された ResultSet の各行の ResultSet を Title ドメインオブジェクトにマップします。

  • Java

  • Kotlin

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;

public final class TitleMapper implements RowMapper<Title> {

	public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
		Title title = new Title();
		title.setId(rs.getLong("id"));
		title.setName(rs.getString("name"));
		return title;
	}
}
import java.sql.ResultSet
import com.foo.domain.Title
import org.springframework.jdbc.core.RowMapper

class TitleMapper : RowMapper<Title> {

	override fun mapRow(rs: ResultSet, rowNum: Int) =
			Title(rs.getLong("id"), rs.getString("name"))
}

GenreMapper クラスは、次のように、指定された ResultSet の各行の ResultSet を Genre ドメインオブジェクトにマップします。

  • Java

  • Kotlin

import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;

public final class GenreMapper implements RowMapper<Genre> {

	public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
		return new Genre(rs.getString("name"));
	}
}
import java.sql.ResultSet
import com.foo.domain.Genre
import org.springframework.jdbc.core.RowMapper

class GenreMapper : RowMapper<Genre> {

	override fun mapRow(rs: ResultSet, rowNum: Int): Genre {
		return Genre(rs.getString("name"))
	}
}

RDBMS の定義に 1 つ以上の入力パラメーターがあるストアドプロシージャにパラメーターを渡すには、次の例に示すように、スーパークラスの型なし execute(Map) メソッドに委譲する厳密に型指定された execute(..) メソッドをコーディングできます。

  • Java

  • Kotlin

import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

public class TitlesAfterDateStoredProcedure extends StoredProcedure {

	private static final String SPROC_NAME = "TitlesAfterDate";
	private static final String CUTOFF_DATE_PARAM = "cutoffDate";

	public TitlesAfterDateStoredProcedure(DataSource dataSource) {
		super(dataSource, SPROC_NAME);
		declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
		declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
		compile();
	}

	public Map<String, Object> execute(Date cutoffDate) {
		Map<String, Object> inputs = new HashMap<String, Object>();
		inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
		return super.execute(inputs);
	}
}
import java.sql.Types
import java.util.Date
import javax.sql.DataSource
import oracle.jdbc.OracleTypes
import org.springframework.jdbc.core.SqlOutParameter
import org.springframework.jdbc.core.SqlParameter
import org.springframework.jdbc.object.StoredProcedure

class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) {

	companion object {
		private const val SPROC_NAME = "TitlesAfterDate"
		private const val CUTOFF_DATE_PARAM = "cutoffDate"
	}

	init {
		declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
		declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
		compile()
	}

	fun execute(cutoffDate: Date) = super.execute(
			mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate))
}