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"として生成してます。

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

2009年5月23日土曜日

イケメンはしゃべりも上手い

久しぶりにアメージングで試合。最近になってゴーリーが誰一人これなくなり、代わりにオグさんがやるらしく、プレーヤーが足りなくなる。ほら、ビシッと決めれるプレーヤーがいないと、あれじゃないっすか。

今期になって色々あって全然試合にも出れず、チームにも迷惑をかけてたな。なんてことは、これっぽっちも思ってないんだけどさ。だって普通に勝ち越してるし。いてもいなくても勝敗に関係ないじゃん...。でも前期のギリギリプレーオフに比べれば、勝ち越してる今期のほうが全然気が楽だから、小さい事は気にしない。

9:30開始だから心が折れそうだったけど、試合が久しぶりだったからかワクワクしすぎて目覚ましより早く起きれた。ちょっと嬉しい。今日はいい感じでプレーできる気がする。そんな気がするときはたいていいい感じでプレーできないんだけどね!

レフェリーがタカチさんでなんか不安だったけど試合開始。で、一点目を決めたんだけど、タカチさんなかなか笛を吹かず、こっち見てる。あれ?入ってるよ?なんで認めないんだ!早く笛を吹いてオレの得点だとコールしたまえよ!イヤそうな顔しながらしょうがなくコールするレフェリー...。久しぶりなんだからもう少し優しくして...。

2点目をセト君のが決めたんだけど、ここで事件発生。セト君は開始時間に間に合って無くて、背番号空白でロースターだしてたのをすっかり忘れてた。あちゃ~。ベンチマイナーで無駄にキルプレーっす。それは自分の責任なのでしっかりキープして時間を使い守りきれました。あぶね~。

でも、なんやかんやと前半結構点を取られまして。そもそもシュートを打たせたらダメなのに、守り方間違えてた。難しいな~。前半終わったところで2-4。負けてるし。後半しっかり攻めて点を取ればいいから、これ以上失点さえしなければ逆転できる。気楽に考えてたけど、アメージングが久しぶりなのをすっかり忘れてたよ。滑っても滑っても全然スピードにのらない。ウィールがぼろいのもあるけど、滑りかたが戸塚の堅いリンク用の滑りかたになってたみたい。全然相手を振り切れない滑りになってて、無駄に疲れた。ちょっと不安になってきた。

でもそんな不安をシンゴが吹き飛ばしてくれた。前回の試合もそうだったらしいんだけど、あれよあれよと追いつき逆転。シンゴ...。君はそんなしっかり者のプレースタイルじゃなかったじゃないか。ここぞというときに外すのがシンゴスタイルじゃなかったのか?どうやら、たけはら不在の間に、チームを引っ張ってたのはシンゴだったみたいね。おぬし、なかなかのもんだな。それに引き替えたけはらさんは、足で蹴り入れたんじゃないかという疑惑のゴールがあったり。でも、相手のペナルティーによるパワープレーで疑惑を晴らすかのようなディフェンスラインからのスラップで名誉挽回(汚名返上?)。

最終的に相手チームのスタミナ切れで何とか逃げ切って勝てたけど、内容的には反省点の多い試合でした。次回以降もう少し丁寧に大人のホッケーをしないとね。

そうそう、マリちゃんとは初めて一緒に試合出たけど、今日の試合ではマリちゃん含め、なんと全員ポイント!ミカさんもイマイちゃんも、意外なことにミチも!全員にポイントが付く試合はあんまりないけど、それが出来た時は一番嬉しい。最後にちょっとイマイちゃんがケガしちゃって、最後までプレー出来なかったが残念でした。

話は変わるんだけど5/20にボーランドソリューションセミナーっていうのがあってそれに行ってみた。無料だったし。でも、個人参加は一人だけで、少し場違いな気がしなくもなく...。テストツールに興味があったからというより、ケースケがしゃべるってことなんで行ってみた。まるで追っかけ。ファンにはたまらない。無料イベント最高!いやいや、そうじゃなくて、ケースケがしゃべる内容がRIAってことだったんで行ったわけです。RIAの話もリサーチした数値の説明があって現状どういうとらえ方をしてるのかを分かりやすく説明してくれたり、今後もっといろんなデバイスにRIAを展開していく話とか、デザイナーとデベロッパー間での効率を良くするためのCatalystのデモをやったりと盛りだくさんで面白かったよ。でも、一番面白かったのは市ヶ谷の駅前で迷子になって地図とにらめっこしてるたけはらさんをケースケが見つけて会場までつれてってくれたところだね!ちなみに駅前迷子の前に、電車の乗り換え間違えて全然違う方向に向かってて途中慌てて乗り換えてなんとかたどり着いたという事件があったりなかったり。最近3回東急東横線に乗ったんだけど、そのうち2回電車が遅れるということもあったりなかったり。電車って怖い...。

