簡易ORMフレームワークを作成してみる(11)

データアクセスヘルパー コンポーネント

データアクセスヘルパー コンポーネントは、P&Pなどでも紹介されているデータアクセスの基本的なコンポーネントで、データアクセスを実行する際に必ず処理を経由するコンポーネントです。

主な役割はSQL文の実行だが追加ではあるが重要な機能としてSQL文のロギングや共通の例外ハンドリングの仕組みを提供します。今回はもう少し実践的な仕組みを追加しました。多くの場合、データアクセスはデータ層で行われますが、トランザクション自体はビジネス層で行われます。したがって、DBの接続オブジェクトやトランザクションをどのように引き渡すかが問題になります。ビジネス層で接続オブジェクトを引き回すのは避けたいところです。解決方法としてはFx2.0であればTransactionScopeを利用するアイデアはありますが、DTCを利用したくない場合は別の仕組みが必要になります。他の方法としては、スレッドローカルに保存するやり方があります。この方法は案外上手くいきます。.NETではクラススコープのメンバー変数をスレッドローカルにする仕組みもあり案外簡単です。しかし、メンバー変数を利用するのは柔軟性が欠けるので、もう少し工夫したいところです。
今回はCallContextと呼ばれる、呼び出し関係のある処理間でオブジェクトを共有できる仕組みを利用します。利用法方法はWebのSessionを使うようなイメージと同じで、スロットル名を指定してデータを保持します。このスロット名を一連のデータベース処理を行う1つのセッション名と考えて処理を行います。もちろん、複数のスロット名を指定すれば複数の接続を利用して処理することもでできます。以下のシナリオは、NestContext処理で呼び出しもとのメソッドオープンしたコネクションを利用しています。SqlDataCommandHelperのコンストラクタの引数がスロット名です。

[Test]
public void ネストしたコンテキスト()
{
	DbConnection c;
	using (DataCommandHelper h1 = new SqlDataCommandHelper("DbSession"))
	{
		h1.Open(new SqlConnection(settings.ConnectionString));
		c = h1.Connection;
		NestContext();
		Assert.AreEqual(ConnectionState.Open, c.State);
	}
	Assert.AreEqual(ConnectionState.Closed, c.State);
}

private void NestContext()
{
	using (DataCommandHelper h2 = new SqlDataCommandHelper("DbSession"))
	{
		Assert.AreEqual(ConnectionState.Open, h2.Connection.State);
	}
}

これで、ビジネス層のファサードで開始した接続やトランザクションオブジェクトをデータ層に引き回す必要が無くなります。

作成されたデータアクセスヘルパー コンポーネント

public abstract class DataCommandHelper : IDisposable   
{
	public class Context
	{
		private DbConnection _connection;
		public DbConnection Connection
		{
			get { return _connection; }
			set { _connection = value; }
		}

	private DbTransaction _transaction;
	public DbTransaction Transaction
	{
		get { return _transaction; }
		set { _transaction = value; }
	}
	}

	public DataCommandHelper() : this("DataAccessHelperContext")
	{
	}

	public DataCommandHelper(string contextName)
	{
		this._contextName = contextName;
		if (CallContext.GetData(_contextName) == null)
		{
			_isRootContext = true;
			CallContext.SetData(_contextName, new Context());
		}
	}

	public void Dispose()
	{
		if (IsRootContext)
		{
			if (Transaction != null) Transaction.Dispose();
			if (Connection != null)
			{
				TraceTransaction("Dispose", Connection);
				Connection.Dispose();
			}
			CallContext.FreeNamedDataSlot(ContextName);
		}
	}

	private bool _isRootContext;
	protected bool IsRootContext
	{
		get { return _isRootContext; }
	}

	private string _contextName;
	protected string ContextName
	{
		get { return _contextName; }
	}

	protected Context CurrentContext
	{
		get 
		{
			return (Context)CallContext.GetData(ContextName);
		}
	}


	public DbConnection Connection
	{
		get { return CurrentContext.Connection; }
		protected set { CurrentContext.Connection = value; }
	}

	public DbTransaction Transaction
	{
		get { return CurrentContext.Transaction; }
		protected set { CurrentContext.Transaction = value; }
	}

	protected abstract DbProviderAdapter Adapter
	{
		get;
	}

