はじめに
データリポジトリ のために開発していたデータベース処理ライブラリに以下の機能を追加しました。
- 非同期メソッド
- インターフェースでプログラミングしやすくするための拡張メソッド
- コネクションやコマンドのメソッドに任意の処理を割り込ませるためのフィルタ
公開先
非同期メソッド
IDbConnection などのインターフェースに対してデータベース処理を非同期化する拡張メソッドを追加しました。.NET Standard 2.1 でサポートされた非同期メソッドと同等の機能を .NET Standard 2.0 でも利用できるようになります。
IDbConnection connection = SampleDatabase.CreateConnection();
try
{
await connection.OpenAsync(cancellationToken);
using var transaction = await connection.BeginTransactionAsync(cancellationToken: cancellationToken);
using var command = connection.CreateCommand();
command.CommandText = "insert into SAMPLE_TABLE values (1, 'abc')";
command.Transaction = transaction;
await command.ExecuteNonQueryAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
finally
{
await connection.DisposeAsync();
}
追加されるメソッドは以下の通りです。
IDbConnection インターフェース
- DisposeAsync
- OpenAsync
- CloseAsync
- ChangeDatabaseAsync
- BeginTransactionAsync
IDbTransaction
- DisposeAsync
- CommitAsync
- RollbackAsync
IDbCommand
- DisposeAsync
- PrepareAsync
- ExecuteReaderAsync
- ExecuteScalarAsync
- ExecuteNonQueryAsync
IDataReader
- DisposeAsync
- CloseAsync
- GetSchemaTableAsync
- ReadAsync
- NextResultAsync
IDataRecord
- IsDBNullAsync
- GetValueAsync
- GetValuesAsync
拡張メソッド
各インターフェースに対して頻繁に実装する処理を拡張メソッドとして追加しました。
IDbConnection インターフェース
OpenIfClosed メソッド
コネクションが閉じている場合は開きます。開いている場合は何もしません。
void Execute(IDbConnection connection)
{
// コネクションが閉じている場合は開く
bool opened = connection.OpenIfClosed();
try
{
var command = connection.CreateCommand();
command.CommandText = "update SAMPLE_TABLE set ...";
command.ExecuteNonQuery();
}
finally
{
// コネクションを開いた場合は閉じる
if (opened) { connection.Close(); }
}
}
Cast<TConnection> メソッド
インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。
- コネクションの型が IDbConnectionWrapper インターフェースを実装する場合、ラップしているコネクションをキャストします。ラッパーの仕様によっては、ラップしているコネクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
IDbConnection connection = CreateConnection();
connection.Cast<SqlConnection>(x => {
// SqlConnection 固有のプロパティに値を設定する
x.StatisticsEnabled = true;
});
IDbTransaction インターフェース
Cast<TTransaction> メソッド
インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。
- コネクションの型が IDbTransactionWrapper インターフェースを実装する場合、ラップしているトランザクションをキャストします。ラッパーの仕様によっては、ラップしているトランザクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
IDbTransaction transaction = connection.BeginTransaction();
transaction.Cast<OracleTransaction>(x => {
// OracleTransaction 固有のメソッドを実行する
x.Save();
});
IDbCommand インターフェース
Add{値型}Parameter メソッド
コマンドにパラメーターを追加します。
-
追加されたパラメーターのインデックスとパラメーターのタプルを戻り値として返します。
-
パラメーターの値を Object 型で指定する AddParameter メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
- AddParameter
- AddBooleanParameter
- AddByteParameter
- AddInt16Parameter
- AddInt32Parameter
- AddInt64Parameter
- AddSByteParameter
- AddUInt16Parameter
- AddUInt32Parameter
- AddUInt64Parameter
- AddSingleParameter
- AddDoubleParameter
- AddDecimalParameter
- AddDateTimeParameter
- AddDateTimeOffsetParameter
- AddTimeParameter
- AddStringParameter
- AddCharParameter
- AddGuidParameter
- AddBinaryParameter
IDbCommand command = CreateCommand();
var (index1, parameter2) = command.AddBooleanParameter("param1", true);
var (index1, parameter2) = command.AddStringParameter("param2");
parameter2.Value = "abc";
GetParameter メソッド
指定されたインデックスまたはパラメーター名に対応するパラメーターを取得します。
- IDataParameterCollection インターフェースのインデクサで取得できるパラメーターの型は Object 型ですが、このメソッドの戻り値の型は IDbDataParameter 型です。
IDbCommand command = CreateCommand();
command.AddBooleanParameter("param1", true);
command.AddStringParameter("param2", "abc");
IDbDataParameter param1 = command.GetParameter(0);
IDbDataParameter param2 = command.GetParameter("param2");
Set{値型}ParameterValue メソッド
指定されたパラメーターに値を設定します。
- パラメーターの値を Object 型で指定する SetParameterValue メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
- SetParameterValue
- SetBooleanParameterValue
- SetByteParameterValue
- SetInt16ParameterValue
- SetInt32ParameterValue
- SetInt64ParameterValue
- SetSByteParameterValue
- SetUInt16ParameterValue
- SetUInt32ParameterValue
- SetUInt64ParameterValue
- SetSingleParameterValue
- SetDoubleParameterValue
- SetDecimalParameterValue
- SetDateTimeParameterValue
- SetDateTimeOffsetParameterValue
- SetTimeParameterValue
- SetStringParameterValue
- SetCharParameterValue
- SetGuidParameterValue
- SetBinaryParameterValue
IDbCommand command = CreateCommand();
command.AddBooleanParameter("param1");
command.AddStringParameter("param2");
command.SetBooleanParameterValue(0, true);
command.SetStringParameterValue("param2", "abc");
Cast<TCommand> メソッド
インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。
- コマンドの型が IDbCommandWrapper インターフェースを実装する場合、ラップしているコマンドをキャストします。ラッパーの仕様によっては、ラップしているコマンドを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
IDbCommand command = connection.CreateCommand();
command.Cast<OracleCommand>(x => {
// OracleCommand 固有のプロパティに値を設定する
x.BindByName = true;
});
IDataParameterCollection インターフェース
Cast<TParameters> メソッド
IDataParameterCollection インターフェースに対しても Cast メソッドを追加していますが、使う場面はほとんどないと思います。
- パラメーターコレクションの型が IDataParameterCollectionWrapper インターフェースを実装する場合、ラップしているパラメーターコレクションをキャストします。ラッパーの仕様によっては、ラップしているパラメーターコレクションを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
using var connection = SampleDatabase.CreateConnection();
var command = connection.CreateCommand();
command.CommandText = "select * from SAMPLE_TABLE where ID = @id";
command.Parameters.Cast<SqlParameterCollection>(x =>
{
// SqlParameterCollection 固有のメソッドを実行する
x.AddWithValue("id", 1);
});
IDbDataParameter インターフェース
Set{値型}Value メソッド
指定されたパラメーターに値を設定します。
- パラメーターの値を Object 型で指定する SetValue メソッドの他、各値型で指定するメソッドを追加しています。なお、SByte, UInt16, UInt32, UInt64, Guid はデータベースプロバイダによっては例外がスローされます。それぞれのデータベースプロバイダのリファレンスを参照してください。
- SetValue
- SetBooleanValue
- SetByteValue
- SetInt16Value
- SetInt32Value
- SetInt64Value
- SetSByteValue
- SetUInt16Value
- SetUInt32Value
- SetUInt64Value
- SetSingleValue
- SetDoubleValue
- SetDecimalValue
- SetDateTimeValue
- SetDateTimeOffsetValue
- SetTimeValue
- SetStringValue
- SetCharValue
- SetGuidValue
- SetBinaryValue
IDbCommand command = CreateCommand();
var (_, param1) = command.AddBooleanParameter("param1");
var (_, param2) = command.AddStringParameter("param2");
param1.SetBooleanValue(true);
param2.SetStringValue("abc");
Cast<TParameter> メソッド
インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。
- パラメーターの型が IDbDataParameterWrapper インターフェースを実装する場合、ラップしているパラメーターをキャストします。ラッパーの仕様によっては、ラップしているパラメーターを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
IDbCommand command = CreateCommand();
var (_, param1) = command.AddBooleanParameter("param1");
param1.Cast<SqlParameter>(x => {
// SqlParameter 固有のプロパティに値を設定する
x.ForceColumnEncryption = true;
});
IDataReader インターフェース
Cast<TCommand> メソッド
インターフェースの型を用いてアプリケーションロジックを実装しているとき、データベースプロバイダが提供する具象型のプロパティに値を設定したいことがあります。Cast メソッドは自身を具象型にキャストし、指定された処理を実行します。
- データリーダーの型が IDataReaderWrapper インターフェースを実装する場合、ラップしているデータリーダーをキャストします。ラッパーの仕様によっては、ラップしているデータリーダーを直接操作することが望ましくない場合があります。そのような場合はこのメソッドを使用しないようにしてください。
using var connection = SampleDatabase.CreateConnection();
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = "select 1 as ID, 'abc' as NAME";
using var reader = command.ExecuteReader();
reader.Cast<SqlDataReader>(x =>
{
// SqlDataReader 固有のメソッドを実行する
var idType = x.GetProviderSpecificFieldType(0);
});
}
IDataRecord インターフェース
IsDBNull(string name) メソッド
フィールド名を引数とする IsDBNull メソッドです。
- 非同期メソッドも追加しています。
IDataReader reader = GetData();
var isnull = reader.IsDBNull("id");
Get{値型}(string name) メソッド
フィールド名を引数とする GetValue メソッドです。
- 非同期メソッドも追加しています。
IDataReader reader = GetData();
var field1 = reader.GetValue("field1");
var field2 = reader.GetBoolean("field2");
var field3 = reader.GetString("field3");
Get{値型}OrNull メソッド
指定されたフィールドの値が DBNull である場合、null を返します。
-
戻り値の型はNULL許容型です。GetStringOrNull メソッドの戻り値の型は String 型ですが、null である可能性を示す String? として定義しています。
-
非同期メソッドも追加しています。
IDataReader reader = GetData();
bool? field1 = reader.GetBooleanOrNull("field1");
string? field2 = reader.GetStringOrNull("field2");
Get{値型}OrDefault メソッド
指定されたフィールドの値が DBNull である場合、値型の既定値を返します。
- 非同期メソッドも追加しています。
IDataReader reader = GetData();
bool field1 = reader.GetBooleanOrDefault("field1");
GetStringOrEmpty メソッド
指定されたフィールドの値が DBNull である場合、String.Empty を返します。
- 非同期メソッドも追加しています。
IDataReader reader = GetData();
string field2 = reader.GetStringOrEmpty("field2");
フィルタ
次のインターフェースのメソッドに対してフィルタを適用することができます。フィルタは任意の処理を割り込ませるための仕組みです。
- IDbConnection
- IDbTransaction
- IDbCommand
- IDataParameterCollection
- IDataReader
// フィルタを生成する
var connectionFilters = new IDbConnectionFilter[]{ new SampleConnectionFilter() };
var commandFilters = new IDbCommandFilter[]{ new SampleCommandFilter() };
// コネクションにフィルタを適用する
using var connection = CreateConnection().WithFilter(
connectionFilters: connectionFilters,
commandFilters: commandFilters
);
SampleConnectionFilter クラスは生成されたコマンドに既定値を設定するフィルタです。このフィルタが適用されたコネクションの CreateCommand メソッドは、CommandTimeout プロパティに 60 が設定されたコマンドを返します。
internal class SampleConnectionFilter : DbConnectionFilterBase
{
internal SampleConnectionFilter()
{
}
public override DbConnectionFilterTargets TargetMethods =>
DbConnectionFilterTargets.CreateCommand;
public override IDbCommand CreateCommand(IDbConnection connection, Func<IDbConnection, IDbCommand> continuation)
{
var command = continuation(connection);
command.CommandTimeout = 60;
return command;
}
}
SampleCommandFilter クラスはコマンドを実行するときにログを出力するフィルタです。このフィルタが適用されたコマンドを実行すると、ログが出力されます。
internal class SampleCommandFilter : DbCommandFilterBase
{
internal SampleCommandFilter()
{
}
public override DbCommandFilterTargets TargetMethods =>
DbCommandFilterTargets.ExecuteNonQuery | DbCommandFilterTargets.ExecuteReader | DbCommandFilterTargets.ExecuteScalar;
public override int ExecuteNonQuery(IDbCommand command, Func<IDbCommand, int> continuation)
{
WriteExecutingLog(command);
try
{
var result = continuation(command);
WriteExecutedLog(command);
return result;
}
catch (Exception ex)
{
WriteExceptionLog(command, ex);
throw;
}
}
public override IDataReader ExecuteReader(IDbCommand command, CommandBehavior behavior, Func<IDbCommand, CommandBehavior, IDataReader> continuation)
{
WriteExecutingLog(command);
try
{
var result = continuation(command, behavior);
WriteExecutedLog(command);
return result;
}
catch (Exception ex)
{
WriteExceptionLog(command, ex);
throw;
}
}
public override object ExecuteScalar(IDbCommand command, Func<IDbCommand, object> continuation)
{
WriteExecutingLog(command);
try
{
var result = continuation(command);
WriteExecutedLog(command);
return result;
}
catch (Exception ex)
{
WriteExceptionLog(command, ex);
throw;
}
}
private void WriteExecutingLog(IDbCommand command)
{
// ログを出力する
}
private void WriteExecutedLog(IDbCommand command)
{
// ログを出力する
}
private void WriteExceptionLog(IDbCommand command, Exception exception)
{
// ログを出力する
}
}
DbConnectionFactory クラス
DbConnectionFactory クラスはコネクションを生成するためのファクトリクラスです。フィルタを適用するコネクションを複数回生成する場合、このファクトリクラスを利用してください。フィルタの初期処理で発生するオーバーヘッドを回避することができます。
// フィルタを生成する
var connectionFilters = new IDbConnectionFilter[]{ new SampleConnectionFilter() };
var commandFilters = new IDbCommandFilter[]{ new SampleCommandFilter() };
// フィルタを指定してファクトリを生成する
var factory = new DbConnectionFactory(
connectionFilters: connectionFilters,
commandFilters: commandFilters
);
// コネクションを生成する
using var connection = factory.CreateConnection();
おわりに
この手の機能を提供するライブラリは既に存在すると思いますが、デファクトスタンダードと言えるものを見つけることができませんでしたので開発しました。
データリポジトリにも非同期リポジトリを追加しようと考えています。