ということで、続きました。
LINQ to EntityがMonoで使えないとなると、やっぱりLINQ to MySQLを使おうかなというところに戻りますよね。これがまたなんだかんだとがんばってみたんだけど...。ガッカリな結果でした。理由はサッパリ分からない。最初はライセンスの問題なのかな~、とかいろいろ試してみたけどできなかった。
↑これMySQLとxsp2(Mono)で動かしてるんだけど、ちゃんとログインできてるのが確認できると思います。なのでMembershipProvider経由のDBアクセスは成功ってことです。だけど、Sakilaを使ったサンプルはやっぱりダメで。
左が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でできるのかもしれないけど、ここもスキップして実行!
おぉ~。チュートリアルと同じ結果だ。そりゃそうだ。今度はコレをASP.NET MVCで実装。すったもんだの末に動くようになりました。
ぱっと見、普通ジャンって思えるけど、コレ実行環境は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はアセンブリのローカルコピーを忘れずにね。