	public void Open(DbConnection openConnection)
	{
		if (openConnection == null) throw new ArgumentNullException("connection");
		if (Connection != null) throw new InvalidOperationException();
		this.Connection = openConnection;
		try
		{
			TraceTransaction("Open", Connection);
			Connection.Open();
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	public void Close()
	{
		if (Connection != null && Connection.State != ConnectionState.Closed)
		{
			try
			{
				TraceTransaction("Close", Connection);
				Connection.Close();
			}
			catch (DbException ex)
			{
				Exception sqlEx = GetCustomSqlException(ex);
				if (sqlEx != null) throw sqlEx;
				throw;
			}
			finally
			{
				TraceTransaction("Dispose", Connection);
				Connection.Dispose();
				Connection = null;
			}
		}
	}

	public void BeginTransaction()
	{
		BeginTransaction(IsolationLevel.ReadCommitted);
	}

	public void BeginTransaction(IsolationLevel level)
	{
		if (this.Connection == null) 
          throw new ObjectDisposedException(this.GetType().FullName);
		try
		{
			TraceTransaction("BeginTransaction",Connection);
			Transaction = Connection.BeginTransaction(level);
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	public void Commit()
	{
		if (Connection == null) throw new ObjectDisposedException(this.GetType().FullName);
		if (Transaction == null) throw new InvalidOperationException(this.GetType().FullName);
		try
		{
			TraceTransaction("Commit", Connection);
			Transaction.Commit();
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
		finally
		{
			Transaction.Dispose();
			Transaction = null;
		}
	}

	public void Rollback()
	{
		if ( (Connection != null) && (Transaction != null))
		{
			try
			{
				TraceTransaction("Rollback", Connection);
				Transaction.Rollback();
			}
			catch (DbException ex)
			{
				Exception sqlEx = GetCustomSqlException(ex);
				if (sqlEx != null) throw sqlEx;
				throw;
			}
			finally
			{
				Transaction.Dispose();
				Transaction = null;
			}
		}
	}
	public int Fill(DbCommand command, DataTable dataTable)
	{
		return Fill(command, dataTable, 0, 0);
	}

	public int Fill(DbCommand command, DataTable dataTable, 
		int startRecord, int maxRecords)
	{
		if (Connection == null) throw new ObjectDisposedException(this.GetType().FullName);
		try
		{
			DbDataAdapter fillAdapter = Adapter.CreateDataAdapter();
			command.Connection = Connection;
			command.Transaction = Transaction;
			fillAdapter.SelectCommand = command;
			TraceCommand("Fill", command);
			if (startRecord > 0 || maxRecords > 0)
			{
				return fillAdapter.Fill(
                   dataTable.DataSet, startRecord, maxRecords, dataTable.TableName);
			}
			else
			{
				return fillAdapter.Fill(dataTable);
			}
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	public object ExecuteScalarQuery(DbCommand command)
	{
		if (Connection == null) throw new ObjectDisposedException(this.GetType().FullName);
		try
		{
			command.Connection = Connection;
			command.Transaction = Transaction;
			TraceCommand("Scalar", command);
			return command.ExecuteScalar();
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	public int ExecuteCommand(DbCommand command)
	{
		if (Connection == null) throw new ObjectDisposedException(this.GetType().FullName);
		try
		{
			command.Connection = Connection;
			command.Transaction = Transaction;
			TraceCommand("Command",command);
			return command.ExecuteNonQuery();
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	public int ExecuteUpdateRow(DbCommand command, DataRow row)
	{
		if (Connection == null) throw new ObjectDisposedException(this.GetType().FullName);
		try
		{
			command.Connection = Connection;
			command.Transaction = Transaction;
			TraceCommand("UpdateRow", command);
			using (IDataReader reader = command.ExecuteReader(
               CommandBehavior.SequentialAccess))
			{
				do 
				{
					while (reader.Read())
					{
						for (int ii = 0; ii < reader.FieldCount; ii++)
						{
							string columnName = reader.GetName(ii);
							row[columnName] = reader.GetValue(ii);
						}
					}
				} while (reader.NextResult());
				
				reader.Close();
				return reader.RecordsAffected;
			}
		}
		catch (DbException ex)
		{
			Exception sqlEx = GetCustomSqlException(ex);
			if (sqlEx != null) throw sqlEx;
			throw;
		}
	}

	protected virtual void TraceCommand(string actionName, DbCommand command)
	{
		Debug.WriteLine(actionName);
		Debug.WriteLine(command.CommandText);
	}

	protected virtual void TraceTransaction(string actionName, DbConnection dbConnection)
	{
		Debug.WriteLine(actionName);
	}

	protected virtual Exception GetCustomSqlException(DbException ex)
	{
		return null;
	}

}

public class SqlDataCommandHelper : DataCommandHelper
{
	public SqlDataCommandHelper()
	{
	}

	public SqlDataCommandHelper(string contextName) : base(contextName)
	{
	}

	protected override DbProviderAdapter Adapter
	{
		get { return new SqlDbProviderAdapter(); }
	}
}