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が動かないらしい...。凹む。ベコっと音が出た。

続く。

2009年4月19日日曜日

いっぱいいっぱい

4月だけど、もう十分暑いね~。あまりにも人が少ない&オグさんいないってことで、気分転換もかねて久しぶりにアメージングに行ってきました。

海の向こうではNHLのプレーオフが始まってて、NHL.comのトップページにオベチキンガックリの写真が上がってたりと目が離せないね。レギュラーシーズンの録画も全然観れてないんだけど...。レンジャーズが2-0でリードしてるけど、まだまだこれから巻き返せる範囲だからキャピタルズにはがんばってもらいたい。初戦を落としたシャークスもダックス相手にがんばって!レッドウィングスはとりあえず2-0でリードだから連覇を目指してこの調子で勝ってね!

自分達の試合の話は...あんまり覚えてないな~。とりあえずレフェリーがタカチさんでチョイチョイいじられたのだけは覚えてる。ハッキリと。4点目のダブルアシストを指差して除外されたのもハッキリ覚えてますよ!

ぎりぎりまでゴーリーがいないってことで、オグさんゴーリーに引き続き今回はオレがゴーリーやんだろーなーと思ってたら、タクちゃんが来てくれるってことになって、超助かった。試合前から「滑れないから無理」と弱気なタクちゃんだったけど、そんなのどうでもいいっす。もう来てくれただけで助かる。ところでタクちゃんと一緒にいた女性の方は誰なんですかね。気になってしょうがなかったじゃないか。

試合開始前に男子3人女子3人+タクちゃんで、どういうセットにしようか悩んだけど、開始直前にミズノメンが来てくれて男子4人になったから、これまた助かった。これなら常時男2人をリンクに乗せておけるもんね。 試合は開始早々にミズノメンの個人技で2得点。うひょ~。オレいらね。今日はこのままミズノメンになんとかしてもらいたいところデス。

が、ミズノ君と一緒に組んでたテンパ君がゴール前のマークをことごとく外して3失点。アレは無理。止められません。タクちゃんが滑れる滑れない以前に無理。バックドアから触るだけで入るシュートだし。外す方が難しい。テンパがんばって。後輩がベンチから応援してるんだから! たけはらさんはね~、まぁね~、ぼちぼちですよ。普通にブラスに溶け込んでる感じっていうんですかね。あの子がんばってるね、的な?いっぱいいっぱいデス。

とりあえず、ギリギリだけど勝ててよかったね。ここで負けてたら負け越すところだったしね。負け越すといえば、駒沢NEXTにヤマちゃん出てたね。対戦するのかな。楽しみにしとこ~っと。今期から加入のミサキちゃん(?)はなにげに上手い気がする。下のクラスで出てるって話だけど、ブラスでも別に問題ない気がする。何度も惜しいシュートがあったしね。

そういえばS40蒲公英には"ミ"が付く女子が多い気がする。ミカ・ミチ・ミナ・ユミ・ミサキ...。7人中5人もいるね。流行なの?

2009年4月13日月曜日

ASP.NET MVCをMonoで動かしてみる

長い戦いだった...。現時点での結論から言えばそれなりに動くけど、型付きViewPageだけはどうにもならない感じでしょうか。

そもそもMonoでASP.NET MVCを動かすことにどれほどの意味があるのかという所だけど、これは凄く意味のあることですよね。なんせ実行環境はすべてオープンソースで構築できるんだから。こうなってしまえば個人的にはJavaに何もメリットを感じない。Google App Engineが羨ましいくらいか。でもPythonあるし、そもそもAzureがあるし。

構築した環境はVista Ultimate 32bitにVMWare Server 1.0.9を入れ(2.0.1だとどうも上手く動かなかった)、Linuxディストリビューション(っていうの?)はCentOS 5.3openSUSE 11.1、Mono 2.4とMonoDevelop 2.0、データベースにMySQL 5.0.45(CentOS上にyum install)。

My Adventures Installing mono 2.0 on CentOS 4 to work with apache via mod_mono « Pale Musings

Linuxを生まれて初めて触ってみたけど、さっぱりチンプンカンプンですね。でも、ネットで検索すればいくらでも情報が出てくるから、とりあえず動かすくらいならどうとでもなるもんです。ちゃんとまともに運用環境を作るにはかなり難しそうな気がするけどね。

