はじめに
Dapper で DB 接続クラスを作ってみました。
環境
- Visual Studio Community 2022
- .NET Framework 4.8.1
- Dapper 2.1.72
ソース
小~中規模の想定で機能をひとつにひっくるめています。
DbSession.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using Microsoft.Extensions.Logging;
namespace DapperExampleNet481
{
/// <summary>
/// データベースセッションを表すインターフェイスです。
/// </summary>
public interface IDbSession : IDisposable, IAsyncDisposable
{
/// <summary>
/// トランザクションを開始します。
/// </summary>
void BeginTransaction();
/// <summary>
/// トランザクションをコミットします。
/// </summary>
void CommitTransaction();
/// <summary>
/// トランザクションをロールバックします。
/// </summary>
void RollbackTransaction();
/// <summary>
/// SQL クエリを実行し、結果を動的型で取得します。
/// </summary>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果を動的型で返します。</returns>
IEnumerable<dynamic> Get(string sql, object param = null);
/// <summary>
/// SQL クエリを非同期で実行し、結果を動的型で取得します。
/// </summary>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果を動的型で返します。</returns>
Task<IEnumerable<dynamic>> GetAsync(string sql, object param = null);
/// <summary>
/// SQL クエリを実行し、結果を指定された型で取得します。
/// </summary>
/// <typeparam name="T">取得する型。</typeparam>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果を指定された型で返します。</returns>
IEnumerable<T> Get<T>(string sql, object param = null);
/// <summary>
/// SQL クエリを非同期で実行し、結果を指定された型で取得します。
/// </summary>
/// <typeparam name="T">取得する型。</typeparam>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果を指定された型で返します。</returns>
Task<IEnumerable<T>> GetAsync<T>(string sql, object param = null);
/// <summary>
/// SQL クエリを実行し、最初の行を動的型で取得します。
/// </summary>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果の最初の行を動的型で返します。</returns>
dynamic GetFirstOrDefault(string sql, object param = null);
/// <summary>
/// SQL クエリを非同期で実行し、最初の行を動的型で取得します。
/// </summary>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果の最初の行を動的型で返します。</returns>
Task<dynamic> GetFirstOrDefaultAsync(string sql, object param = null);
/// <summary>
/// SQL クエリを実行し、最初の行を指定された型で取得します。
/// </summary>
/// <typeparam name="T">取得する型。</typeparam>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果の最初の行を指定された型で返します。</returns>
T GetFirstOrDefault<T>(string sql, object param = null);
/// <summary>
/// SQL クエリを非同期で実行し、最初の行を指定された型で取得します。
/// </summary>
/// <typeparam name="T">取得する型。</typeparam>
/// <param name="sql">実行する SQL クエリ。</param>
/// <param name="param">SQL クエリのパラメータ。</param>
/// <returns>SQL クエリの実行結果の最初の行を指定された型で返します。</returns>
Task<T> GetFirstOrDefaultAsync<T>(string sql, object param = null);
/// <summary>
/// SQL コマンドを実行し、処理した行数を返します。
/// </summary>
/// <param name="sql">実行する SQL コマンド。</param>
/// <param name="param">SQL コマンドのパラメータ。</param>
/// <returns>処理した行数を返します。</returns>
int Execute(string sql, object param = null);
/// <summary>
/// SQL コマンドを非同期で実行し、処理した行数を返します。
/// </summary>
/// <param name="sql">実行する SQL コマンド。</param>
/// <param name="param">SQL コマンドのパラメータ。</param>
/// <returns>処理した行数を返します。</returns>
Task<int> ExecuteAsync(string sql, object param = null);
}
/// <summary>
///データベースセッションを表すクラスです。
/// </summary>
public class DbSession : IDbSession
{
private static readonly string s_exceptionMessage = "Exception occurred. SQL: {Sql} Params: {@Params}";
private static readonly string s_sqlExecutingMessage = "Executing SQL. SQL: {Sql} Params: {@Params}";
private static readonly string s_sqlCompletedMessage = "SQL execution completed. Result: {Result} SQL: {Sql} Params: {@Params}";
private readonly IDbConnection _connection;
private readonly int _commandTimeout;
private readonly ILogger<DbSession> _logger;
private IDbTransaction _transaction;
/// <summary>
/// DbSession クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="connection">データベース接続オブジェクト。</param>
/// <param name="commandTimeout">コマンドのタイムアウト時間(秒)。</param>
/// <param name="providerName">データベースプロバイダーの名前。</param>
/// <param name="logger">ロガーオブジェクト。</param>
public DbSession(
IDbConnection connection,
int commandTimeout,
ILogger<DbSession> logger = null)
{
_connection = connection;
_commandTimeout = commandTimeout;
_logger = logger;
}
/// <inheritdoc />
public void BeginTransaction()
{
try
{
// もし接続されていない状態でトランザクションを開始した場合、
// 以下の例外が発生する。
// トランザクションを開始する前にデータベース接続を開いておく。
// ------------------------------------------------------------
// System.InvalidOperationException:
// '操作が無効です。接続は閉じています。'
// ------------------------------------------------------------
if (_connection.State != ConnectionState.Open)
{
_connection.Open();
}
// サブトランザクション(ネストされたトランザクション)を防ぐため、
// トランザクションが既に存在する場合は、新しいトランザクションを開始しない。
// サブトランザクションはデータベース毎に仕様が異なること、
// またトランザクションの管理自体が煩雑になるため、禁止とする。
if (_transaction == null)
{
_transaction = _connection.BeginTransaction();
}
}
catch (Exception ex)
{
LogException(ex);
throw;
}
}
/// <inheritdoc />
public void CommitTransaction()
{
try
{
if (_transaction != null)
{
_transaction.Commit();
_transaction.Dispose();
_transaction = null;
}
}
catch (Exception ex)
{
LogException(ex);
throw;
}
}
/// <inheritdoc />
public void RollbackTransaction()
{
try
{
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = null;
}
}
catch (Exception ex)
{
LogException(ex);
throw;
}
}
/// <inheritdoc />
public IEnumerable<dynamic> Get(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return _connection.Query(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public async Task<IEnumerable<dynamic>> GetAsync(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return await _connection
.QueryAsync(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout)
.ConfigureAwait(false);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public IEnumerable<T> Get<T>(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return _connection.Query<T>(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public async Task<IEnumerable<T>> GetAsync<T>(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return await _connection
.QueryAsync<T>(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout)
.ConfigureAwait(false);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public dynamic GetFirstOrDefault(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return _connection.QueryFirstOrDefault(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public async Task<dynamic> GetFirstOrDefaultAsync(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return await _connection
.QueryFirstOrDefaultAsync(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout)
.ConfigureAwait(false);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public T GetFirstOrDefault<T>(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return _connection.QueryFirstOrDefault<T>(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public async Task<T> GetFirstOrDefaultAsync<T>(string sql, object param = null)
{
try
{
// 接続が Closed の場合、内部で自動的に Open される。
return await _connection
.QueryFirstOrDefaultAsync<T>(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout)
.ConfigureAwait(false);
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public int Execute(string sql, object param = null)
{
try
{
LogSqlExecuting(sql, param);
// 接続が Closed の場合、内部で自動的に Open される。
var result = _connection.Execute(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout);
LogSqlCompleted(result, sql, param);
return result;
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public async Task<int> ExecuteAsync(string sql, object param = null)
{
try
{
LogSqlExecuting(sql, param);
// 接続が Closed の場合、内部で自動的に Open される。
var result = await _connection
.ExecuteAsync(
sql: sql,
param: param,
transaction: _transaction,
commandTimeout: _commandTimeout)
.ConfigureAwait(false);
LogSqlCompleted(result, sql, param);
return result;
}
catch (Exception ex)
{
LogException(ex, sql, param);
throw;
}
}
/// <inheritdoc />
public void Dispose()
{
try
{
// トランザクションが残っている場合、ロールバックして破棄する。
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = null;
}
// 接続を破棄する。
_connection.Dispose();
}
catch (Exception ex)
{
// 例外ログを出力する。
LogException(ex);
// Dispose メソッド内で例外が発生しても throw しない。
// throw;
}
finally
{
GC.SuppressFinalize(this);
}
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
try
{
// トランザクションが残っている場合、ロールバックして破棄する。
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = null;
}
// 接続を破棄する。
if (_connection is IAsyncDisposable asyncConnection)
{
await asyncConnection.DisposeAsync().ConfigureAwait(false);
}
else
{
_connection.Dispose();
}
}
catch (Exception ex)
{
// 例外ログを出力する。
LogException(ex);
// Dispose メソッド内で例外が発生しても throw しない。
// throw;
}
finally
{
GC.SuppressFinalize(this);
}
}
/// <summary>
/// 例外ログを出力します。
/// </summary>
private void LogException(Exception ex, string sql = null, object param = null)
{
try
{
if (_logger == null)
{
return;
}
_logger.LogError(ex, s_exceptionMessage, sql, param);
}
catch
{
// ログ出力に失敗しても処理を継続する。
// throw;
}
}
/// <summary>
/// SQL 実行ログを出力します。
/// </summary>
private void LogSqlExecuting(string sql, object param = null)
{
try
{
if (_logger == null)
{
return;
}
_logger.LogTrace(s_sqlExecutingMessage, sql, param);
}
catch
{
// ログ出力に失敗しても処理を継続する。
// throw;
}
}
/// <summary>
/// SQL 完了ログを出力します。
/// </summary>
private void LogSqlCompleted(int result, string sql, object param = null)
{
try
{
if (_logger == null)
{
return;
}
_logger.LogTrace(s_sqlCompletedMessage, result, sql, param);
}
catch
{
// ログ出力に失敗しても処理を継続する。
// throw;
}
}
}
}
おわりに
細かく動作確認はしていません。