5. Fluent Mapping(2)

リレーションシップ

HasManyToMany / many-to-many

多対多のマッピングはHasManyToMany関数を使用します。

多対多は、2つのエンテェティ間双方で1つのプロパティから相手の1つが特定できない関係性を言います。

 

はい、わからないですね。

 

例えば、映画スキーマと監督スキーマの関係性では、ある映画を特定するとその監督が自動的に特定され、逆に監督を特定しても映画が特定されないので多対1ですが、映画スキーマと俳優スキーマでは、映画を特定しても出演俳優が一人特定にできず、逆に俳優を特定しても出演映画が特定されないため、多対多の関係性になります。

この場合、一般的にデータベースの世界では中間テーブルを挟んで、n:mをn:1と1:mに分割して表現します。

これをデータベーススキーマで表現すると、以下のようになります。

中間テーブルを挟むことで、多対多の関係性が2つの1対多の関係性に分解されました。

 

これをFluent NHibernateを使ってマッピングするの場合、マッピングされるmovie、actorクラスはそれぞれが相手のリストを持つように表現されます。この際、中間テーブルのことは考える必要はありません。

ここで、MovieクラスにのみActorを追加するAddActorメソッドがあることに気付いた方は鋭いです。後程説明しますが、マッピングの際にCascade属性をつけ、一方のクラスが変更された際の影響を伝播させる設定にするために、ActorクラスにAddMovieメソッドを用意する必要がないのです。もちろんあってもいいのですが、不用意に追加されないようにここでは実装はしません。

using System.Collections.Generic;


namespace FluentNHibernateSample.Entities
{
    public class Movie
    {
        public virtual int id { set; get; }
        public virtual string title { set; get; }
        public virtual IList actors { set; get; }

        public Actor Actor
        {
            get
            {
                throw new System.NotImplementedException();
            }
            set
            {
            }
        }
 
        public Movie()
        {
            actors = new List();
        }
 
        public virtual void AddActor(Actor actor)
        {
            actor.movies.Add(this);
            actors.Add(actor);
        }
    }
}

(Entities\Movie.cs)

using System.Collections.Generic;

namespace FluentNHibernateSample.Entities
{
    public class Actor
    {
        public virtual int id { set; get; }
        public virtual string name { set; get; }
        public virtual IList movies { private set; get; }

        public Movie Movie
        {
            get
            {
                throw new System.NotImplementedException();
            }
            set
            {
            }
        }
 
        public Actor()
        {
            movies = new List();
        }
        
    }
}

(Entities\Actor.cs)

using FluentNHibernateSample.Entities;
using FluentNHibernate.Mapping;

namespace FluentNHibernateSample.Mappings
{
    public class MovieMap : ClassMap<Movie>
    {
        public MovieMap()
        {
            Id(x => x.id);
            Map(x => x.title);
            HasManyToMany(x => x.actors)
                .Inverse()
                .Cascade.All()
                .Table("movieactor");
        }
    }
}

(Mappings\MovieMap.cs)

using System.Collections.Generic;
using FluentNHibernateSample.Entities;
using FluentNHibernate.Mapping;

namespace FluentNHibernateSample.Mappings
{
    public class ActorMap : ClassMap
    {
        public ActorMap()
        {
            Id(x => x.id);
            Map(x => x.name);
            HasManyToMany(x => x.movies)
                .Cascade.All()
                .Table("movieactor");
        }
    }
}

(Mappings\ActorMap.cs)

以上のクラスを実装し、次にこれらを利用するサンプルを実装します。

いま、

  • Michael J. Fox
  • クリストファー・ロイド

という、俳優2人と

  • Back to the future
  • Stuart Little

という、映画2タイトルを想定します。

「Michael J. Fox」は、両方の映画に出演しています。

クリストファー・ロイド」は「Back to the future」にのみ出演しています。

 

これを、サンプルとして実装すると以下のようになります。

var fox = new Actor() { name = "Michael J. Fox" };
var lloyd= new Actor() { name = "クリストファー・ロイド" };

var BTF = new Movie() { title = "Back to the future" };
var SLittle = new Movie() { title = "Stuart Little" };

BTF.AddActor(fox);
BTF.AddActor(lloyd);
SLittle.AddActor(fox);

全体サンプルは後ろに乗せますが、サンプルを実行するとデータベース上には3つのテーブルが作成されていることが解ります。(Actor, Movie, movieactor)

movieactorテーブルが多対多の解決用の中間テーブルです。

データベース内のデータは、Actor,Movie,movieactorの順に以下のようになります。

サンプルアプリケーションソース

以下は、PostgreSQL用の設定です。

実行前に「testdb」を作成しておく必要があります。

using System.Collections.Generic;
using FluentNHibernateSample.Entities;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;

namespace FluentNHibernateSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var sessionFactory = CreateSessionFactory();

            using (var session = sessionFactory.OpenSession())
            {
                using (var transaction = session.BeginTransaction())
                {
                    var actor1 = new Actor() { name = "Michael J. Fox" };
                    var actor2 = new Actor() { name = " クリストファー・ロイド" };

                    var movie1 = new Movie() { title = "Back to the future" };
                    var movie2 = new Movie() { title = "Stuart Little" };

                    movie1.AddActor(actor1);
                    movie1.AddActor(actor2);
                    movie2.AddActor(actor1);

                    // save both stores, this saves everything else via cascading
                    session.SaveOrUpdate(movie1);
                    session.SaveOrUpdate(movie2);

                    transaction.Commit();
                }
            }
        }

        private static ISessionFactory CreateSessionFactory()
        {
          return Fluently.Configure()
           .Database(PostgreSQLConfiguration.PostgreSQL82
           .ConnectionString(x => x
               .Host("localhost")
               .Port(5432)
               .Database("testdb")
               .Username("postgres")
               .Password("postgres")))
           .Mappings(m => m
               .FluentMappings.AddFromAssemblyOf())
           .ExposeConfiguration(BuildSchema)
           .BuildSessionFactory();
        }
    

        private static void BuildSchema(Configuration config)
        {
            // this NHibernate tool takes a configuration (with mapping info in)
            // and exports a database schema from it
            new SchemaExport(config)
                .Create(false, true);
        }
    }
}

(Program.cs)