NHibernate a FluentNHibernate

Varování: ORM neboli objektově relační mapování pro vás může být Vietnam of computer science. Pečlivě zvažte, zda pokračovat ve čtení.

Jak spojit hluboký objektový svět C# s něčím tak plochým jako je relační databáze? První rada by teoreticky mohla znít: nijak. Místo relační použijme objektovou nebo dokumentovou databázi. V praxi se ale ještě relačním databázím nevyhneme. Automatizované mapování na naše objekty nám alespoň může práci usnadnit. V tomto postu se pokusím popsat základy NHibernate ORM a využití FluentNHibernate pro jeho pohodlnější konfiguraci.

Objektově relační mapování

Když se rozhodujeme, jak budeme modelovat tak zvanou problémovou doménu naší aplikace, máme na výběr mimo jiné dvě základní možnosti:

  • transaction script, který vede krajinou spíše procedurálního návrhu, kde případných objektových featur využijeme akorát při použití knihoven a frameworků,
  • nebo domain model. Tedy skutečně objektový model našeho problému. Znamená to řešit hříčky, které známe z knih o OOP, jako třeba, jestli kolo může existovat bez auta, jestli je mezi nimi vztah asociace nebo snad dědičnosti a tak dále.

Při volbě transaction scriptu zbude z našeho návrhu jen spoustu izolovaných metod typu Button1_Click apod. I když tento způsob je jistě legitimní a na některé jednoduché problémy se hodí, zkušenější programátoři využijí spíše síly objektového modelu.

Ale samozřejmě to má (ne)jeden háček: pěkný objektový model = graf objektů a pokud se do této rovnice připlete ještě persistence, tedy ukládání dat třeba zrovna do relační databáze, řešení není vůbec jednoduché. Podívejme se na takový zjednodušený model:
Model tříd Book, Writer a Publication
Kniha má vztah N:M k autorům (jeden autor mohl napsat více knih, kniha může být od více autorů) a 1:N ke svým publikacím (kniha může mít více publikací). Co by se nám tak líbilo, je mít k dispozici nástroj, který by naše objekty ukládal do databáze a pak je zase na vyžádání vybíral. Mohlo by to vypadat například takhle:

var brown = new Writer("Dan Brown");
var book = new Book(brown);
book.AddPublication(new Publication("Sifra mistra ...", "cs", "80-86518-62-0"));
Persister.Save(book);
// ....
var books = Persister.GetAll<Book>();
foreach(var book in books) {
  Console.WriteLine(book.Writer.Name);
  foreach(var pub in book.Publications) {
    Console.WriteLine(pub.Name);
  }
}

NHibernate

Knihovnou, která nám něco takového poskytne je právě NHibernate. NHibernate je open source dostupný i pro různé prehistorické verze .NET jako je 2.0, na druhou stanu v novějších verzích můžeme použít i NHibernate.Linq provider. Jak to tedy celé funguje, aneb první NHibernate projekt:

Nejprve je třeba si stáhnout a nareferencovat potřebné assemblies. Nejlépe společně s FluentNHibernate, což je extenze pro NHibernate, která nám zjednoduší konfiguraci. Všechno tohle lze stáhnout na hornget.net. (Potřebné dll jsou i součástí ukázkového projektu).

A teď jdeme programovat. Zjednodušený kód zjednodušeného modelu tedy může vypadat například takto:

    public class Book
    {
        public Book()
        {
            this.Publications = new List<Publication>();
            this.Writers = new List<Writer>();
        }

        public virtual Guid Id { get; private set; }
        public virtual string Name { get; set; }
        public virtual IList<Publication> Publications { get; set; }
        public virtual IList<Writer> Writers { get; set; }
    }

    public class Publication
    {
        public virtual Guid Id { get; private set; }
        public virtual string Name { get; set; }
        public virtual string ISBN { get; set; }
    }

    public class Writer
    {
        public virtual Guid Id { get; private set; }
        public virtual string Name { get; set; }
    }

Všimněte si klíčového slova virtual u všech členů našich persistentních tříd. NHibernate virtualitu vyžaduje aby mohl provádět lazy-loading. (Zbavit se virtualu lze pomocí PostSharpu). O lazy-loadingu a dalších featurách správného ORM se podrobněji rozepíšu možná příště. Teď pokračujeme dál. Potřebujeme NHibernate nějak sdělit, co a jak má do databáze ukládat. Klasicky se to dělá pomocí xml konfigurace, ale my použijeme FluentNHibernate a všechno pěkně nakonfigurujeme v kódu:

    public class BookMap : ClassMap<Book>
    {
        public BookMap()
        {
            this.Id(x => x.Id);
            this.Map(x => x.Name);
            this.HasMany(x => x.Publications).Cascade.SaveUpdate();
            this.HasManyToMany(x => x.Writers).Cascade.SaveUpdate();
        }
    }

    public class WriterMap : ClassMap<Writer>
    {
        public WriterMap()
        {
            this.Id(x => x.Id);
            this.Map(x => x.Name)
                .Not.Nullable();
        }
    }

    public class PublicationMap : ClassMap<Publication>
    {
        public PublicationMap()
        {
            this.Id(x => x.Id);
            this.Map(x => x.ISBN)
                .Length(32);
            this.Map(x => x.Name);
        }
    }

