2. エンテェティとマッピング
1.環境設定でコンソールアプリケーションの作成と参照設定が終わっているとします。サンプルでは以下のデータベーススキーマが想定されています。
- Store(店舗)
- Employee(従業員)
- Product(商品)
- StoreProduct(多対多解決テーブル)
Storeを中心に考えると
- Employee(従業員)はStore(店舗)に依存し(1対多)
- Product(商品)とStoreの関係は一義的に決められない(多対多)
の関係です。
まあ、兼務という概念がないということですね。
多対多は組合せテーブル(StoreProdust)を別途作ることで管理します。
つぎに、プロジェクトのフォルダ構成を準備します。
ルート以下に
- Entities
- Mappings
フォルダを作成し、下図の構成にします。
これが、Fluent NHibernateの推奨フォルダ構成のようです。
Entities
さあ、実装を始めます。
Entityとは、データベースのテーブルをプログラミング言語サイドから定義したものというところでしょうか。何気ない単語もイメージが湧きにくいものです。簡単に言えば、テーブルの項目を含むクラスです。
初めは「Employee」エンテェティです。
public class Employee { public virtual int Id { get; set; } public virtual string FirstName { get; set; } public virtual string LastName { get; set; } public virtual Store Store { get; set; } }
(Entities\Employee.cs)
ここで、NHibernateに不慣れな方に2つの注意点です。
- Idのプライベートセッター(Setter)
- virtualキーワード
です。
IdがNHibernate経由以外で更新されないためと、NHibernateはデータベースアクセスを効率化する上で、遅延バインディング(lazy loading)をするためにエンティティのプロキシを作成することが理由です。
まあ、これもお作法といったところでしょうか。
プライベートセッターがあると実行時エラーになるようです。(2012.10.4)
現状はprivateは付けないでください。
Employeeクラスには
- Int型のId
- String型のFirstName
- String型のLastName
- Store型のStore
が、含まれます。
EmployeeクラスにはStoreが1つだけ存在することがポイントです。
つまり、従業員が決まれば、勤務先もただ1つ決まるということです。(1対多)
次に「Product」エンテェティです。
public class Product { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual double Price { get; set; } public virtual IList<Store> StoresStockedIn { get; set; } public Product() { StoresStockedIn = new List<Store>(); } }
(Entities\Product.cs)
ProductクラスはID、NameとPriceで構成されているので
- Int型のId
- String型のName
- double型のPrice
は、問題ないと思います。
最後のIList<Store>は多対多を表現するために
Productインスタンスが複数のStore情報を保持するためのものです。
これもプライベートセッターになっていることは注意が必要です。
あとは、コンストラクタでListをインスタンス化しているだけです。
最後は「Store」エンティティです。
public class Store { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual IList<Product> Products { get; set; } public virtual IList<Employee> Staff { get; set; } public Store() { Products = new List<Product>(); Staff = new List<Employee>(); } public virtual void AddProduct(Product product) { product.StoresStockedIn.Add(this); Products.Add(product); } public virtual void AddEmployee(Employee employee) { employee.Store = this; Staff.Add(employee); } }
(Entities\Store.cs)
StoreエンテェティはIdとName以外に、ProductエンテェティとEmployeeエンテェティのコレクションをメンバとして持ちます。
コンストラクタでは両コレクションの初期化を行います。
Storeエンテェティは簡単なロジックを持ちます。
AddProductとAddEmployeeで、それぞれメンバのコレクションに追加したいインスタンスを引数に持ちます。
ロジック内部では、それぞれの引数のStoreまたはStoreStockInメンバにthis(Store自身)を登録し、それをStoreインスタンスのコレクションに追加しています。ちょっと分かりにくいですが、Storeと(Product or Employee)両方に登録されることが何となく感じられると思います。
Mappings
エンテェティの実装の次は、マッピングです。
ここからは、Fluent NHibernateの流儀に合わせていく必要があります。
まずは、名前空間
- FluentNHibernate.Mapping
- [SampleAppNameSpace].Entities
をusing設定しておきます。
マッピングクラス名は(Entity名+Map)が一般的です。
また、マッピングクラスはClassMap<T>を継承します。
public class EmployeeMap : ClassMap<Employee> { public EmployeeMap() { Id(x => x.Id); Map(x => x.FirstName); Map(x => x.LastName); References(x => x.Store); } }
(Mappings\EmployeeMap.cs)
マッピングはコンストラクタの中に記述します。
ここでは、ラムダ式が多用されています。
5行目は、xというEmployeeクラスをラムダ式の引数にするとEmployee.Idが戻ってきて、それがIdメソッドの引数となっていることを表しています。
(xに意味はなく、一つ目の引数であることを示しているだけです)
Id(主キー)は特別にIdメソッドがありますが、そのほかはMap関数でマッピングしていきます。Employee内のStoreは1対多として、Referenceメソッドでマッピングします。
(NHibernateではMap=Property, Reference=many-to-oneとなります)
次はStoreクラスのマッピングです。
public class StoreMap : ClassMap<Store> { public StoreMap() { Id(x => x.Id); Map(x => x.Name); HasMany(x => x.Staff) .Inverse() .Cascade.All(); HasManyToMany(x => x.Products) .Cascade.All() .Table("StoreProduct"); } }
(Mapping\StoreMap.cs)
StoreクラスのマッピングもEmployeeクラスと同様にClassMapを継承しStore+Mapという名前で作成します。
Storeクラスは
- Id(主キー)
- Name(プロパティ)
- Staff(1対多)
- Products(多対多)
をメンバとして持っておりIdとNameはEmployeeMapと同様なので省略します。
Staffは1対多マッピングをHasManyで定義します。ここで関係性の主語はStoreMapクラスなのでStaffではなくStoreです。ですから(Store) has many (employee.)です。
Inverce属性は保存時の責任を放棄することを示します。つまり、Insert, Update時のデータストアはStoreテーブルではなくEmployeeテーブル側でやってねということです。これにはルールがあり1対多の場合、多の方にInverce属性を付けることになります。主語はStoreなので多側のStaffにInverce(反対)と言っているわけです。
書くと難しいのですが、ER図を見ればStoreテーブルにはEmployeeに関する情報はなく、Employee側にStoreIDを持っていることから、Inverce設定になっているとみれば解り易いのではないでしょうか。
Cascade.All()は、Storeクラスに変更があった時にEmployee、Productsクラスにも変更を伝播させるということです。Storeクラスを保存すると、Productsコレクション内のProductクラスも同時に変更されるということです。
Allはdelete, save-updateを含めたすべての操作時に伝播させるということです。
Productsには、多対多の解決テーブル名をTable()で指定しています。これはFluent NHibernateが解決テーブル名を推測できないことが原因です。
最後に、Productクラスのマッピングです。
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() .Table("StoreProduct"); } }
(Mappings\ProductMap.cs)
Productマッピングはこれまでの応用なので、特に難しくはないはずです。
多対多のStoresStockedInにInverce()設定がありますが、StoreMapの時と違い、多対多の場合はどちらか(ここではProductMapかStoreMap)で設定すれば良いので、深くは考えません。(両方に設定しないように)
さて、お疲れ様でした。ここまでで下記のような構成になっているでしょうか。
では、次に3.アプリケーションの実装に移りましょう。
補足
参照設定にPostgresql用のデータプロバイダNpgsql.dllを参照に追加しています。
今回は、インストール先のms.net.4.0のアセンブリを選択しました。