何でCentOSとopenSUSEを入れたかたというと、MonoDevelopをCentOSにインストールするのに3日がんばったけどできなかったから。GLibのバージョンが入ってるのじゃダメと言われ、ソースコンパイルからやってみたけど、芋ずる式にアレもコレもダメだと言われ、コンパイルできてmake installで実行しても、Gtk+からちゃんと新しいのが見えないと言われ...。うぎゃ~!!となってやめました。Monoはすんなり入るのにね。で、一番簡単なのはNovellが出してるディストリビューションを使う事だと思って、openSUSEも入れることにしたんです。まぁ一緒だろと思ったらyumじゃなくてzypper(ソフトウエア管理のコマンドライン比較 – openSUSE)だとかよくわかんないことに時間を取られたものの、流石Novellなだけあってワンクリックでインストールもあっという間に完了。

mono1 mono2

MonoDevelopでASP.NET MVCのプロジェクトを新規作成したところ、流石にテンプレートは空っぽでControllerもViewも何もない状態でした。T4無いしね(たぶん)。なので、簡単にControllerとViewを作って動かしてみると、あら素敵。アッサリ動いた。簡単なものだけど、動くとビックリ。スゴイね~。んじゃ、ってことで、コレをCentOSの方にコピーしよと思ってハタと手が止まる。あれ?こういう時ってFTP?それともSSHっていう何かよくわかんないシェル?ふぬ~。あ、産婆。じゃなくてsamba。

Windowsファイルサーバー構築(Samba) - CentOSで自宅サーバー構築

これをCentOSとoenSUSE両方で起動してコピペ。ちなみにVMWareで動かしてるときにopenSUSEは何もしなくてもマウスが動くしGNOME端末での日本語入力もできるのに、CentOSはなんかやらなきゃいけないみたいで。VMWare toolsっていうんですかね。よくわかんないけど、さんざんやって上手く出来なかったから諦めた。いいもん。毎回Alt+Ctrl押すもん。

とりあえず、MonoDevelopだとVisual Studioに慣れた体では開発しにくいし、どうせ動かすのはCentOSだしってことで、ここヵらはCentOSにデプロイ、Vista+VSで開発に戻ります。openSUSEで動かしてもいいんだけどさ~。なんとなくきかん坊が気になるからさ~。

mono3

VSで作ったデフォルトのプロジェクトをそのままの状態でビルド>発行して、CentOSにコピー。んで、Apacheで動かすためにmod_monoの設定をしようかなってところで、XSPっていうのが簡易Webサーバーの役割を果たしてくれるということなんで、コピーしたフォルダをカレントに"xsp2"!アッサリ動くの図↑。

続いてMembershipを使いたいから、データベースを入れなきゃいけないよね。なのでMySQL。入れるのは簡単。とにかくyumさん。なんだこいつ、スゴイヤツだ。sudoさんと仲がいいのか?最初はsuで切り替えてやってたけど、普通のユーザーアカウントで読み取れないフォルダがばかばかできちゃって面倒なことになったから、結局何度もやり直して須藤さんsudo(SuperUser DO?)に任せる事に。

でもって、MembershipProviderがなんかあるはず。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 24.2.2.2 Mono を使用した Unix に Connector/NET をインストールする

あるのね...。Connector/Netと言われても...。GACに登録したいけど、インストールパスってどこなんでしょう。そもそもどこにインストールされてるのかが分からない。"/usr/lib/mono/2.0"にいろいろ入ってみたいだけどここでいいのかな...。とりあえずコピーしてgacutilでグローバルアセンブリキャッシュに入れて見る。エラーは出てないからいいのかな。

んで"nolan bailey's blog: MySQL ASP.NET Membership and Role Provider"や”knowledge shared is knowledge²: ASP.NET and MySQL - membership provider (part 1)”ここに書かれてるようにweb.configをセット。autogenerateschema=”true”にしておくと勝手にテーブルを準備してくれるからaspnet_regsqlやInstallMembership.sqlをどうのこうの気にしなくてもいいから楽ちんです。忘れずにセットしなきゃね。

mono4

うほほ。ちゃんとデータベースに登録されました。

でも、本当はここが最初ちゃんと動かなくて、あれれな感じだったのがプロジェクトのアセンブリに追加したMySql.Webをローカルコピーしとくようにしたらなんとなく先に進むようになるものの、そこからさらにエラーでなんとも動かなくて。

