ショボイ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、メール送信、タスク制御…)ライブラリのリファインでもしようかしら(・∀・)