2011年1月18日火曜日

ASP.NET MVC 3とEntity Framework Code Firstを触ってみた SQL Server編

ASP.NET MVC 3がリリースされたので、Entity Framework Code First:CTP5と一緒に評価してみた。今回のデータストレージはSQL Server 2008を使用するがそのうちMySQLを使って解説したい。

・セットアップ
それぞれ下記からインストールして欲しい。
ASP.NET MVC 3
EF Code First CTP5

EF Code FirstはNuGetを使用したほうがより簡単に導入できるのでそちらの方法をお勧めする。NuGetを使用しての詳細な説明はこちら。NuGetでインストールする方法は、View->Other Windows->Package Manager Consoleで“Install-Package EFCodeFirst”と入力するだけだ。

はじめてASP.NET MVCを触る人は下記を参考にしてもらいたい。
Intro to ASP.NET MVC 3
ASP.NET MVC3の基礎を知ることができるので有用だ。これと合わせてユニットテスト用にRepositoryパターンなども勉強すると良いだろう。

・今回のアプリ
今回は次のようなWebアプリを作成する。武将の一覧があり、武将の登録、編集、削除が行え、かつ武将から武将へのコメントも行える。もちろんコメントの削除、編集も行える。

Index.cshtml

Create.cshtml - 検証(StringLength)

Create.cshtml - 検証(Required)

AddComment.cshtml

・EF Code First
今回はEntity Framework Code Firstでデータストレージを作成するので前もってDatabaseやTableを用意しておく必要がない。Data Modelは下記になる。

public class Bushou
    {
        public int BushouID { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }

        public virtual ICollection<Comment> Comments { get; set; }
    }

    public class Comment
    {
        public int CommentID { get; set; }
        public int CommentedBushouID { get; set; }
        public int CommentBushouID { get; set; }
        public string Text { get; set; }
   
        public virtual Bushou CommentedBushou { get; set; }
        public virtual Bushou CommentBushou { get; set; }
    }

    public class Sengoku : DbContext
    {
        public DbSet<Bushou> Bushous { get; set; }
        public DbSet<Comment> Comments { get; set; }

        protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Comment>().HasRequired(c => c.CommentedBushou)
                                  .WithMany(m => m.Comments)
                                  .HasForeignKey(c => c.CommentedBushouID)
                                  .WillCascadeOnDelete();

            modelBuilder.Entity<Comment>().HasRequired(c => c.CommentBushou)
                                          .WithMany()
                                          .HasForeignKey(c => c.CommentBushouID)
                                          .WillCascadeOnDelete(false);
        }
    }

Bushouクラス、Commentクラスともにvirtual属性以外のものがいずれTableの各列となる。またEF Code FirstはConvention(規約、協定みたいなもの)があり、たとえばBushouクラスであればID、またはBushouIDとなっているものを主キーとみなし自動的に設定を行ってくれる。また外部キーも同様に、今回の例ではないがCommentクラスにBushouIDなどがあればそれを外部キーとして設定を行ってくれる。ここでは、コメントされる人、コメントする人、と2つの外部キーがあるためConventionの法則に則っていないのでその設定は自動で行われない。そういう場合はDbContextのOnModelCreatingを使い自前で設定を行う。OnModelCreatingで行っている処理はFluent APIと呼ばれるもので、大抵の設定はこれで行えてしまう。Fluent APIの詳細な資料はこちらを参照してほしい。

他にも[Key]属性や[ForeignKey]属性などプロパティに設定することでFluent APIを使用せずとも明示的にフレームワークに通知することができる。

・Error Validation
EF Code FirstはData Annotationもサポートされている。つまり[Required]属性や[MaxLength]属性などでデータの説明を行えるようになっている。さらにData AnnotationはASP.NET MVC3と完璧に協調するのでクライアントサイド検証や、サーバサイド検証をフレームワークが自動で行ってくれる。しかしここに一つ問題がある。というのもData Annotationのデフォルト言語は英語のためエラーメッセージはすべて英語になってしまう。ここで前述のData Modelを検証と日本語に対応したものに更新しよう。