mono5 mono6

ユーザー登録はできたのに、ログインでエラーになるの図。これはなんだも。

System.ArgumentNullException: Argument cannot be null.
Parameter name: type
 at System.ComponentModel.TypeDescriptor.GetConverter (System.Type type) [0x00000]
 at System.Web.Mvc.DefaultModelBinder.BindModel (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValue (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ParameterDescriptor parameterDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.GetParameterValues (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ActionDescriptor actionDescriptor) [0x00000]
 at System.Web.Mvc.ControllerActionInvoker.InvokeAction (System.Web.Mvc.ControllerContext controllerContext, System.String actionName) [0x00000] 
なんでこんなエラーが...。全然ダメじゃないか。いろいろ検索してみたら同じ現象が起きてる人もいて。

http://go-mono.com/forums/#nabble-to23006435

特に返信もなくて途方に暮れかかったけど、Forumあるならスレッド見ていけばなにかヒントがあるかもと、見ていくと、↓こんな書き込みがありました。

http://go-mono.com/forums/#nabble-to22854337

Windowsで使ってるアセンブリをローカルコピーしとけと。なるほど。確かにバイナリ互換なMonoなんだからそういう手もありだ。System.Web.Mvc.dllをコピーして動かす。

mono7

拡大すると見えるけど、ちゃんとログインできてますね。グレート!

あとはLINQでデータベースにアクセスできれば普通に開発できる。でも、最初に書いたとおり型付きViewPageだけは今のところ解決策が見つからずなので、当面はコードビハインド指定で乗り切ることになるのかな。そんなこんなでここまで5日ほどかかったけど、面白い発見もいろいろあって実行環境としてLinuxも選択肢に入れていこうと思ったり思わなかったり(EC2もWindows Server+SQLServerよりもLinux+Mono+MySQLの方が同じインスタンスでも安く運用できるからね)。

2009年4月4日土曜日

もう一度AsyncController

正式にドキュメントが公開されてるので、改めてドキュメントに書かれてるようにサンプルを書いてみました。

ASP.NET - Release: ASP.NET MVC v1.0 Source

現時点で”Using the AsyncController.docx”はあんまり人気無いみたいね...。ダウンロード数の桁が違う。

async

ドキュメントから勝手に引用。ようするに非同期ハンドラなんですが、いろいろ(アクションだけじゃなくて非同期で大丈夫なように)とあるようで結構なコード量での対応になってます。

非同期にするにあたり、ルーティングの登録が通常の物と違います。非同期ハンドラを使わないとダメなので、MapRouteではなくMapAsyncRouteでルーティング登録。とりあえず、Futuresのアセンブリ参照とMicrosoft.Web.Mvcのusingは忘れずに。

続いて、非同期化したいコントローラのベースクラスをControllerからAsyncControllerに変更しましょう。AsyncManagerやActionInvokerなんかがあるし。ここまでは決まり事です。

非同期アクションを実装する場合、何パターンかあります。ドキュメントを上から見ていくと以下のようなパターンが書かれてます。

  • IAsyncResultパターン
  • イベントパターン
  • デリゲートパターン

それぞれ書き方も違うので一つずつ見ていきます。画像のURLを渡して、そのバイナリを取得し、そのままレスポンスするというものをサンプルに書いてみます。

まずはIAsyncResult。これはアクションが呼び出される際に、アクション内で使用する引数の他にAsyncCallbackとobjectが渡されて来ます。"IAsyncResult を使用した非同期メソッドの呼び出し"これの一番うえのパターンでしょう。End~は勝手に呼び出してくれます。

    public IAsyncResult BeginLoad(string url, AsyncCallback cb, object state)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     return req.BeginGetResponse(cb, req);
   }

   public ActionResult EndLoad(IAsyncResult ar)
   {
     WebRequest req = ar.AsyncState as WebRequest;
     var res = req.GetResponse();
     return new FileStreamResult(res.GetResponseStream(), res.ContentType);
   }
こんな感じでしょうか。時間のかかる処理向けというよりは、Begin/Endの組み合わせでIAsyncResultを返す非同期機能を呼び出すときに使うものですね。

次にイベントパターン。最初に呼び出されるアクションの戻り値はvoidにして置いて、引数も必要なものだけを定義。完了時に呼び出されるコールバックのサフィックスにCompletedをつけるルールる、そのコールバックがActionResultを返します。ここでAsyncManagerが登場します。AsyncManager.Parametersディクショナリにコールバックに渡される引数名と同じキーで値を入れておくと、勝手に引数に渡してくれます。コールバックがいつ呼び出されるのかというとAsyncManager.OutstandingOperations.Incrementした値がDecrementで0になったときみたいです。なので、複数の時間のかかる処理を平行して実行させるとき、それぞれの処理をQueueUserWorkItemに登録。同じ数だけインクリメントをしておくけど、処理終了時点でデクリメント。すべてのQueue内の処理が完了するとカウンタが0になるのでコールバックが呼ばれる流れですね。コールバックにはサフィックスとしてCompletedをアクション名につけたものを登録する掟です。

    public void LoadE1(string url)
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       var req = WebRequest.Create(url);
       req.Method = "get";
       var res = req.GetResponse();
       AsyncManager.Parameters["stream"] = res.GetResponseStream();
       AsyncManager.Parameters["contentType"] = res.ContentType;

       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult LoadE1Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

こんな感じで動いてます。長時間の処理でIAsyncResultじゃない場合にはこれがいいかと。でも正直AsyncManager.Parametersに値を入れるのがこのタイミングでいいのかは自身ないです。Futuresのソース見れば分かると思うけど...。

同じくイベントパターンですが、今度はAsyncManager.RegisterTaskを使って実行したい処理を登録しておく方法です。これはIAsyncResultパターンとのコンビネーションみたいな感じです。ただし自分でインクリメントやデクリメントして処理数を管理しないです。RegisterTaskの2個目の引数に渡すendDelegateが完了したら勝手にカウンタを管理してCompletedを呼び出してくれる感じです。これも複数タスクを登録可能。

    public void LoadE2(string url)
   {
     var req = WebRequest.Create(url);
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback,null),
         ar => {
           var res = req.EndGetResponse(ar);

           AsyncManager.Parameters["stream"] = res.GetResponseStream();
           AsyncManager.Parameters["contentType"] = res.ContentType;
         });
   }

   public ActionResult LoadE2Completed(Stream stream, string contentType)
   {
     return new FileStreamResult(stream, contentType);
   }

