2009年11月29日日曜日

LinqToSqlでの開発の注意点 その2

今回はLinqToSqlを使用してDBアクセスする際の注意点を解説する。

LinqToSqlで開発する場合、DBMLファイルを使用しDBMLで生成されたDataContextを使用するのが常道だろう。その際に、LinqToSqlを介しDBのテーブルにマッピングされたクラスを使用していると、ともするとメモリ上のデータを扱っているような錯覚を起こす場合がある。そのため経験の浅いプログラマはパフォーマンスを考慮せずに実装したりするのだが、実際にCo-opの学生が実装したコードを次に紹介しよう。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge))
 {
  DoSomething(user);
  var userClients = user.UserClients.Select( x => new SimpleUserClient(){ Name=x.Name, Address=x.Address });
  DoSomethingWithClients(userClients);
 }
}

上記の例は実際に彼が実装していたのとは異なるのだが、やっているコンセプトは同じだ。もちろん上記のコードで期待した動作はできるのだがDBのパフォーマンス上よろしくない。

その理由は上記のコードをSqlに展開するとよく分かる。展開したSqlが下記になる。

select * from Users where Hoge=1
select Name, Address from UserClients where UserId=1
select Name, Address from UserClients where UserId=2
select Name, Address from UserClients where UserId=n




という風に2行目以降は1行目で取得したUser分だけ生成、実行されることになる。仮にUserを100人分取得した場合は100回DBにアクセスすることになる。SqlServer 2008とともにインストールされているSql Server Profilerを起動してから上記のコードを実行すると実際にSqlServer上で実行されているSqlが参照できるので良く分かると思う。

実際にはこのように書くべきだ。