public class Bushou
    {
        public int BushouID { get; set; }

        [Required(ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "Required")]
        [StringLength(10, ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "StringLength")]
        [Display(Name="名称")]
        public string Name { get; set; }

        [Required(ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "Required")]
        [StringLength(10, ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "StringLength")]
        [Display(Name = "拠点")]
        public string Address { get; set; }

        public virtual ICollection<Comment> Comments { get; set; }
    }

    public class Comment
    {
        public int CommentID { get; set; }

        [Display(Name = "コメントされる人")]
        public int CommentedBushouID { get; set; }

        [Display(Name = "コメントする人")]
        public int CommentBushouID { get; set; }

        [Required(ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "Required")]
        [StringLength(30, ErrorMessageResourceType = typeof(ErrorMessages), ErrorMessageResourceName = "StringLength")]
        [Display(Name = "コメント")]
        public string Text { get; set; }

        public virtual Bushou CommentedBushou { get; set; }
        public virtual Bushou CommentBushou { get; set; }
    }

BushouクラスのNameプロパティやAddressプロパティにそれぞれRequired属性とStringLength属性が付与されている。またErrorMessageResourceTypeでリソースクラスの指定、ErrorMessageResourceNameでリソースキーの指定を行っている。リソースクラスを使う方法以外にもErrorMessageを直接指定する方法があるが、ここでは前者を採用している。ちなみにこのリソースクラスを使用する方法は多言語対応と同じ方法だ。

リソースクラスの追加方法を説明する。まずは下図のようにASP.NETフォルダの追加からApp_GlobalResourcesフォルダを追加し、ErrorMessages.ja.resxとErrorMessages.resxを追加しよう。


ついで、ErrorMessages.ja.resxに下図の内容を追加する。その際にAccess ModifierをPublicにすることを忘れないように注意しよう。


またErrorMessages.resxにも同様のキーを追加しておこう。ここにキーを追加しておかないと実行時にリソースキーが見つからないというエラーになる。

※Data Annotationのデフォルト言語を英語以外にする方法は現状無い
日本語のみを対象とするアプリケーションでは今回のような多言語対応の方法だとかなり冗長になる。そのためそれぞれリソースクラスを指定する方法ではなく、Data Annotationのデフォルトエラーメッセージ自体を変更できないかと色々と調べたけれどその方法はなさそうだ。というのもReflectorでSystem.ComponentModel.DataAnnotations.dllの中身を調べたけれど、エラーメッセージ表示部分で内部リソースを直接参照しており、そのリソースをアセンブリ外部から操作できないようになっていた。

・ASP.NET MVC3
ここからASP.NET MVC3のアプリケーションの説明をする。新規プロジェクトからASP.NET MVC 3 Web Applicationを選択し、Emptyプロジェクトを選択しよう。名前は適当につけてほしい。

まず、ControllersフォルダにHomeControllerを追加しよう。なぜHomeかというとデフォルトRouteを変更するのが面倒だからだ。デフォルトRouteを変更したい場合はGlobal.asax.csでRouteのマッピングを行っているのでそこで変更すればよい。

まず最初にIndexを追加する。

Sengoku db = new Sengoku();
        public ActionResult Index()
        {
            return View(db.Bushous.ToList());
        }

ここでは面倒なのでSengokuはHomeControllerのフィールド変数としているけれど、実際のプロジェクトではユニットテストでMockオブジェクトを使用するためにデータアクセス層を分けるのでこのような実装をすることはない。詳しくはRepositoryパターンを調べ欲しい。

またSengokuクラスをインスタンス化しているが、EF Code FirstのConventionの一つでクラス名と同一のコンフィグキーを探し、その接続文字列でDatabaseに接続するというのがある。そのため下記の接続文字列をWeb.configに追加しておこう。

<add name="Sengoku" connectionString="Data Source=.;Initial Catalog=Sengoku;Persist Security Info=True;User ID=yoo;Password=matsuosoftwareisgreat" providerName="System.Data.SqlClient" />

また最初のほうで言及したように接続時にSengoku Databaseが無ければEF Code Firstは定義されている内容でDatabaseを作成してくれる。今回はSQL Server 2008を使っているがExpressでも同様の動作をする。

ついで、Index.cshtmlを追加する。Index()で右クリック→Add ViewからIndex.cshtmlを追加しよう。その際にCreate a strongly-typed viewにチェックをして型を指定するとScaffold templateを指定できる。ちなみにここではIListを指定しているのでtemplateの選択はできない。