これで。

最後がデリゲートパターン。さっきもデリゲートじゃないか!と言われればごもっとも。何がデリゲートかというとアクションの戻り値がデリゲートです。これまでとは少し感じが違って1つのアクション(メソッド)しか定義しません。コールバックはどうするかというと、コールバックをこのアクションの戻り値にしてしまうというのが、デリゲートパターンの名前の由来(ウソです、知りません)。

    public Func<ActionResult> LoadD(string url)
   {
     var req = WebRequest.Create(url);
     WebResponse res = null;
     req.Method = "get";
     AsyncManager.RegisterTask(
         callback => req.BeginGetResponse(callback, null),
         ar => {
           res = req.EndGetResponse(ar);
         });

     return () => {
       return new FileStreamResult(res.GetResponseStream(), res.ContentType);
     };
   }

1つの定義で済むので完結に書けていいですね。これもRegisterTaskを使ってるので、複数登録してもすべてが終わるまでコールバックは呼ばれないですね。

だいたいこんな感じです。

通常のControllerとAsyncControllerを簡単に比べるためのアクションを書いてみます。3秒スリープして画像ファイルをレスポンスするという簡単なものです。

同期版

    public ActionResult Dog()
   {
     Thread.Sleep(3000);
     return File("/Content/dog.jpg", "image/jpeg");
   }

非同期版

    public void Dog()
   {
     AsyncManager.OutstandingOperations.Increment();
     ThreadPool.QueueUserWorkItem(w => {
       Thread.Sleep(3000);
       AsyncManager.Parameters["path"] = "/Content/dog.jpg";
       AsyncManager.Parameters["contentType"] = "image/jpeg";
       AsyncManager.OutstandingOperations.Decrement();
     }, null);
   }

   public ActionResult DogCompleted(string path, string contentType)
   {
     return File(path, contentType);
   }

実行するとどっちも3秒たってから画像が表示されます。

async2

前回同様Measure-CommandとTinyGetでパフォーマンスを測定。

PS C:\Program Files\IIS Resources\TinyGet>  Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Home/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 10
Milliseconds      : 165
Ticks             : 101652219
TotalDays         : 0.00011765303125
TotalHours        : 0.00282367275
TotalMinutes      : 0.169420365
TotalSeconds      : 10.1652219
TotalMilliseconds : 10165.2219

