Azureはじめました

Windows Azureで業務システムを組んでみる日記

Relationが定義されたエレメントツリーごと履歴に保存したい

業務プログラムだとMaster-Detailモデルのエンティティの更新履歴を保存したいなんてのは良くある話。
そこはDBのTriggerあたりを使ってやるのも手ではあるんだけど、もうちょっと手軽にやる方法は無いもんかと。

       |
   \  __  /
   _ (m) _ピコーン
      |ミ|
   /  .`´  \
     ∧_∧  / ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄
    (・∀・∩< モデルごとシリアライズしてテキストで保存しよう
    (つ  丿 \_________
    ⊂_ ノ
      (_)

というはなし。

public class Master{
  public int id {get;set;}
  public DateTime date{get;set;}
  public virtual ICollection<Detail> details{get;set;}
}

public class Detail{
  public int id{get;set;}
  public int master_id{get;set;}
  public string item{get;set;}
  public decimal count{get;set;}
  public decimal uop {get;set;}
  public decimal total{get;set;}
  public decimal tax {get;set;}

  public Master master{get;set;}
}  

こんなかんじの良くあるMaster-Detail。
相互にmaster-detailsで参照関係にある。

  public static void main(string[] args){
    using (AnyDBEntities db = new AnyDBEntities(){

        var master = new Master(){
            id=1,
            date=DateTime.Today,
        };
        master.details.Add(
            new Details(){
                id=1,
                master_id=1,
                item="coke",
                count=1,uop=100,total=100,tax=8,
            }
        );
        master.details.Add(
            new Details(){
                id=2,
                master_id=1,
                item="Humberger",
                count=1,uop=300,total=300,tax=24,
            }
        );
        db.Master.Add(master);
        db.saveChanges();
    }

こんな感じで。

データ更新時に自動で履歴を作成するための切り口を作る

データ更新はSaveChanges()/SaveChangesAsync()で行われるので、ここをオーバーライドし変更のあったエンティティを拾い上げて処理するようにしよう。
EFで作成されるEntitiyコンテナはpartial classなので、外側に別枠でクラスを作って拡張する。

public partial class AnyDBEntities {
  public override int SaveChanges(){
    var changedEntities = ChangeTracker.Entries().ToList();
    foreach (var changedEntity in changedEntities) {
        //ここで更新前の処理
    }

    int res = base.SaveChanges();
     
    foreach (var changedEntity in changedEntities) {
        //ここで更新後の処理
    }
  }
}

こんな感じで変更されたエンティティを拾って処理する。


この時の処理の移譲先をどうするかは設計次第だけど、今回はモデル自身に後処理を移譲してみる。

DB DrivenのEFでモデルの継承元を作る

モデルデザイナで継承元を指定してもいいんだけど、一律で設定する場合

これの応用でT4テンプレートを変更する。

対象となるのは.ttの以下の部分

    public string EntityClassOpening(EntityType entity)
    {
        return string.Format(
            CultureInfo.InvariantCulture,
            "{0} {1}partial class {2}{3}",
            Accessibility.ForType(entity),
            _code.SpaceAfter(_code.AbstractOption(entity)),
            _code.Escape(entity),
            ":ModelBase"); // ←ここ
			//_code.StringBefore(" : ", _typeMapper.GetTypeName(entity.BaseType))
    }

で、移譲先になるメソッドを基底クラスに定義しておく

public class ModelBase{
  public virtual void OnBeforeSave(EntityState state,AnyDbEntities entity){}
  public virtual void OnAfterSave(EntityState state,AnyDbEntities entity){}
}

このメソッドを前後処理が必要なエンティティでオーバーライドすればOK

移譲
public partial class AnyDBEntities {
  public override int SaveChanges(){
    var changedEntities = ChangeTracker.Entries().ToList();
    foreach (var changedEntity in changedEntities) {
        if (changedEntity.Entity is ModelBase) {
           ((ModelBase)changedEntity.Entity).OnBeforeSave(
                changedEntity.State, this
           );
        }
        //ここで更新前の処理
    }

    int res = base.SaveChanges();
     
    foreach (var changedEntity in changedEntities) {
        //略
    }
  }
}

さっくり。

こいつをJSONにしてみる

リアライザは今イチオシのJil*1を。

public class Master{
  public int id {get;set;}
  public DateTime date{get;set;}
  public virtual ICollection<Detail> details{get;set;}

  public override void OnAfterSave(EntityState state,AnyDbEntities entity){
    var text = Jil.JSON.Serialize<Master>(this);
    
    //textをどこかに保存
    log4net.LogManager.GetLogger(typeof(Master)).info(text);
  }
}

これを実行してみると例外が発生

Jil.InfiniteRecursionException 
Master>Detail>Master>Detail...

oh...

この場合Detail.masterはシリアライズの必要無いのでAnnotationで出力を抑制

public class Detail{
  public int id{get;set;}
  public int master_id{get;set;}
  public string item{get;set;}
  public decimal count{get;set;}
  public decimal uop {get;set;}
  public decimal total{get;set;}
  public decimal tax {get;set;}
  
  [Jil.JilDirective(Ignore=true)]
  public Master master{get;set;}
}  

これでOK

と、ここまでやってはみたものの

結構面倒くさい問題が発生して悩みは深まる(;´д`)

1. JilDirectiveの記述場所

データ・ドリブンでEF使ってるとエンティティはT4テンプレートで自動生成される。
このエンティティへ外部からアノテーションを追加するのはここ*2 でやったとおり。

上の例みたいに1:nの1側なら問題は無いんだけど、n側は

  public virtual ICollection<type> ...

と仮想メソッドで宣言されてる。

仮想メソッドは仮想なのでを上の方法では上書きできず、さりとて自動生成されるソースをいちいち手直しするわけにもいかず。
(;´д`)

2. Jil特有の問題

Supported Types

Jil will only (de)serialize types that can be reasonably represented as JSON.

The following types (and any user defined types composed of them) are supported:

  • Strings (including char)
  • Booleans
  • Integer numbers (int, long, byte, etc.)
  • Floating point numbers (float, double, and decimal)
  • DateTimes & DateTimeOffsets

Note that DateTimes are converted to UTC time to allow for round-tripping, use DateTimeOffsets if you need to preserve timezone information
See Configuration for further details

  • TimeSpans

See Configuration for further details

  • Nullable types
  • Enumerations

Including [Flags]

  • Guids

Only the "D" format

  • IList implementations
  • IDictionary implementations where TKey is a string or enumeration

と、ICollectionが対象外なのでDeserializeで戻すことができない。
これをDeserializeDynamicで代用できるならそれでOKだけど…


~追記するかも~