using( var db = new HogeDataContext())
{
 foreach( var user in db.Users.Where(x=> x.Hoge).Select(x=> new { User=x, Clients=x.Clients.Select(x=>new SimpleUserClient(){ Name=x.Name, Address=x.Address }) })
 {
  DoSomething(user.User);
  DoSomethingWithClients(user.Clients);
 }
}

これでSqlの実行は一度だけになりDBのリソースを無駄に消費することはなくなった。最後に上記のコードから生成されるSqlを紹介しておこう。(実際に生成されたSqlの不要な部分を削ったりしてあります)

SELECT [t0].[UserId], [t0].[UserName], [t1].[Name], [t1].[Address](
SELECT COUNT(*)
FROM [dbo].[UserClients] AS [t2]
WHERE [t2].[UserId] = [t0].[UserId]
) AS [value]
FROM [dbo].[Users] AS [t0]
LEFT OUTER JOIN [dbo].[UserClients] AS [t1] ON [t1].[UserId] = [t0].[UserId]
ORDER BY [t0].[UserId], [t1].[Id]

Co-opの彼が担当した部分に今回紹介したようなコードが大量に散見されたために卒倒しそうになったので、今回紹介したようなコードを安易に大量生成するのはやめよう。

2009年11月18日水曜日

IModelBinderの話

今回はカスタムModelBinderを解説する。ModelBinderとはリクエストパラメータとControllerのActionの引数になっている型を見て、リクエストパラメータがその引数に変換可能であれば値を変換・設定してくれるという何とも素晴らしい機能のことだ。

ModelBinderの動作は下記のようになる。

-Model-
class Clip
{
 public string Description { get; set; }
}

-View-
<%Html.TextBox("Description")%>

-Controller-
pubilc ActionResult AddClip(Clip clip)
{
 var description = clip.Description;   // You can directly retrieve Description here!!
 // code here...
}

上記の例でModelBinderは次の処理を行っている。Controller.AddClipの引数にClipクラスがあり、そのクラスのプロパティにDescriptionがあるのでリクエストパラメータのDescriptionという名前の値をClip.Descriptionに代入している。

と、このようにModelBinderは値の変換、代入という煩雑な処理を一手に引き受けてくれる大変便利でシンプルな構造になっているのが理解できたかと思う。しかし、Viewが複雑になってくると独自にバインド処理を記述したくなる場面がでてくる。その方法をここから解説しよう。

ここではViewから配列情報を格納したJsonを文字列として受け取り、そのJsonをListクラスに変換・代入するという男気あふれる処理を行う。

-Model.cs-
[ModelBinder(typeof(PathModelBinder))]
public class PathModel
{
 public string Title { get; set; }
 public List<PathItemModel> Paths { get; set; }
 public string PathsJson { get; set; }

 public void SetModelValue(ModelStateDictionary modelState)
 {
  modelState.SetModelValue("Title", new ValueProviderResult(this.Title, this.Title, null));
  modelState.SetModelValue("PathsJson", new ValueProviderResult(this.PathsJson, this.PathsJson, null));
 }
}

public class PathModelBinder : IModelBinder
{
 public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
 {            
  var list = JsonConvert.DeserializeObject<List<PathItemModel>>(bindingContext.ValueProvider["PathsJson"].AttemptedValue);
  var model = new PathModel()
  {
   Paths = list,
   Title = bindingContext.ValueProvider["Title"].AttemptedValue,
   Description = bindingContext.ValueProvider["Description"].AttemptedValue,
   PathsJson = bindingContext.ValueProvider["PathsJson"].AttemptedValue
  };
  return model;
 }
}

-Controller.cs-
public ActionResult AddPath(PathModel path)
{
  if (!_service.AddPath(path))
  {
      path.SetModelValue(this.ModelState);
      return View("AddPath", path);
  }
  return RedirectToAction("Index");
}

上記の例では、Controller.AddPathは引数としてPathModelを受け取る。このPathModelはModelBinder属性が指定してあり、その指定でバインド処理をPathModelBinderで行うと明示してある。そのPathModelにはList<PathItemModel>というプロパティがあるのだが、その値はポストされたPathsJsonを解析して生成されるものなのでその処理をPathModelBinderで行っている。実際の処理はPathModelBinderのBindModelを見てもらえば一目瞭然なので省略する。

以上がカスタムModelBinderを使用するために必要な実装だ。どうだろう、かなりシンプルにできているのが実感できたと思う。これでController内に「値の変換を行う」という処理がなくなり、Controller本来の役目であるフローの制御に集中できるようなった。より関心の分離が進んだ状態というわけだ。

最後に一点注意が必要なのがPathModelのようにカスタムModelBinderを使用しているModelをViewへ渡す場合だ。その場合は、PathModelのSetModelValueで行っているようにModelStateへ値を設定してやらないとViewで値の取得ができないので忘れずに実装してほしい。

2009年11月12日木曜日

Google maps APIのGClientGeocoderの使い方

今回はGoogle maps APIのGClientGeocoderの使い方を解説する。GClientGeocoderはクライアントスクリプトから直接Googleの膨大な地図情報にアクセスできるという大変便利なクラスだ。

呼び出し方法は下記のようになる。

var geocoder = new GClientGeocoder();
var center = map.getCenter();
geocoder.getLocations(
 center, 
 function(response) {
  if (!response || response.Status.code != 200) {
   alert("データが取得できませんでした");
  }
  else {
   var place = response.Placemark[0];
   var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]); 
   // Code here...
   var address =  place.AddressDetails.Country.AddressLine[0];
   // Retrieve address for whatever you want to...
  }
});

getLocationsのオーバーロードとして、文字列の住所を受け取るものと、GLatLngを受け取るものと両方あるので用途に分けて使用して欲しい。

getLocationsで返却されるresponseの中身であるJSONクラスは下記になる(このページより抜粋)。