PS C:\Program Files\IIS Resources\TinyGet> Measure-Command {.\tinyget -srv:localhost
-r:27721 -uri:/Async/Dog -threads:50 -loop:1}

Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 7
Milliseconds      : 314
Ticks             : 73144834
TotalDays         : 8.46583726851852E-05
TotalHours        : 0.00203180094444444
TotalMinutes      : 0.121908056666667
TotalSeconds      : 7.3144834
TotalMilliseconds : 7314.4834

気持ちだね...。サンプルが良くないから。 早くするというよりも、サーバーのコネクションを素早く解放して同時実効性能を上げるのが目的なんだから早さ見てもしょうがないですよね。

今回登場してもらったわんちゃんは"SPOILED-ROTTEN-TO-THE-CORE dog on Flickr - Photo Sharing!"です。

> UpdateModel で、コレクションであるプロパティの各要素の、一部のプロパティだけ更新する

Developer @ ADJUST : ASP.NET MVC - UpdateModel で、コレクションであるプロパティの各要素の、一部のプロパティだけ更新する

コメント欄に書こうと思ったんですが、サンプルが長すぎたのでこっちに書きます。

おっしゃるとおり、復元したいクラスがリストじゃない場合は簡単に制限をかけられるんですが、コレクションのインスタンスに対しての制限は少し面倒ですね。

復元したいインスタンスの項目を制限する場合、UpdateModelを使うなら

  • IncludeやExcludeを指定
  • インターフェースを指定
  • Form復元用のベースクラスを指定

の3パターンあると思います。

が、Listの場合、Listそのもののインスタンスを生成するところから始まるのでUpdateModel<List<interface>>(…)とは書けないですね。インスタンスが作れないので。かといってUpdateModel<List<FormBase>>(…)と書くと、すべてが復元されないので一発で欲しいインスタンスが生成できません。

Listに関する部分に限りこういう使い勝手になってしまうので、アイテム毎にUpdateModelを実行するのが簡単な解決策になります(たぶん)。

ところで、一般的にこういう形式のデータをどのように復元させているのか気になる所ですね。いろいろなコードを眺めるとLINQ to SQLなんかのオブジェクトを使ってる場合は、必要な項目だけ復元させてあとはオリジナルのインスタンスにAttachという形式が多いと思います。オリジナルのインスタンスに直接復元させないところがミソかなと。

一旦復元用のインスタンスに値を入れて、それをオリジナルにコピー(Attach)してしまうのがもう一つの手段として考えられるのではないかなと思う次第です。

  public class ViewModelBase<T>
 {
   public object Attach<T>(object target)
   {
     foreach (var iprop in typeof(T).GetProperties())
     {
       var srcProp = target.GetType().GetProperty(iprop.Name);
       object val = srcProp.GetValue(target, null);
       if (val!=null)
         this.GetType()
             .GetProperty(srcProp.Name)
             .SetValue(this, val, null);
     }

     return this;
   }
 }

 public interface IPerson
 {
   int? Age { get; set; }
 }

 public class Person : ViewModelBase<IPerson>
 {
   public int? Age { get; set; }
   public string Name { get; set; }
 }

 public class Team
 {
   public List<Person> People { get; set; }
 }
こういうモデルがあるとして(ちょっとダサイですが)。コントローラに↓こういう処理を書くようなイメージです。
    Team LoadTeam()
   {
     return new Team { People = new List<Person>{
         new Person{ Name="Boo",Age=11},
         new Person{ Name="Bar",Age=21},
         new Person{ Name="Foo",Age=43}
       }
     };
   }

   public ActionResult Index()
   {
     var team = LoadTeam();

     return View(team);
   }

   [AcceptVerbs(HttpVerbs.Post)]
   public ActionResult Index(List<Person> people)
   {
     var team = LoadTeam();
     for (int idx = 0; idx < team.People.Count; idx++)
       team.People[idx].Attach<IPerson>(people[idx]);

     return View(team);
   }
オリジナルのTeamにpeopleのアイテムを埋め込んでいく感じです。実際にはIDかなにかでAttachしたいインスタンスとされるインスタンスを結びつける(この例だと単純にインデックスで結びつけてます)ことにナルト思います。

いかがでしょうか?

2009年4月2日木曜日