追加されたIndex.cshtmlを編集したもの。View追加時に指定したようにView EngineにRazorを使用している。Razorの文法についてはこちらを参照してほしい。

@model IList<EFCodeFirst.Models.Bushou>
@{
    ViewBag.Title = "武将一覧";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
    一覧</h2>
@Html.ActionLink("武将登録", "Create")
<div style="margin-top:20px;">
    @foreach (var bushou in Model)
    { 
        <div style="padding:10px;border:1px dashed gray">
            <div style="width:320px;float:left;">
                @Html.ActionLink(bushou.Name, "Edit", new { id = bushou.BushouID }) @string.Format("({0}在住)", bushou.Address)
            </div>
            <div style="float:left;">
                @Html.ActionLink("削除", "Delete", new { id = bushou.BushouID }) |
                @Html.ActionLink("コメント追加", "AddComment", new { id = bushou.BushouID })
            </div>
            <div class="clear"></div>
        @foreach (var comment in bushou.Comments)
        {
            <div style="width:300px;float:left;padding-left:20px;">
                @Html.ActionLink(comment.Text, "EditComment", new { id = comment.CommentID }) by @comment.CommentBushou.Name
            </div>
            <div style="float:left;">
                @Html.ActionLink("削除", "DeleteComment", new { id = comment.CommentID })
            </div>
            <div class="clear"></div>
        }
        </div>
    }
</div>

@modelでこのページのModelの型を指定している。またLayoutで使用するASP.NETで言うところのマスターページを指定している。内部的には何も難しいことはしておらず、Modelをforeachでぐるぐると回してタグを作成している。ここで一点注目して欲しいのはコメントタグ作成部分だ。ちゃんとcomment.CommentBushou.Nameが取得されているのが分かる。

次にCreateを追加する。

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

        [HttpPost]
        public ActionResult Create(Bushou bushou)
        {
            if (ModelState.IsValid)
            {
                db.Bushous.Add(bushou);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            else
                return View(bushou);
        }

とくに難しいことも無いので説明は省略する。Viewの追加からBushouクラスをModelとしたCreate.cshtmlを追加する。

@model EFCodeFirst.Models.Bushou

@{
    ViewBag.Title = "武将登録";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>武将登録</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>武将</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Address)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Address)
            @Html.ValidationMessageFor(model => model.Address)
        </div>

        <div style="clear:both"></div>
        <p>
            <input type="submit" value="登録" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("一覧へ戻る", "Index")
</div>

クライアントサイドの検証を有効にするためにはWeb.configのClientValidationEnabledとUnobtrusiveJavaScriptEnabledをTrueに設定しておく必要がある。ASP.NET MVC3ではデフォルトでTrueになっている。また_Layout.cshtmlで下記スクリプトが指定されていることを確認しよう。

<script src="@Url.Content("~/Scripts/jquery-1.4.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

これでStringLengthで指定されている文字数以上を入力したり、Required指定されている項目を未入力にしたりするとエラーメッセージが表示される。

・まとめ
ここまで見てきて分かると思うがかなり開発しやすくなっている。RazorがデフォルトのView Engineとして提供されたおかげでプログラムコードとHtmlの分離が簡単になった。またData Annotation機能がさらに進化したおかげで検証の組み込みも簡易に行えるし、一つの設定でViewからDatabaseまで有機的に協調できているのが素晴らしい。何よりEF Code FirstはDatabaseの更新を容易に行えるという以上にユニットテストにおいて同一のData Modelが使えるというのが良い。実際に触ってもらうと分かると思うがより開発速度が高まったのを実感できると思う。

他にも今回のアプリではEditやAddCommentといった機能があるが大体ここまでで説明してきたことと一緒なので省略する。興味がある人はソースコードを添付しておくので下記から取得してほしい。


ソースコード

次回はデータストレージをMySQLにした場合の解説をする予定だ。

2 件のコメント:

  1. 理解しやすく説明してくださってありがとうございます。
    すごく勉強になります。
    よるしければ、私のBlogにこの投稿を載せてもいいでしょか。

    返信削除
  2. Hyunsikさん
    コメントありがとうございます。お役に立つようでしたらどうぞ遠慮なく使ってやってください。

    返信削除