2009年5月13日水曜日

HandleErrorの使い方

そういえば、今月に入って全然エントリを書いてなかった事に気がついた。人に言われて気がついた。ずっと本を読んでて、頭がプシューってなっててさ~。

そんなこんなで、ASP.NET MVCでHandleErrorを使って、HTTPステータスコードに合わせたエラーページを用意する方法が日本語情報として無いかもとふと思ったので書いておきます。

使い方はアクションにHandleError属性を指定して、web.configのcustomErrorsのモードをOn/RemoteOnlyに設定しておけば、defaultRedirectが表示されるというものです。

  [HandleError]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

コントローラに指定しても、アクションに指定してもいいですよね。それぞれに指定した場合、コントローラの普通はHandleErrorが優先されます。 例えば、上記コントローラに以下のアクションを追加してみると確認できますね。

    [HandleError(ExceptionType=typeof(InvalidOperationException),View="Exception")]
    public ActionResult ThrowException()
    {
      throw new InvalidOperationException("ぶぶ~");
    }

ViewにThrowExceptionへのリンクを作成して表示させてみたのが↓こちら。

handleerror2

普通に標準のエラーページが表示されます。ちゃんとアクションのHandleErrorにはViewを指定しているにもかかわらず。これをアクションに指定してるほうを優先させるにはコントローラに指定してるHandleErrorの優先順位を下げればOKです。

  [HandleError(Order=1)]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

↑こんな感じで。Orderは0スタートなので1以上なら何でもいいです。そうすると今度はどういう表示になるか確認。

handleerror1 

グレート!

ちなみにリリース版のVSテンプレートが出力する~/Views/Shared/Error.aspxでは例外情報の表示コードが無くなってるので分かりにくいですが、このページは少し特殊でInheritsが単なるViewPageじゃなく、例外情報が渡されるように型付きViewPageになってます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %>

なので、Viewページ内でModelを参照するとアクション内で発生したExceptionが参照出来るようになってます。あら便利。自分でエラーページを定義する場合のInheritsを上記のように変更しておけば、自動で参照できるという寸法です。

ちなみに上記スクリーンショットのViewは~/Views/Shared/Exception.aspxというのを作成して表示してます。

で、ですね、例外を捕捉して専用エラーページを表示するのももちろん必要だと思うんだけど、そこはやっぱりHTTPステータスコードに合わせたエラーページを定義して、レスポンスのステータスコードも合わせた方がサーチエンジンにもエラーが認識できるし、RESTfulな感じがして素敵だと思いませんか。例えば、上記のページの場合レスポンスされるステータスコードはなんになってるか確認してみる。

handleerror3

500ですね。例外だからそれで良し。でも、例えばidを指定してDB検索して一致するデータがない、なんて時は404で返したいな~、なんて思いませんか。

試しに、web.configのcustomErrorsを以下のように変更。

    <customErrors mode="On" defaultRedirect="Errors">
      <error statusCode="400" redirect="/Errors/400" />
      <error statusCode="403" redirect="/Errors/403" />
      <error statusCode="404" redirect="/Errors/404" />
    </customErrors>

で、HomeControllerに以下のコードを追加。

    public ActionResult BadRequest()
    {
      throw new HttpException(400, "Bad Request");
    }

    public ActionResult Forbidden()
    {
      throw new HttpException(403, "Forbidden");
    }

    public ActionResult NotFound()
    {
      throw new HttpException(404, "Not Found");
    }

ErrorsControllerを追加して、HTTPステータスコードに合わせてViewを返すようにします。

  public class ErrorsController : Controller
  {
    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);

      return View(viewName);
    }
  }

handleerror4 

