ショボイO/R Mapper

リファインシリーズ第2弾。
昔作ったモノを拾い出して、簡易的なO/R Mapperを再構築してみる。
今時ならLINQがあるのだけれど、VS 2005な環境で使う必要があったのと、別にDataSet+TableAdapterでやっても良かったんだけど、もう少しドメインっぽい事をしたかったから…っというのは理由の半分でしかなく、残りの半分は、まあ、遊び(・∀・;)


で、再構築にあたってどんな設計にしたかというと、まず、こんな風に何かをパクッた感じで、入れ物件メタデータとなるクラスを準備。

[Table]
public class Test
{
    [Column(PrimaryKey=true, DbGenerate=true, AutoSync=AutoSync.OnInsert)]
    public int Id { get; set; }

    [Column]
    public string Name { get; set; }

    [Column(DbGenerate=true, AutoSync=AutoSync.OnInsert)]
    public DateTime CreatedAt { get; set; }

    [Column(DbGenerate=true, AutoSync=AutoSync.Always)]
    public DateTime UpdatedAt { get; set; }

    [Column(Version=true, DbGenerate=true, AutoSync=AutoSync.Always)]
    public byte[] VersionNo { get; set; }

    [Updating]
    public void OnUpdating()
    {
        System.Diagnostics.Debug.WriteLine( "Updating" );
    }

    [Deleting]
    public void OnDeleting()
    {
        System.Diagnostics.Debug.WriteLine( "Deleting" );
    }
}

っで、これを以下のMapperクラスに喰わせると、SQLを組み立ててCRUDをしてくれる感じ。

public class Mapper
{
    public INaming Naming { get; set; }

    public Executor Executor { get; set; }

    public T Single<T>(ITransactionContext tx, object id, bool forUpdate) where T : new();
    public T Single<T>(ITransactionContext tx, T key, bool forUpdate) where T : new();

    public List<T> List<T>(ITransactionContext tx, bool forUpdate) where T : new();

    public void Insert<T>(ITransactionContext tx, T entity);
    public int Update<T>(ITransactionContext tx, T entity, bool withVersion);
    public int Delete<T>(ITransactionContext tx, object id, bool withVersion);
    public int Delete<T>(ITransactionContext tx, T key, bool withVersion);

    public long Count<T>();

    public void Call<T>(ITransactionContext tx, T parameter);

    public List<T> ListBy<T>(ITransactionContext tx, Criteria<T> criteria, bool forUpdate) where T : new();
    public int UpdateBy<T>(ITransactionContext tx, Criteria<T> criteria);
    public int DeleteBy<T>(ITransactionContext tx, Criteria<T> criteria);
    public long CountBy<T>(Criteria<T> criteria);
}

overloadのあるメソッドは、引数が一番多いバージョンのみを記載。
まあ、なんか5年以上前のORMっぽい臭いもするわけですが(´・ω・`)


それでも、ページング、VersionNoによる楽観的排他、INSERT時のID取得くらいの機能は実装。
基本、テーブル単位の処理なのでJOINには未対応だけれど、内部の実装は複数テーブルの結合も考慮した構造にしてあるので、対応出来ないことはない感じ。
Criteriaも簡易的なものを容易。
まあ、一覧取得は条件が指定できないと使い物にならないし(・ω・)
その他の項目としては、ストアドへの対応だとか。
ストアド用にはProcedureAttributeだとか、InAttribute、OutAttribute、ReturnAttributeだとか、専用の属性を用意。


で、MapperはメタデータやSqlBuilderを使って処理をまとめるだけで、実際のSQL発行処理は下位レイヤのExecutorを使用する2層構成。
Executorはこんな感じの処理で、自動でできないことはExecutorを使って直接やりましょうという考え(・∀・)

public class Executor
{
    public string ConnectionString { get; set; }

    public IProvider Provider { get; set; }

    public ITransactionContext BeginTransaction(IsolationLevel level);

    public int ExecuteNonQuery(ITransactionContext tx, CommandType cmdType, string cmdText, Parameters parameters);
    public object ExecuteScalar(ITransactionContext tx, CommandType cmdType, string cmdText, Parameters parameters);
    public IDataReader ExecuteReader(ITransactionContext tx, CommandType cmdType, string cmdText, Parameters parameters);
}

ProviderはいわゆるDialect兼DB固有クラスのファクトリで、ここを切り替えれば各DBへの対応が可能…なつもり。
とりあえずSQL Server用のプロバイダしか作ってないけど。
Mapper、Executor自体はImplのみだけど、その手足を切り替えて動作を変更できるような方向で。


まあ、この程度のものでもちょっとした作業程度には使えるデスよ(・ω・)
例えばこんな感じ。

// 安全なInsert or Update
using( ITransactionContext tx = mapper.Executor.BeginTransaction( IsolationLevel.Serializable ) )
{
    Employee employee = mapper.Single<Employee>( tx, 4054, true );
    if( employee == null )
    {
        employee = new Employee();
        employee.No = 4054
        employee.name = "...";
        ...
        mapper.Insert<Employee>( employee );
    }
    else
    {
        employee.name = "...";
        mapper.Update<Employee>( employee );
    }
}

これ、本当はもっとサクッと作るつもりだったんだけど、先週はお休みしたり、今週は別件の調査をやったりとかで、今日まで作業が延びてしまいましたとさ。
さて、次はバッチ処理(ETL、メール送信、タスク制御…)ライブラリのリファインでもしようかしら(・∀・)