Entity Frameowork Feature CTP4 のCode Firstをおさらいしてみる
既にあちらこちらで、紹介されていますが、Entity Framwork Feature CTP4(EF)でコードファーストスタイルの開発に対応したので、土日におさらいしてみたメモ(冗長)です。
というのも、先週、某飲み会で、
「まあ、EFもCode Firstに対応して、まあ、だいぶましになってきましたよ~」
などとしゃーしゃーと知ったかぶりをしてしまったのであわてて、おさらいです。
Code Firstについては、ある程度理解してる(つもりな)のでVS+CRUD (ASP.NET MVC) との組み合わせや、(SQL)Azureを自動生成先のDBとして使えるの?とかのテストです。
で、
EFをダウンロードしてきてドキュメント見るも、使用方法などについては、全く書いていない・・・(最近はMSのドキュメントもかなりまともになってきたので読むようにしてたのに・・・)。
あきらめて、最近すっかりメジャーになったScott (G)さんのブログを読む。
が、
「サンプルが冗長だよ!Scott (G)~」
と、飽きっぽい私には、サンプルが少し冗長に感じられたため、もっとシンプルなサンプルを探し回る(いや、素晴らしいサンプルですよ。でも、テーブルが2つあって、リレーションが組まれてる時点でサンデープログラマの私には十分複雑)。
すると、もう一人のScott (Hanselman)さんのブログに行き着く。世の中のScottさんは、みんなASP.NET好きなのか?と思いながら読み進める(両氏ともASP.NET MVCでは、かなり著名です)。
Scott(H)さんのブログを読むにつれ、今度は、
「ハショリ過ぎだよ!Scott (H)~」
と文句(独り言)が出る・・・。
「概念はわかってるからさ~、それをVSでどうやるかおせーてよ~」
とか思う。
ソースを見るに、たぶん、Scott (G)は几帳面、Scott (H)はおおざっぱ。たぶん友達になれるのはHの方だな・・・などと勝手なことを思いつつ、調査終了。
いやいや、ダブルScottのおかげで十分に理解できました(たぶん)。感謝、感謝。
以下メモ (どちらかというとScott Hさんのやつを参考(パクリ))
Scott(H)さんにハショリ過ぎと文句を言っておきながら、かなりのハショリです。ちなみに、まだ一度もMVCやCRUDにさわったことのない人には、ピンとこないと思うので、事前に@ITさんの記事などを見るといいと思います。
全体の流れとしては、
- データモデルを(コードで)記述する
- コントローラーおよびビューを記述する
- (自動生成されたDBを確認する)
- 必要な個所をいじる
という感じです。
では、まず、VSを立ち上げて参照の追加など。前準備です。
VSを立ち上げて、プロジェクトを新規作成し、テンプレートからASP.NET MVC 2 Webアプリケーションを選びます。
こんなキャプチャいらんですね。ディスクの無駄。名前はCodefirstSampleとしました。
次は、参照の追加です。
Microsoft.Data.Entity.Ctpを追加します。
これで前準備完了。
さて、いよいよCode Firstなので、(データに関する)コードを書いていきます。
Modelsフォルダを右クリック、追加を選択し、C#クラスを追加します。
クラス名はScottを見習い、Bookとしました。が、Scott (H)より、さらに短く、こんな感じ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace CodefirstSample.Models
{
public class Book
{
[Key]
public int isbn { get; set; }
public string title { get; set; }
public int price { get; set; }
}
}
制約上、[Key]はハショレ無いので・・・。なお、この属性を使うためには、System.ComponentModel.DataAnnotationsにUsingをかけます。
次に、DbContextを継承した、データを永続化させるためのクラスを定義します。同じく、ModelsにC#のクラスを追加する形で行います。まあ、さっきのやつがテーブルやカラムの設定なら、このクラスはDBの設定に相当します。
名前はSimpleBookCatalog。コードはこんな感じ。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Data.Entity;
namespace CodefirstSample.Models
{
public class SimpleBookCatalog : DbContext
{
public DbSet<Book> Books { get; set; }
}
}
ちょー簡単。
このコードのため(DbContext)にはSystem.Data.EntityにUsingをかけます。
以上でModelsに対するコード書きは終わり。
では、次に、コントローラーおよびビューを定義していきましょう。
Controllerフォルダを右クリック、追加を選択してコントローラーを追加します。
ここではBookControllerとしました。なお、メソッドを追加するにチェックを入れます。
Viewを追加する前に、Viewフォルダ以下にBookフォルダを作ります。Viewフォルダを右クリックすると「フォルダーの追加」がありますので、それで追加します。
下準備はできたので、ControllerおよびViewのコードを書いていきます(と言ってもほとんどテンプレートで自動生成されますが)。
各機能を実装する前に、全体として、
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using CodefirstSample.Models;
モデルにUsingをかけてやるのと、
namespace CodefirstSample.Controllers
{
public class BookController : Controller
{
SimpleBookCatalog db = new SimpleBookCatalog();
クラスの頭で全体で使えるよう、エンティティのインスタンス(db)を宣言しております。
では、とりあえず、Index(List表示)機能を作りましょう。さっき作ったView以下のBookフォルダを選択、右クリックして追加します。
「ビューの追加」ダイアログが開くので、こんな感じに設定します。ちなみに、私はマスターページは使わない派です。
なお、このとき「ビューデータクラス」に何も表示されない場合、ソリューションを一度ビルドしてください。
コードが書けたら(と言ってもViewは何もしてませんが)、実行してみます。
「F5」
を押します。で、localhost/Bookを表示させてみます。
リストこそ表示されませんが、別にエラーを吐くわけでもなく。淡々と表示されます。
まあ、結論から言えば、この時点?でデータベースが自動生成されております。SSMSなどを立ち上げてlocalhost/sqlexpressの状態を確かめてみましょう。
stringはnvarchar(4000)にマップされています(大盤振る舞い!)。もちろん、これは(データベース側で)変更することができます。
EF CTP4では、何も指定しないとローカルのSQLExpressにデータベースおよびテーブルを自動生成します。これらは、SQL CE 4に変更したり、SQL Server、SQL Azureなどに変更可能で、そのためにはWeb.configのconnectionStringなどをいじります(最後の方にSQL Azureのサンプルは書いてます)。
ここまで来ると、後は通常のCRUDと同じです。一応、Createだけ見ておくと、
コントローラーのコードはこんな感じで、
// GET: /Book/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Book/Create
[HttpPost]
public ActionResult Create(Book book)
{
try
{
// TODO: Add insert logic here
db.Books.Add(book);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
//
Viewの追加はこんな感じ。
後は、一連の流れでDetails、Edit、Deleteを追加するだけです。
強いて従来の手法との差を書くと、Viewのコードにおいて、各ページ(List,Create,Edit,Detail,Delete)リンクにおいて、パラメータ生成部がコメントアウトされていて、正しく機能しませんので、その辺を修正する必要があります。
あと、以前のスタイルでは、Deleteの際に、
db.DeleteObject(book)
db.SaveChange()
としてたのですが、DeleteObjectが使えず、
db.Books.Remove(book)
db.SaveChange()
という感じでRemoveを使いました。
パラメータのコメントアウトに関しては、下記のように適切な記述に置換しました(書いた方が早かったかも)。
と
です。何を置換したかは、キャプチャを参考にしてください。
さて、Code Firstとしてはこれ以上説明することがあまりないのですが、せっかくなのでDataAnnotaionsを試しましょう。
Scottも書いておりますが、便利なDataAnnotationsによるデータ検証が利用できます。というか、Scott (H)さんは、
「a more fluent way in my database context class ~」とか言って、builderをつかって検証項目を追加する方法を紹介していますが、ここでは無視して、従来のやり方で。
先ほどのBookクラスのコードを、
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace CodefirstSample.Models
{
public class Book
{
[Key]
[Required]
public int isbn { get; set; }
[Required]
public string title { get; set; }
[Required]
public int price { get; set; }
}
}
という具合に変更し(ちなみにすべての項目を入力必須にしています)、さらにBookController.csのCreateとEditの部分に、
[HttpPost]
public ActionResult Create(Book book)
{
if (!ModelState.IsValid)
{
return View();
}
try
{
// TODO: Add insert logic here
db.Books.Add(book);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
コードを追加します(赤字部分)。ModelState.IsValidはエラーなど、何も問題がなければTrueを返すもので、ここでは、エラーだと元のページ?に戻るようにしています。
そして、実行すると・・・。
このようなエラーがでます(出る場合があります)。 どうやら、データモデルに変更が生じたため、データベースとの整合性がどうしたということのようです。
このような問題を解決するためには、Scottも言っているようにデータベースの生成ポリシーを必要に応じ定義する必要があります。
例えば、今回のようなに開発中でデータ構造が変わるような場合、global.asaxのApplication_Start()に、
Database.SetInitializer(new RecreateDatabaseIfModelChanges<SimpleBookCatalog>());
などと記述すればよいようです。が、私の環境では、
などという、本末転倒なエラーがでました(が、ちゃんと変化を認識してDBをリセットしようとしている気持ちは伝わってきました)。
めんどくさいので、今回は手動でデータベースを一度消しました・・・。まあ、CTPですから(って、私のやり方が悪い可能性大)。
そして、実行すると、
ちゃんとエラー処理されます。これは、サーバサイドの処理ですが、簡単にクライアントサイドの実装もできます。
Annotationsはこんなところでしょうか。
さて、最後に、私的には一番の本丸。SQL AzureでCode Firstを試さないといけません。
SQL Azureに対してCode Firstを行う場合、Web ConfigのconnectionStringをいじります(ろかーるのSQLでも同じですけど)。
このとき、接続文字列名(name)は、DbContextクラス名と同じ名前にしておく必要があります。接続文字列はSQL Azureの管理画面から生成すると簡単です。
ただし、Azureは存在しないデータベースへの接続文字列は生成できないので、とりあえず、masterとかを対象に生成し、Databaseの名前だけ変えます。
そして、「F5」でアプリケーションを実行しlocalhost/Bookを閲覧したところ、、、
こんなエラーがでました。
master dbをいじる権限がいる的なエラーがでます(もちろん、その権限があるユーザでアクセスしています・・・)。
が、SQL Azureの管理画面を見ると、
どうやらデータベースは生成され、
最近お気に入りのHoustonで確認してみると、
ローカルのSqlExpressと同じテーブルがデザインされていました。 再度、アプリケーションを実行すると、問題なく動作しました。
Createした後、Indexに戻り、一覧表示しているところです。
Houstion上で確認すると、当然ですが、きちんと登録されました。もちろん日本語もOKです。
考察
RoRの時もそうでしたが、少し複雑な処理や、標準以外のDBでCode-Firstをやろうとすると、結局いろいろな定義ファイルをいじらなくてはいけなくなり、
「データベースいじった方が早くない?」
という本末転倒論が発生してしまいます。が、私としては、Code Firstは、まず、それができるということに意味があることと、当初はDBにためるつもりが無かったクラスを永続化しておく手法として魅力的だなという感じです。
以上。
参考資料
Controlleのソースは下記の通りです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using CodefirstSample.Models;
namespace CodefirstSample.Controllers
{
public class BookController : Controller
{
SimpleBookCatalog db = new SimpleBookCatalog();
//
// GET: /Book/
public ActionResult Index()
{
var books = from b in db.Books
select b;
return View(books);
}
//
// GET: /Book/Details/5
public ActionResult Details(int id)
{
var book = (from b in db.Books
where b.isbn == id
select b).First();
return View(book);
}
//
// GET: /Book/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Book/Create
[HttpPost]
public ActionResult Create(Book book)
{
if (!ModelState.IsValid)
{
return View();
}
try
{
// TODO: Add insert logic here
db.Books.Add(book);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
//
// GET: /Book/Edit/5
public ActionResult Edit(int id)
{
var book = (from b in db.Books
where b.isbn == id
select b).First();
return View(book);
}
//
// POST: /Book/Edit/5
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
// TODO: Add update logic here
var book = (from b in db.Books
where b.isbn == id
select b).First();
UpdateModel(book);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
//
// GET: /Book/Delete/5
public ActionResult Delete(int id)
{
var book = (from b in db.Books
where b.isbn == id
select b).First();
return View(book);
}
//
// POST: /Book/Delete/5
[HttpPost]
public ActionResult Delete(int id, FormCollection collection)
{
try
{
// TODO: Add delete logic here
var book = (from b in db.Books
where b.isbn == id
select b).First();
db.Books.Remove(book);
db.SaveChanges();
return RedirectToAction("Index");
}
catch
{
return View();
}
}
}
}