で、Home/BadRequestにアクセスしたのが↑。拡大すると見えるけど、200が返ってます。400が返って欲しい。そんなときはViewResultを書き換えてしまいましょう。

  public class StatusViewResult : ViewResult
  {
    public int StatusCode { get; set; }

    public StatusViewResult(ViewResult viewResult)
    {
      ViewName = viewResult.ViewName;
      MasterName = viewResult.MasterName;
      ViewData = viewResult.ViewData;
      TempData = viewResult.TempData;
    }

    public override void ExecuteResult(ControllerContext context)
    {
      context.HttpContext.Response.StatusCode = StatusCode;
      
      base.ExecuteResult(context);
    }
  }

こんな感じのクラスを作成しておいて、Errorsコントローラでこっちを利用するように変更。

    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);
      var resStatusCode = statusCode ?? 500;

      return new StatusViewResult(View(viewName)) {
        StatusCode = resStatusCode
      };
    }

で、同じアクションを実行したのが↓こちら。

handleerror5 

うへへ。ちゃんと400が返ってます。そりゃそーだ。

エラー用のViewとして400/403/404の3種類だけ用意してみました。

handleerror6

それぞれの実行結果は省略しときますが、例えばこの状態で405とかエラー発生させたらどうでしょうね。HomeControllerに以下を追加。

    public ActionResult MethodNotArrowed()
    {
      throw new HttpException(405, "Method Not Arrowed");
    }

これにアクセスしてみたのが↓こちら。

handleerror7

ErrorsControllerのIndexアクションでError405をViewとして返そうとするけど、そんなViewは定義してないのでInternal Server Errorが発生して、テンプレートで用意されてる~/Views/Shared/Error.aspxがレンダリングされてます。すばらしいじゃないですか。

と、いうわけで、今回のプロジェクト一式もダウンロード出来るようにしておきます。

最近こんな話題ばっかりで、周りからはブログつまらないという声もちらほら。しょうがないので前に書いてた今は無き「オレがルールだ!」からサルベージしてごまかそうかと計画中。

2009年4月30日木曜日

VirtualPathProviderを使ってデータベースからViewを読み込む

Scripting ASP.NET MVC Views Stored In The Database

PhilさんのブログではIronRubyのViewEngineを使って、データベースから読み込んだViewを出力するサンプルが公開されてたけど、そこはシンプルに普通のASPXの出力が欲しいところ。でも、そもそもファイルの実体が無くても処理出来るということに驚きですが。

以前ViewEngineの実装をしようとしてて、実体がないと上手く行かないな~、っていう理由で諦めたんですがVirtualPathProviderを使う事で解決出来るんですね。ASP.NETそのものの仕様をきちんと理解してなかったです...。

ASP.NET MVCのソースをみると、標準のViewEngine(WebFormViewEngine)はVirtualPathProviderを含むVirtualPathProviderViewEngineから派生し、WebFormViewをインスタンス化する際にBuildManager.CreateInstanceFromVirtualPathを呼び出してます。

VirtualPathProviderっていうのが、パスの指し示す場所にあるファイル(VirtualFile)かディレクトリ(VirtualDirectory)を返す役割を実装します。ファイルはVirtualFile.Openでストリームを返せばいいので、そのストリームをローカルHDDから読み込もうとDBからだろうと、なんかしら別のサービスから取得(なんならHTTPで別サーバーから)取得しようがお構いなしでOKっていうグレートなクラスになってます。

今回この機能を使って、特に変わったViewEngineではなくWebFormViewEngineをそのまま利用して、VirtualPathProviderがデータベース参照するようにしたものを作ってみました。

まずはどこから手をつけていい物やらよく分からないのでPhilさんのサンプルを眺めるところから。が、ViewEngineが違うからコードが少し多いし、ScriptRuntime使うからなんかちょっと複雑。ふにゅ~。

とりあえず、まずはデータベース作ろう。んで、Repository書いてしまおう。

fromdb1

fromdb2

こんな感じのテーブルでLINQ to SQLを使ってモデルを作成。 Idはオートナンバーの主キー。イヤマジでそれじゃおかしい使い方をしてるんだけど気にしない。 ViewNameが名前。ホントはコレが主キーデスね。 VirtualPathが仮想パス。そのままかよ!ここにパスを入れておいて、そのパスへのアクセスを横取りしてDBからファイル内容を返します。 Contentsにファイル内容。ChangeStampは使ってないけど癖で入れちゃってます。