FancyBox

スクリーンショットや写真なんかをブログに貼り付けること多いです。特に最近はWindows Live Writerで投稿することがほとんどなんだけど、画像の挿入機能を使うと自動で小さなサイズの画像の生成と大きいサイズの画像へのリンクが貼り付けらて、楽ちんだなと思ってました。

<a href="{大きい画像のURL}">
<img style="border: 0px none; display: inline;"
title="{ファイル名}" alt="{ファイル名}"
src="{小さい画像のURL}" border="0" height="{高さ}" width="{幅}" /></a> 
こんな感じのタグが挿入されますね。でも、単純にアンカーでリンクしてるだけなので表示のされ方がちょっと素っ気ない。というか画像だけが表示される。

と、言うわけで、もう少しかっこよく表示させたいので、jQueryを以前ブログで使えるように取り込んでるので、FancyBoxっていうのを使ったかっこよく表示されるようにしてみました。

Fancybox | Fancy lightbox alternative 個人的にはZoomしなくてもちゃんと見れるように、ちゃんと切り抜いて必要な箇所だけにした画像を貼り付けるほうが見やすいとは思うけど、面倒だしね...。

使い方は簡単。Usageに書いてる通り。

  1. scriptタグ書く。
  2. linkタグでcssを取り込む。
  3. aタグで囲まれたimgタグを書く。
  4. エレメントに対してfancybox()を呼ぶ。

これだけ。scriptにはjQueryを含んでないとダメだけど、これはGoogle Ajax Libraryを参照。その他のファイル群をどこに置くのか悩んだけど、結局Google Page Creatorにアップ。もう新規アカウントを作成できないから、アカウント持ってない人は違うスペースを用意しないとね。一般的にはどうするんだろ?SkyDrive?

ページのローディングが終わったところで、fancyboxを呼び出してみる。

$(function(){
$("a[href*=ggpht.com]").fancybox();
});

なにも表示されない...。悲しい。これでいいはずなのに...。大きい画像のURLが間違ってるのかなと思ったけど、そんなこともなくブラウザ上ではちゃんと表示される。なんでだろうと大きい画像をダウンロードしてローカルで確認しようと思ってaタグのリンク先を保存したら、理由が分かった。aタグのhrefに入ってるURLは画像そのものを指してるんじゃなくて、画像を表示するHTMLを指してた。常識?

例えば↓この画像。

Boston City Flow

生成されるHTMLは

<a href="http://lh5.ggpht.com/_3YQxskhNNyI/SdQ4ep_eVJI/AAAAAAAABNM/3hRpwvyUMV4/s1600-h/Boston%20City%20Flow%5B3%5D.jpg">
<img style="border-width: 0px; display: inline;" title="Boston City Flow" alt="Boston City Flow" src=http://lh6.ggpht.com/_3YQxskhNNyI/SdQ4fLPxrfI/AAAAAAAABNQ/pbG3lWDojxQ/Boston%20City%20Flow_thumb%5B1%5D.jpg?imgmax=800”
   border="0" height="160" width="240" />
</a>
こうなる(Picasaだからね)。このアンカーのリンク先はHTMLで画像じゃないんだけど、中身は↓。
<html>
<head>
<title>Boston City Flow[3].jpg (image)</title>
<script type="text/javascript">
<!--
if (top.location != self.location) top.location = self.location;
// -->
</script>
</head>
<body bgcolor="#ffffff" text="#000000">
<img src="http://lh5.ggpht.com/_3YQxskhNNyI/SdQ4ep_eVJI/AAAAAAAABNM/3hRpwvyUMV4/s1600/Boston+City+Flow%5B3%5D.jpg" alt="[Boston City Flow[3].jpg]" border=0>
</body>
</html>

アンカーとのURLの違いはHTMLが/s1600-h/で、画像が/s1600/。なのでこれをfancybox適用前に書き換えてしまいましょう。

$(function(){
$("a[href*=ggpht.com]").each(function(idx,elem){
  elem.href = elem.href.replace('/s1600-h/','/s1600/');
  $(elem).fancybox();
});
});

全然、他のパターンを考慮してないけど、いいよね。これで、画像表示も見やすくなりました。

Costa Rican Frog metalking

2009年4月1日水曜日

EC2のWindows上で日本語Webアプリケーションを動かす

