2009年5月26日火曜日

UnityとEntityのAttach

第2回 スキャフォールディング機能で軽々DB連携アプリケーション - @IT

まだ2回目だけど、楽しみにしてる連載です。今回の記事で少し気になる部分があったので、実際に動くコードを書いてみました。暇人...。いやいや、お勉強デス!

その気になる部分っていうのが、ページ5の「既存のレコードを変更したい今回のようなケースでは、UpdateModelメソッドを使用する必要がある。」という部分。いや、もちろん、ModelBinderのみでDBに保存なんてしないし、これが間違ってるという分けじゃないけど、単純に出来る出来ないという話なら出来るっていう話です。

EntityKey and ApplyPropertyChanges() - Stack Overflow

なんだかんだと、実際にコードを書いてみたわけじゃなく、たんなる耳年増なだけだと、ちょっとカッコ悪し。

efunity2

Home/Indexが一覧。Editを用意。

efunity3

"Chai"を"Chai XXX"に編集。

efunity4

ちゃんと保存されました。

↓これがObjectContextにアタッチされていないエンティティを使って、DB更新してみるコード。

   public void Save<TEntity>(TEntity entity, Func<TEntity, TEntity> setIDFunc) 
where TEntity : new() { var entitySetName = _dataContext.DefaultContainerName + "." + EntitySetName(typeof(TEntity)); setIDFunc(entity); // 空のエンティティをアタッチしておいて、更新情報はクリア _dataContext.AttachTo(entitySetName, setIDFunc(new TEntity())); _dataContext.AcceptAllChanges(); _dataContext.ApplyPropertyChanges(entitySetName, entity); }

オブジェクトのアタッチ (Entity Framework)

AttachToでObjectContextに空のエンティティをアタッチしておいて、ModelBinderで復元したエンティティの値をApplyPropertyChangesで反映させる流れです。ObjectStateManagerを使うともっと綺麗にできるのかも?

↑このコードはRepositoryクラスに。コレを呼び出すControllerコードは↓。

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, Product entity)
    {
      if (ModelState.IsValid)
      {
        _repository.Save<Product>(entity, e =>
        {
          e.ProductID = id;
          return e;
        });
        _repository.SaveChanges();

        return RedirectToAction("Index");
      }

      return View(entity);
    }

認証、認可、入力検証ははしょってるんですが、何となく雰囲気は伝わるかな~、と思います。データベースはNorthwindで、Entity Frameworkを使ってProducts/Categoriesだけのエンティティクラスを作成してます。

efunity

HomeControllerのIndexが一覧ページになるようにしておいて、Editを追加しただけのプロジェクトで試してます。今回はコレに加えてUnityを使ってDIでObjectContextをRepositoryにコンストラクタインジェクションと、RepositoryをControllerにコンストラクタインジェクションさせるようにしてみました。

Unityを使ったControllerFactoryなんかはMvcContribにもコードがあったりするので、そちらを参考にするのが近道です。

でもMvcContribのUnityControllerFactoryはLifetimeManagerを指定しないので、Resolveの度に新しいインスタンスを作ります(TransientLifetimeManager)。HttpContextにObjectContextを入れておいて、同じコンテキストなら無駄使いしないようにするためのHttpContextLifetimeManagerクラスを用意。「ASP.NET MVC Tip: Dependency Injection with Unity Application Block - Shiju Varghese's Blog」に書かれてる、HttpContextLifetimeManagerを使わせてもらいました。Get/Set/RemoveのoverrideでHttpContextを使うようにしてるだけですね。

ObjectContextのインスタンスがどうなってるのかを確認するコードをHomeControllerに書いて確認。

    INorthwindRepository _repository;

    public HomeController(INorthwindRepository repository)
    {
      _repository = repository;
      _repository.Debug("Constructor");
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _repository.Debug("OnActionExecuting");
      base.OnActionExecuting(filterContext);
    }

    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      _repository.Debug("OnActionExecuted");
      base.OnActionExecuted(filterContext);
    }

    public ActionResult Index()
    {
      var httpApp = HttpContext.ApplicationInstance as IUnityContainerAccessor;
      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-1");

      var list = _repository.All<Product>();

      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-2");

      return View(list);
    }

Debug内ではObjectContext.GetHashCode()を出力するようにしてます。コンストラクターで渡されるRepositoryはUnityに任せたもので、OnActionExecutingとOnActionExecutedではコンストラクタで渡されたものを出力し、Index内でそれぞれ(Other-1/2)新しくリポジトリのインスタンスを作成(Resolve)して出力させてます。

TransientLifetimeManagerを使った出力。

Constructor:6658142
OnActionExecuting:6658142
Other-1:5603269
Other-2:50559794
OnActionExecuted:6658142

Otherはそれぞれ違うインスタンスが生成されてますね。

HttpContextLifetimeManagerを使った出力。Other-1/2は全然違うのが出力されますね。

Constructor:60183783
OnActionExecuting:60183783
Other-1:60183783
Other-2:60183783
OnActionExecuted:60183783

Otherも同じインスタンスです。お利口さんです。Global.asaxに以下のコードを追加してUnityに登録してます。

    void InitializeContainer()
    {
      if (_container == null)
        _container = new UnityContainer();

      IControllerFactory controllerFactory = new UnityControllerFactory(_container);
      ControllerBuilder.Current.SetControllerFactory(controllerFactory);

      // Register
      _container.RegisterType<NorthwindEntities>(
        /*
         * ContainerControlledLifetimeManagerを使うとSingleton。
         * デフォルトはTransientLifetimeManager
         */
          new HttpContextLifetimeManager<NorthwindEntities>()
                )
                .Configure<InjectedMembers>()
                .ConfigureInjectionFor<NorthwindEntities>(
                  new InjectionConstructor(
                    ConfigurationManager.ConnectionStrings["NorthwindEntities"].ConnectionString
                    )
                );
      _container.RegisterType<INorthwindRepository, NorthwindRepository>();
    }

とりあえず、今回はProductデータだけを利用したけど、他にもいろいろなテーブルを同じコードで簡単に処理できるように「An Irishman Down Under - Polymorphic Repository for ADO.Net Entity Framework - Keith Patton's blog」に書かれてるようなジェネリックなリポジトリを書いてみようかな~と試してみたんですが、どうですかね。あんまり便利な気がしない。簡単な機能実装ならいいかもしれないけど、この辺は機能ドメイン毎にちゃんと書いた方がいい気がする。ところで、EntitySet名ってエンティティクラスから簡単に取得する方法ってないんですかね。EntityKeyには入ってるみたいだけど、アタッチして無いとnullで取り出せないじゃないですか。今回はずるっこして、エンティティクラス名+"s"として生成してます。

んん~。今回もプロジェクト添付しておくので、動かしてみたい方はどーぞー。あ、データベースファイルは添付してないです。