namespace FromDB.Core
{
public interface IFromDBRepository : IDisposable
{
  IQueryable<VirtualView> All();
  VirtualView GetById(int id);
  VirtualView GetByViewName(string viewName);
  VirtualView GetByVirtualPath(string virtualPath);
  void Create(VirtualView model);
  void SubmitChanges();
}
}
リポジトリはこんなインターフェース。単純に全部取得、1件取得(3パターン)、新規、保存だけね。
  public class FromDBRepository : IFromDBRepository
{
  FromDBDataContext _context;

  public FromDBRepository() : this(new FromDBDataContext()){}
  public FromDBRepository(FromDBDataContext context)
  {
    _context = context;
  }

  public void Dispose()
  {
    if (_context != null)
      _context.Dispose();
  }

  public IQueryable<FromDB.Models.VirtualView> All()
  {
    return from view in _context.VirtualViews
           orderby view.Id
           select view;
  }

  public FromDB.Models.VirtualView GetById(int id)
  {
    return All().Where(v => v.Id == id).FirstOrDefault();
  }

  public FromDB.Models.VirtualView GetByViewName(string viewName)
  {
    return All().Where(v => v.ViewName == viewName).FirstOrDefault();
  }

  public FromDB.Models.VirtualView GetByVirtualPath(string virtualPath)
  {
    return All().Where(v => v.VirtualPath == virtualPath).FirstOrDefault();
  }

  public void Create(VirtualView model)
  {
    _context.VirtualViews.InsertOnSubmit(model);
  }

  public void SubmitChanges()
  {
    _context.SubmitChanges();
  }
}
今回もDIは使ってないデス。とりあえず、ココまで出来た段階でHomeControllerをガッツリ書き換えて、このデータを編集する機能にしてしまいます。コードは省略。ダウンロードしてから見てくだい。

fromdb3

↑Indexが一覧。

fromdb4 fromdb5

新規と編集が出来るように。コードエディタはPhilさんみたいにカッコイイのを使わずに、Html.TextAreaでごまかす。いいじゃん! そうそう、このやり方だと、Htmlタグをポストすることになるので、POSTを受け取るアクションのActionFilterで[ValidateInput(false)]を指定するのを忘れずに。 さてさて、ここまでは普通のASP.NET MVCでの作業。ここからVirtualPathProviderの実装に移ります。すったもんだの末、以下の2つがあればいいことが分かりました。 まずはVirtualPathProviderを派生させたクラス。
  public class FromDBVirtualPathProvider : VirtualPathProvider
{
  private VirtualView GetVirtualView(string virtualPath)
  {
    VirtualView vv;
    using (var rep = new FromDBRepository())
    {
      vv = rep.GetByVirtualPath(VirtualPathUtility.ToAppRelative(virtualPath));
    }

    return vv;
  }

  private bool IsVirtualPath(string virtualPath)
  {
    var path = VirtualPathUtility.ToAppRelative(virtualPath);
    return path.StartsWith("~/FromDBViews/", StringComparison.InvariantCultureIgnoreCase);
  }

  public override bool FileExists(string virtualPath)
  {
    if (IsVirtualPath(virtualPath))
      return GetVirtualView(virtualPath) != null;
    else
      return base.FileExists(virtualPath);
  }

  public override VirtualFile GetFile(string virtualPath)
  {
    if (IsVirtualPath(virtualPath))
    {
      var vv = GetVirtualView(virtualPath);

      return new FromDBVirtualFile(virtualPath, vv.Contents);
    }
    else
    {
      return base.GetFile(virtualPath);
    }
  }

  public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
  {
    if (IsVirtualPath(virtualPath))
      return null;
    else
      return base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
  }
}
必要なのはFileExistsとGetFileとGetCacheDependency。GetCacheDependencyをオーバーライドしとかないと、実体フォルダもファイルもないのに、ファイルの書き換えが発生しないかどうかのフォルダ監視をスタートさせようとするので実行時にエラーになります。 とりあえず、nullを返しておくようにしてるけど、ここはSqlCacheDependencyとかちゃんと使うのがいいと思います。ちなみに、動かすと分かるんだけどnullを返すとキャッシュしたVirtualFileを使いまわされてちょっと不便。 あと、FileExistsとGetFileの2箇所で無駄にデータベースに問い合わせてるのもキャッシュを使って回避するようにしないとダサイですね。 ~/FromDBViewsフォルダにファイルがあるように見せかけるようにしてます。