{
  "name": "1600 Amphitheatre Parkway, Mountain View, CA, USA",
  "Status": {
    "code": 200,
    "request": "geocode"
  },
  "Placemark": [
    {
      "address": "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA",
      "AddressDetails": {
        "Country": {
          "CountryNameCode": "US",
          "AdministrativeArea": {
            "AdministrativeAreaName": "CA",
            "SubAdministrativeArea": {
              "SubAdministrativeAreaName": "Santa Clara",
              "Locality": {
                "LocalityName": "Mountain View",
                "Thoroughfare": {
                  "ThoroughfareName": "1600 Amphitheatre Pkwy"
                },
                "PostalCode": {
                  "PostalCodeNumber": "94043"
                }
              }
            }
          }
        },
        "Accuracy": 8
      },
      "Point": {
        "coordinates": [-122.083739, 37.423021, 0]
      }
    }
  ]
}

GClientGeocoderが独自のCacheメカニズムを実装しているので同じアドレスであればServerへのRound tripは発生しない。しかし、複数のアドレスを問い合わせるためにGClientGeocoderをループ内で複数回使用することは推奨されていないので、その場合はサーバ側などでHTTP Geocoderを使用するのがよいだろう。

2009年11月8日日曜日

Googleが提供するJavaScriptコンパイラ:Closure Compilerの紹介

GoogleがGmail、Google docs、Google mapsなどの開発用に使用しているツール群をオープンソース化するということで、その第一弾としてJavaScriptのコンパイラ:Closure Compilerが公開されたのでここで紹介する。

コンパイラと言っても実際にマシン語などに変換するわけではなく、より良いコードへと最適化してくれる、という表現のほうが正しい。また文法的な間違いも検出してくれるのでJavaScript開発者にとってはかなり嬉しいツールとなるだろう。

使い方は簡単でこのページにコードをペペッとはっつけてあとはCompileボタンを押下するだけ。これだけで右側に文法チェック後に最適化されたコードが出力される。エラーやらワーニングがなければペペッとコピーするなり「The code may also be accessed at <ファイル名>」からファイルをダウンロードすればよい。ちなみにこのファイルは一時間限りの限定的なものなので、開発時において一時的に直接参照するのは良いだろうが、本番環境にあるファイルから直接参照するようなことはやめよう。

また、上記で紹介したWeb上のツールも短いコードをテストする分には良いけれど、少しでも規模が大きくなってくると毎回の設定が面倒になってきてしまうだろうから、こちらのAPIを使用することが推奨されている。使ってみて勝手が分かってきたらこちらのほうもおいおい紹介しようと思っている。

今まではコードの軽量化のためにjsminのC#コードを少しいじって使用していたけれど、文法チェックまで行えるClosure Compilerのほうを今後はメインに使っていくことになるだろう。

2009年11月4日水曜日

Google mapsをブラウザ幅にあわせて伸縮させる方法

Google codeにあるGoogle mapsのサンプルそのままでは、地図をブラウザ幅にあわせて伸縮させた際にJavaScriptの解析エラーが出てしまう。今回はその対処方法を紹介する。

サンプルのコードは以下。
if (GBrowserIsCompatible()) {
 var map = new GMap2(document.getElementById("map_canvas"));
 map.setCenter(new GLatLng(37.4419, -122.1419), 13);
 map.setUIToDefault();
}

ちなみにエラーの理由はsetUIToDefault()にあるようなので、それをのけて下記のように手動で設定すればよい。

if (GBrowserIsCompatible()) {
 var  map = new GMap2(document.getElementById("map_canvas"));
 map.enableContinuousZoom();
 var ui = new GMapUIOptions();
 ui.maptypes = { normal: true, physical: true, satellite: true, hybrid: true };
 ui.zoom = { doubleclick: true, scrollwheel: true };
 ui.controls = { largemapcontrol3d: false, smallzoomcontrol3d: true, scalecontrol : true, maptypecontrol: false, menumaptypecontrol : true };
 map.setUI(ui);
}

ここでは、ダブルクリックでのズームアニメーションの有効化(enableContinuousZoom())、表示するマップ種類の設定(maptypes)、UI操作での地図ズーミングの設定(zoom)、地図上に表示するUIコントロールの設定(controls)などを行っているので参考にして欲しい。もちろん言うまでも無いことだが詳細情報はAPI Referenceを参照してほしい。