ここ最近、ずっとAmazon AWSでの環境構築をしてて、運用環境の変更を行ってました。クラシックASPや、ASP.NET 1.1、2.0で動かしているものをEC2に移行してしまおうという魂胆です。ハードウェアも古くなりすぎてて、にっちもさっちも行かない感じだったのもあり、いっそのことクラウドへ。Azureにいければ一番良かったけど、用途も合わないし、そもそもがまだ無理だしね。

Amazon EC2でWindowsサーバーを動かすだけなら、前に一度チャレンジしてるし今ならいろんな所でエントリもされてるから検索すれば沢山見つかると思います。もちろん日本語で。実際の運用環境を作ってみるにあたりデプロイ含めて少し。自分が忘れないためのメモエントリです。作業はElasticfoxとS3 Organizer、AWS Management Consoleですべて完結。自動化するなら"Amazon Web Services Developer Community : C# Library for Amazon EC2"を使ってコード書いて対応。今回は2台しかインスタンス立ち上げないので使わないです。

まずはどのインスタンスを使うか。x86(32ビット)かx64(64ビット)が大きな分かれ目。x86ならStandard Smallか、High-CPU Mediumの2択。x64ならStandard Large/Extra Largeか、High-CPU Extra Largeの3択。それぞれのVMスペックは"Amazon EC2 Instance Types"に全部書いてるの詳細はそちらで確認。それぞれのインスタンスにSQL Server 2005か同Expressが入ってるイメージがあるけど、ぶっちゃけStandard Smallでは使い物にならない。SQL Serverを使うならx64しか選べないけど、同Expressを使うならx86でもいいので、High-CPU Mediumが一番安く現実的な選択肢。そうなるとSQLServer AgentがないのでWindowsのタスク機能を使ってバックアップとることになるので、ちょっと作業が増える。それよりなによりライセンス的に大丈夫なのかがよく分からない。ウェブアプリケーションのストレージとしてExpressっていいんでしたっけ?Datacenter Editionだとなんかライセンス違うのかな。ちなみにAuthentication Serviceは今回使ってないのでよく分かりません。インスタンスの料金については"Amazon Elastic Compute Cloud (EC2) Running Microsoft Windows Server and SQL Server"ここで。なにげにSQLServerを使いたいなら最低$1.10必要だから1日$26.4が最低価格になるんだよね。1アプリケーション1DBサーバーなんて贅沢な使い方をしようとすると、それだけで月に$800くらいかかっちゃうから気をつけないとね。

AMIも最初は名前見ただけじゃ何がインストールされるのかよく分からなかったけど、何度もやってると普通の名前に思えてくるから恐ろしい。

ec2_1

OwnerがAmazonになってるWindows AMIはこれで全部。Authenticationの有無(WinAuthと付いてるか付いてないか)の組み合わせが5種類なので全10種類のみ。”i386”が32ビットで”x84-64”が64ビット。SQLServer入り(SqlSvrStd)が2種類で、同Express(SqlSvrExp)入りが4種類。どっちも入って無いのが4種類。
...分かりやすいじゃないか。

これらとインスタンスタイプの組み合わせだけなので、慣れればなんてことないね。

で、日本語化。当然、すべて英語版なのでそのままだと日本語が表示されない(インスタンス上で)ばかりか、各種書式フォーマット(ロケール)も変えないと、アプリケーションで変な表示になってしまう。内部的にも文字列から復元出来なかったりするところが出るかもしてないし。

と、言うわけで"Amazon Web Services Developer Community : Configuring Windows Components on Amazon EC2"に書いてるスナップショットIDをもとにしたVolumeを作成してドライブを割り当てておいてから、コントロールパネルの"Regional and Language Options"で設定を変えること。

ec2_2

ec2_3

ec2_4

それと、Time Zoneも変えた方がいいかも。

とりあえずこれで、サーバー上で日本語が使えるようになるし、書式化フォーマットも見慣れた物になるけど、ASP.NETアプリケーションでの書式が変。ここの設定が反映されてない。おかしいな~、と思って調べて見たら"globalization 要素 (ASP.NET 設定スキーマ)"でカルチャー設定するのが正しい対応でした。てへ。

DBも言語や照合順序の設定が日本語向けのものじゃないから、その辺の設定も忘れずに。だいたいこんな感じでOKなんじゃないでしょうか(ちょいちょいコントロールパネルの設定が英語に戻ってるのは、なにがきっかけなんだろう)。