次に、データベースから読み込んだデータをファイルだと偽るために、VirtualFileの派生クラスを実装。

  public class FromDBVirtualFile : VirtualFile
{
  string  _contents;

  public FromDBVirtualFile(string virtualPath, string contents)
    : base(virtualPath)
  {
    _contents = contents;
  }

  public override System.IO.Stream Open()
  {
    MemoryStream stream = new MemoryStream();
  
    using (var writer = new StreamWriter(stream))
    {
      writer.Write(_contents);
    }

    return new MemoryStream(stream.GetBuffer());
  }
}

データベースからの読み込みはFromDBVirtualFileProviderで行ってるから、 ここでは文字列をStreamにするだけです。エンコーディング手抜きでサーセン。

あと忘れずにやっておかなきゃいけないのが、VirtualFileProviderの登録。これはGlobal.asaxのApplication_Startで。

    protected void Application_Start()
  {
    RegisterRoutes(RouteTable.Routes);

    System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(new FromDBVirtualPathProvider());
  }

コレでViewの実体ファイルが無くてもデータベースから読み込んだデータを使ってViewを表示することが出来るようになります。最後にFromDBControllerがこのViewを表示するようにします。

  public class FromDBController : Controller
{
  public ActionResult Dynamic(string viewName)
  {
    ViewData["Message"] = "動的ページ生成 from Database";

    return View("~/FromDBViews/" + viewName + ".aspx");
  }
}

※ルーティング登録もしてます。

fromdb6

コレだけ。 That's it.

ひゃっほ~!

2009年4月24日金曜日

ウエストでブレードが激安

ずっとね、アイスのスケートをベランダに放置してたのを忘れてた。ちょっと、というか尋常じゃないくらい臭くて。そもそも、1年くらい研磨してなかったから、ズルズルに横滑りしてたんだけど、あんまり履かないし、いいかな、なんて。

そしたらね、今日スケート出してたのを思い出して見てみたらね、サビサビだった...。うぬ。雨も日も放置してたし、それもやむなし。

道具は大事にしなきゃ!ってことで、久しぶりにウエストに研磨しにいったらね、ブレードが激安で売ってたよ。1シーズン前のがテーパーもレギュラーも7000円均一。2シーズン前のは同じく5000円均一。

カーブは全部揃ってないってノダさんが言ってたけど、ぱっと見一通り揃ってる風でしたよ。展示品無くなり次第終了なので(とは言ってもまだまだ沢山あったけど)、今のうちに買いだめしとくといいよ!

最新モデルは高いしね。

MySQLとMonoでASP.NET MVC - その2

ということで、続きました。

LINQ to EntityがMonoで使えないとなると、やっぱりLINQ to MySQLを使おうかなというところに戻りますよね。これがまたなんだかんだとがんばってみたんだけど...。ガッカリな結果でした。理由はサッパリ分からない。最初はライセンスの問題なのかな~、とかいろいろ試してみたけどできなかった。

mono1

↑これMySQLとxsp2(Mono)で動かしてるんだけど、ちゃんとログインできてるのが確認できると思います。なのでMembershipProvider経由のDBアクセスは成功ってことです。だけど、Sakilaを使ったサンプルはやっぱりダメで。

mono3 mono2

左がMonoの実行結果で、右が同じ物をVista上で動かしたもの。悲しいな~。エラーの意味が全く分からない。なんでリフレクションのエラーなんだろ...。どうしよっかな~。

ってことで、ふとNHibernateが気になりだした。Monoでも動くし。ドライバはDevartじゃなくてConnector/NETを使えばいいから、無料で構築できるのも魅力的。ただ...、xmlでマッピングはマッピラだ。韻を踏もうとして失敗した。それはいいとして、Fluent NHibernateっていうのがあったのも思い出した。NHibernateを使ったことないんだし、いっそのこと最初からFluent NHibernateでいいじゃない。

Fluent NHibernate

まずはNHibernateを調べなきゃ。近道するためにDime castでNHibernate Episodesを全部見て分かった気になったところでFluent NHibernate。

Getting Started: First Project in Fluent NHibernate

ここでいきなりSakilaを使うのはちょっと難しそう(マッピングの仕方もサッパリ分からないし)ので、チュートリアルに書かれてるデータベースをMySQLに構築して作ってみることにしてみました。何となくリレーションに使う項目名がStore_id/Product_idっていうのがカッコ悪いきがするのでStoreId/ProductIdにしてみる。