Pro každou třídu modelu jsme definovali třídu se stejným názvem a koncovkou Map. Tato „mapovací“ třída dědí od ClassMap, což je třída z FluentNHibernate, která poskytuje potřebné „konfigurační“ metody. V konstruktoru každé „mapovací třídy“ pak nastavíme jaké properties „modelové třídy“ se mají mapovat, případně i další detaily jako například, že ISBN má délku maximálně 32, nebo, že jméno spisovatele nesmí být null. Díky fluent rozhraní je vše celkem intuitivní. Člověk napíše this [tečka] a pak už jen vybírá v intellisense.

Máme nastavené mapování na databázi, teď by to chtělo se k ní připojit. Jedna možnost je opět konfigurace v xml souboru, tentokrát lze úspěšně použít i Web.config či App.config, ale i zde můžeme použít FluentNHibernate:

            var dbConfig = MsSqlConfiguration.MsSql2005
                    .ConnectionString(c =>
                    {
                        c.Database("tests");
                        c.Server(".\\SQLEXPRESS");
                        c.TrustedConnection();
                    })
                    .ShowSql()
                    .ProxyFactoryFactory("NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu");

            var configuration =
                Fluently.Configure()
                    .Database(dbConfig)
                    .Mappings(x => x.FluentMappings.AddFromAssemblyOf<PublicationMap>());

            sessionSource = new SessionSource(configuration);
            sessionSource.BuildSchema();

Kromě klasického „connection stringu“ jsme ještě nastavili zobrazování sql na konzoli, takže uvidíme, co NHibernate na pozadí provádí. Příkaz BuildSchema vytvoří námi definované schéma tabulek, pokud už schéma existuje, vymaže všechna data. (Proxy factory souvisí s lazy-loadingem – na ten už tu nezbývá prostor)

Session source, které jsme při konfiguraci vytvořili je něco jako hlavní manažer přístupu do databáze. Pokud se chceme připojit k databázi, řekneme si session source o novou session. Session nám může vrátit existující objekty z databáze a nové nebo změněné objekty můžeme do session ukládat pomocí metody Save(object). Všechny příkazy INSERT a UPDATE se provedou najednou až potom, co na session zavoláme metodu Flush. Jinou možností, jak postupovat při ukládání objektů, je místo volání Flush na session použít transakce. V tomto případě se data zapíší ihned po zavolání metody Commit na objektu transakce. Transakce v pojetí NHibernate samozřejmě znamenají i to, že jednotlivé příkazy zadané NHibernatí transakci se pak provedou v rámci jedné databázové transakce. Právě tento přístup je použit v následující ukázce.

            using (var session = sessionSource.CreateSession())
            {
                using (var tx = session.BeginTransaction())
                {
                    var brown = new Writer() { Name = "Brown" };
                    var book = new Book() { Name = "Sifra" };
                    book.Publications.Add(
                        new Publication()
                        {
                            ISBN = "80-86518-62-0",
                            Name = "Sifra mistra Leonarda"
                        });
                    book.Writers.Add(brown);

                    var book2 = new Book() { Name = "Symbol" };
                    book2.Writers.Add(brown);

                    session.Save(book);
                    session.Save(book2);

                    tx.Commit();
                }
            }

A když už máme mapování, databázi a v ní data, chtělo by to nějaký ten LINQ.

            using (var session = sessionSource.CreateSession())
            {
                Console.WriteLine("================");
                Console.WriteLine("Vypis knih:");
                foreach (var book in session.Linq<Book>())
                {
                    Console.WriteLine("{0} by {1}", book.Name, book.Writers.First().Name);
                }

                Console.WriteLine("================");
                Console.WriteLine("Filtrovany vypis knih:");
                var query = from b in session.Linq<Book>()
                    where b.Publications.Count > 0
                    select b;
                foreach (var book in query)
                {
                    Console.WriteLine(book.Name);
                }
            }

            Console.ReadLine();

A to by bylo pro dnešek vše. Příště bychom se mohli podívat na techničtější detaily jako je lazy loading a nebo na to, jak si usnadnit práci pomocí NHibernateFluent auto mappingu a nechat tak zvítězit konvence nad konfiguracemi, kterých se lze téměř zbavit.

Uvedené zdrojáky včetně solution souboru a referencovaných dll pěkně pohromadě můžete stáhnout zde.

Post a Comment

Your email is never published nor shared. Required fields are marked *