んで、言われるがままにコンソールアプリケーションを作って、エンティティ定義して、マッピング定義して...。いざ実行!動かず。泣ける。これだけじゃダメ?そりゃそうですね。接続の設定とか全然してないし。Fluentで出来るのかもしれないけど、よくわかんないのでapp.configに書く。

NHibernate - Learning with Code Samples

でも、これもFluent出来るんだね。まぁ、いいや。そこは早足で後回し。

Fluent NHibernate - Configuration - Chad Myers' Blog -

リレーションのカラム名を変えて作っちゃったおかげで、マッピングに失敗して動かず。面倒なことをしてしまった...。

なんだかんだでEomployeeMap。

  public class EmployeeMap : ClassMap<Employee>
 {
   public EmployeeMap()
   {
     Id(x => x.Id);

     Map(x => x.FirstName);
     Map(x => x.LastName);
    
     References(x => x.Store)
       .ColumnName("StoreId");
   }
 }
ProductMapにも挑戦。
  public class ProductMap : ClassMap<Product>
 {
   public ProductMap()
   {
     Id(x => x.Id);
     Map(x => x.Name);
     Map(x => x.Price);
     HasManyToMany(x => x.StoresStockedIn)
         .Cascade.All()
         .Inverse()
         .WithTableName("StoreProduct")
         .WithChildKeyColumn("ProductId");
   }
 }
最後にちょっと難しそうなStoreMap。
  public class StoreMap : ClassMap<Store>
 {
   public StoreMap()
   {
     Id(x => x.Id);
     Map(x => x.Name);

     HasManyToMany(x => x.Products)
         .Cascade.All()
         .WithTableName("StoreProduct")
         .WithChildKeyColumn("ProductId")
         .WithParentKeyColumn("StoreId");

     HasMany(x => x.Staff)
         .KeyColumnNames.Add("StoreId")
         .Cascade.All()
         .Inverse();
   }
 }
たったこれだけのマッピングにえらい時間がかかってしまいました。もっと簡単にAccess.As~CaseFieldでできるのかもしれないけど、ここもスキップして実行!

mono4

おぉ~。チュートリアルと同じ結果だ。そりゃそうだ。今度はコレをASP.NET MVCで実装。すったもんだの末に動くようになりました。

mono5 mono6

mono7 mono8

ぱっと見、普通ジャンって思えるけど、コレ実行環境はxsp2。Mono上で動かしてるんですよ!感動ですよ!もちろんNHibernate.Linqでアクセス!

これだけだと、Fluent NHibernate(r453で試しました)は含まれて無いし、データベース定義も入って無いから動かないけど、その辺はチュートリアルを見ながら試してみて下さいまし。

一応、ダウンロードできるようにして置きます。興味があるならどうぞ。とりあえず、コードを説明。

namespace MonoTest.FluentNHibernate.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
   IStoreRepository _repository;

   public HomeController()
   {
     _repository = new StoreRepository();
   }

   protected override void Dispose(bool disposing)
   {
     base.Dispose(disposing);
    
     _repository.Dispose();
   }

   public ActionResult Index()
   {
     var list = _repository.AllStore();

     return View(list);
   }

   public ActionResult Details(int id)
   {
     var store = _repository.GetById(id);

     return View(store);
   }

   public ActionResult Edit(int id)
   {
     var store = _repository.GetById(id);

     return View(store);
   }

   [AcceptVerbs(HttpVerbs.Post)]
   public ActionResult Edit(int id, FormCollection forms)
   {
     var store = _repository.GetById(id);
     TryUpdateModel<IStore>(store, forms.ToValueProvider());

     if (!ModelState.IsValid)
       return View(store);

     try
     {
       _repository.Save(store);

       return RedirectToAction("Index");
     }
     catch{
       ModelState.AddModelError("", "保存に失敗デス");
     }

     return View();
   }

   public ActionResult About()
   {
     return View();
   }
 }
}

まずは、コントローラ。HomeControllerを書き換えてます。普通ですね。はい。データアクセスをRepositoryにしといたくらいです。でもDI使ってないデス。

ViewはMonoで動かすのに型付きには出来ないので、スキャッフォールディングで作成(コンテキストメニューのAdd View)した後に、PageディレクティブのInheritsは修正。さらに、LINQ to SQLでもLINQ to Entityでもないので、ID列も自動でリンクに反映されないのを修正。

Repositoryは↓これだけ。

  public interface IStoreRepository : IDisposable
 {
   IQueryable<Store> AllStore();
   Store GetById(int id);
   void Save(Store store);
 }
  public class StoreRepository : IStoreRepository
 {
   ISession _session;

   public StoreRepository() : this(DbSession.GetSession()) { }

   public StoreRepository(ISession session)
   {
     _session = session;
   }

   public IQueryable<Store> AllStore()
   {
     return from store in _session.Linq<Store>()
            select store;
   }

   public Store GetById(int id)
   {
     return AllStore().Where(s => s.Id == id).FirstOrDefault();
   }

   public void Save(Store store)
   {
     using (var transaction = _session.BeginTransaction())
     {
       _session.SaveOrUpdate(store);
       transaction.Commit();
     }
   }

   public void  Dispose()
   {
     if (_session == null )
       return;

     if (_session.IsConnected)
       _session.Close();

     _session.Dispose();
   }
 }

データベースのSession(DataContext的な?)は↓こう。

  public class DbSession
 {
   private static ISessionFactory CreateSessionFactory()
   {
     return Fluently.Configure()
         .Database(
           MySQLConfiguration.Standard.ConnectionString(c =>
             c.FromConnectionStringWithKey("MySqlServices")))
         .ExposeConfiguration(c => c.Properties.Add("use_proxy_validator", "false"))
         .Mappings(m =>
             m.FluentMappings.AddFromAssemblyOf<DbSession>())
         .BuildSessionFactory();
   }

   public static ISession GetSession()
   {
     return CreateSessionFactory().OpenSession();
   }
 }

この辺、どうやって作るのがいいのかまだよく分かってないけど、とりあえずは動くものってことで。

Fluent NHibernateをORMに使って、データアクセスにはLINQ、データベースはMySQLでドライバはMySQL.Data(Connector/Net)。実行環境はCent OS 5.3上のMono 2.4。完全にフリーで動かす環境で、ASP.NET MVCを使ったプロダクト開発が出来ることが分かりました!過負荷の時にも上手く動き続けるのかは今後の課題。運用時にはxspじゃなくてApache+mod_monoで動かしましょう。

で、ここまでやってやっとSharp Architectureの良さに気がつくわけですね。

sharp-architecture - Google Code

Sharp Architectureを使うと、ASP.NET MVC用のヘルパーがちゃんと用意されてて、NHibernateが更に使いやすいみたい。T4 Toolboxでのマッピングの自動生成や、Sessionを管理出来るTransactionAttributeなんかがあるし、プロジェクトのフォルダ構成なんかがきちんと区切られてて、もっともっとちゃんと調べて使えるようになれば、Windows環境だけじゃなく、Monoにもデプロイ出来るプロジェクトを綺麗に作れること間違いなし。

#75 - Introdction to S#arp Architecture

Dime Castsにもビデオあるから、これも興味あれば見てみるといいと思います。10分くらいだし。

しかし、分からないことだらけで、時間かかりすぎたな~。もっと、サクッと動くコードが書けると思ったのに。

追記

Monoで動かす際に、System.Web.Mvc/System.Data.Servicesはアセンブリのローカルコピーを忘れずにね。

MySQLとMonoでASP.NET MVC

思い出す度にいろいろ試してみたけど、どうにもこうにも...。

Using MySQL with Entity Framework and ASP.NET MVC – Part I « Christopher Patterson

ホントにConnector/Net 6.0でデザイナーつかってORマッピングの定義できるの~??どんだけやってもできないんだけどな~。とりあえず現バージョンではできないってことにしとこう。自分で定義ファイルを書けばできるんだろうけど、それじゃ~、全然便利じゃないしよくわかんないし。あ、コマンドラインで実行したらできたりするのかな?まぁ、いいや。

なので、有料だけどトライアルもあるし、DevartのdotConnect for MySql(Download dotConnect for MySQL)を使う事にする。流石にコレならできるでしょう。買う事になってもDeveloper Editionで$229.95だしね。個人でも買える値段で助かる(買わずにトライアルだけど)。

まずは、インストール。サクッと終了。チュートリアル「dotConnect LINQ to MySQL Tutorial」を見ながら、コードを書いてみようと思ったけど、どうも"Devart LINQ to SQL Model"のItemTemplateが見つからない。おやや?インストールに失敗したのかな?と思いきや、なんの事はない、VSのテンプレートフォルダを見てみたら1033(英語)には入ってるけど1041(日本語)には入って無かっただけでした。DevartLINQtoSQL.zipを1041フォルダにコピーしてdevenv /InstallVSTemplates。これでプロジェクトに追加出来るようになりました。ちなみにテストで使うデータベースのサンプルをどうしようか。検索してみたらMySqlだとSakilaっていうのがNorthwindやAdventureWorks的なデータベースっぽいのでそれをインストールして使う事にしました。

SakilaSampleDB - MySQL Forge Wiki

で、Sakila.lqmlという名前で作成したので、デザイナーでテーブルをチョイチョイ登録しようと思ったら、外部デザイナーが立ち上がった。

devart1

ん?なんか違和感。そういうものなのかな、と思いつつ、適当にデータベース登録してテーブル追加して作成。

devart2

で?えーと、あ、ProjectメニューにGenerateってある。これをクリックして、なにやらファイルを出力してみる。で、VSに戻ってコンパイル。おぉ~。なんか普通に使える~。

※新しいバージョンで試してみたら、勝手に生成してくれるようになってた。

でも、待てよ。Entity Frameworkってこんなじゃなかったような...。ちゃんと見たら全然違うの作ってた。LINQ to MySqlって書いてるジャン...。やりたかったのはコレじゃなくて"dotConnect for MySQL Entity Framework Tutorial"こっちでした。これはこれで普通に取得やら更新もできたんだけど、リレーションのデータがちゃんと取れなくて、なんでなんで~と頭を抱えて損した。まぁ、いっかと、気を取り直してEntity Framework。今度は間違えないように"ADO.NET Entity Data Model"をちゃんと選んでテーブルを追加。そうそう、これこれ。このウィザードでテーブルを選ぶところでエラーが出て先に進まなかったんだけど、今度はなんの問題も無くクリア。

devart3 devart9

あとは、Scaffoldでちゃちゃっと作る。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Ajax;

using System.Transactions;
using MonoTest.Models;

namespace MonoTest.Controllers
{
    public class SakilaController : Controller
    {
      //
      // GET: /Sakila/
      sakilaEFEntities context;

      public SakilaController()
      {
        context = new sakilaEFEntities();
      }

      public ActionResult Index()
      {
        var list = (from act in context.actor
                    select act).Take(10).ToList();
        return View(list);
      }

      public ActionResult Details(int id)
      {

        var actor = (from act in context.actor
                                        .Include("film_actor")
                                        .Include("film_actor.film")
                     where act.actor_id == id
                     select act).FirstOrDefault();
        if (actor == null)
          return RedirectToAction("Index");
        
        return View(actor);
      }

      public ActionResult Edit(int id)
      {
        var actor = (from act in context.actor
                     where act.actor_id == id
                     select act).FirstOrDefault();
        if (actor == null)
          return RedirectToAction("Index");

        return View(actor);
      }

      [AcceptVerbs(HttpVerbs.Post)]
      public ActionResult Edit(int id, actor actor)
      {
        if (ModelState.IsValid)
        {
          using (var ts = new TransactionScope())
          {
            var entity = (from act in context.actor
                          where act.actor_id == id
                          select act).FirstOrDefault();
            if (entity != null && actor != null)
            {
              actor.actor_id = id;
              actor.last_update = DateTime.Now;
              context.ApplyPropertyChanges(entity.EntityKey.EntitySetName, actor);
              context.SaveChanges();
            }
            ts.Complete();

            return RedirectToAction("Details", new { id });
          }
        }

        return View(actor);
      }
    }
}

Controllerはこんなので。

devart4 devart5 devart6

ちゃんと動く~。CentOS側のデータを確認してみる。

devart7

ちゃんと更新できてる~。なんか嬉しい。でも、Monoで動かしてるわけじゃないので当たり前っすね。これをMonoで動かすのが目的なのでxsp2!

devart8

ガッカリです。エラー出て動きませんでした。

ASP.NET Provider Model Support in dotConnect for MySQL

色々試してみたけど、どうやっても動かない。うぎゃ~!!と思って調べて見たら...。MonoではLINQ to Entityが動かないらしい...。凹む。ベコっと音が出た。

続く。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。