Showing posts with label mapping. Show all posts
Showing posts with label mapping. Show all posts

May 30, 2013

Migrating from Castle ActiveRecord

Unfortunately Castle ActiveRecord project is abandoned and not developed anymore. Latest release uses NHibernate 3.0 and is not compatible with last NHibernate due  to the changes in API of dynamic proxy.

So we have decided to remove dead assembly and move on. In order to do that there some major steps that needed to be done:

  1. Rewrite all dependent code that used ActiveRecordMediator class
  2. Implement ISession, ISessionFactory lifetime management
  3. Reconfigure NHibernate without using AR wrappers over configuration
  4. Export existing AR mappings based on attributes to XML
  5. Migrate XML mappings to mapping by code

First two steps are tightly related. Methods in ActiveRecordMediator class are easily translated into appropriate analogs in ISession object. We already had a Repositories layer that was used to encapsulate all NHibernate related code, so it was not a really big problem to inject session to them.

For lifetime management we implemented same solution that was described in my blog earlier – let the IoC container resolve it.

Now about third step. NHibernate has not a lot of configuration in fact. All the new API descriptions can be found here. AR doesn’t add a lot, so nothing fancy here.

Export ActiveRecord mappings to XML

With exporting mappings things are starting to get interesting. I couldn’t find an out of the box way in AR to do that. So in fact I had to copy paste methods and to make something from AR public to support this. So here is a mapper class that allows exporting (btw AR sources that we have used before removing it). Line 21 is a place where you have map as a string and do anything you want with it.

Migration from hbm.xml to code

Sure you can stop migration here and just keep XML mappings. But that is just not good enough. Now there is a task of converting XML mappings to code. What is the best tool to covert XML to anything else?.. Well its XSLT. Sorry for that, but it is as it is. So I’ve created a transformation that is capable to do what is needed. Source is too long to put here, so I’m putting a link to appropriate gist. There are some preparations need for mappings – remove all nhibernate namespaces and wrappers like

<hibernate-mapping  auto-import="true" default-lazy="false" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:nhibernate-mapping-2.2">
    <!-- mapping here should be kept --> 
</hibernate-mapping>

It uses XSLT 2.0 that is not supported by default .net tools. To run it you can use saxonica tool. In order to run it you can use following command line:

c:\Program Files\Saxonica\SaxonPE9.4N\bin\Transform.exe -s:mappings.xml -xsl:nbmToCs.xslt -o:mappings.cs

So after doing that you will have new nice mappings. Not everything that is supported by NHibernate is supported by XSLT, I’ve added only things that we needed. If you need to add something, feel free to contact me.

Mar 28, 2012

ASP.NET MVC extension points in action

Recently I made a tech talk about my alt.net web stack of love. It has lots of things there. Validation, NHibernate, Routing etc. So here are slides:

Source code for it is on bitbucket. Its purpose is just see it all in action. If you want, you should move code your new/old projects. Don’t try to make some type of project template out of it.

To try things out

Once again link to sources.

I was asked to give some kind of practice task to try all stack together. So here it is:

Implement blog details page to show posts inside it. So when I open localhost/blogs/3 I should see something like:

untitled_page

When I navigate to post details page I should be able to see post content and leave comments. Just like on this blog Smile.

Jun 9, 2011

Getting started with NHibernate and ConfOrm (revamp)

I’m trying to post different things about NHibernate, but still post NHibernate for beginners is most popular. I wrote them in 2009 (Wow, how old I am! :D). First of all its in Russian and I think things have changed a bit since that time, mainly because of awesomeness of NuGet. This getting started will contain not only recommended mapping technique, but also references to some other useful things like GetHashCode, Equals implementations, transactions management, etc.

So lets get started. First of all install NuGet. Create a new empty MVC 3 project and add two class library projects name them %WhatYouLike%.Core and %WhatYouLike%.Data. First assembly will contain all business entities that NHibernate will save and load from data base. Data assembly is going to be used for NHibernate configuration, repositories, etc.

So when projects are created lets install NHibernate. In visual studio navigate to Tools –> Library Package Manager –> Package Manager Console. In the opened command prompt select your MVC project as default project and execute command: “Install-Package ConfOrm”. Do the same for Data project.

ConfOrm is an open source project that allows you to map all your domain model without any XML. I do use ConfOrm instead of Fluent NHibernate in this example just because first one is going to be added in core of NHibernate, so it is preferable (but of course not necessary) to get familiar with it.

Just can’t allow myself to forget about some political issues with ConfOrm and Fluent NHibernate :). ConfOrm project was started by Fabio Maulo who is lead of NHibernate project (correct me here if I’m wrong). And 3.2 version of NHibernate was released with mapping in code part that basically was very similar to ConfOrm way of doing things.

This fact has raised a little storm in twitter and James Gregory (author of Fluent NHibernate) wrote a blog post to explain what he thinks about it.

Some time later Fabio wrote a response post where he has shown how you can reuse your Fluent mappings with new API :).

So, don’t where it will end, but politics in open source space also takes place :). I recommend you to read mentioned two posts, just to be informed.

Now lets get back to our project. First thing we are going to do is add some entities to work with. NHibernate uses POCO objects. It means that you don’t need to inherit from base classes or implement some interfaces in order to create an entity that is going to be managed by NHibernate. Two things required:

  1. Entity should have public/protected default constructor (constructor with no parameters)
  2. All properties and methods should be virtual

So lets create some entities with associations and properties. Here is example domain model I’ve created:

domain model

Here is source code for one of the entities:

using System.Collections.Generic;

namespace nhrevamp.Core
{
    public class Post : Entity
    {
        public Post()
        {
            this.Comments = new HashSet<Comment>();
            this.Tags = new HashSet<Tag>();
        }

        public virtual string Title { get; set; }

        public virtual string Content { get; set; }

        public virtual ICollection<Comment> Comments { get; protected set; }

        public virtual ICollection<Tag> Tags { get; protected set; }
    }
}

All entities inherit from base class Entity with single property Id:

public abstract class Entity
{
    public virtual int Id { get; protected set; }
}

Notice that we are using ICollection from System.Collections.Generic not from Iesi.Collections. The reason for that is to keep our domain model as clean as possible. Also we initialize collections in constructor in order to avoid nasty NullReferenceException’s. And also we mark setters for collections as protected, just to avoid possibility to replace all collection with new one accidently.

So we have:

  1. One to many association (Post has many Comments)
  2. Many to one association (each Comment belongs to one Post)
  3. Many to many association (Post has many Tags and Tag has many Posts)

NHibernate has two main interfaces that you are going to work with. First is ISessionFactory. Instance of it should be used as a singleton and configured only once. This operation will take time, especially for complex domain models. The good news is that ISessionFactory is serializable and are able to cache it in order to not recreate it each time. Second interface is ISession. This one is used to query entities, update and delete them. Creation of ISession object is small, but you should consider some lifetime management issues with ISession. Here I’ve described how you can easily integrate it with MVC. 

In order to create ISessionFactory we need to configure it and provide mappings for our domain model. Lets start with configuration (you can read full list of available options here). Add new class to your .Data project, lets call it NHibernateConfigurator and create a single method there -BuildSessionFactory. Code there should be the following:

public class NHibernateConfigurator
{
    public ISessionFactory BuildSessionFactory()
    {
        var cfg = new Configuration();
        cfg.SessionFactory()
           .Proxy.Through<ProxyFactoryFactory>()
           .Integrate.Using<MsSql2008Dialect>()
           .Connected.ByAppConfing("connectionString");

        return cfg.BuildSessionFactory();
    }
}

So we say that we are going to work with SQL Server 2008 and connection string is placed in connection strings section of application settings file (web.config in our case).

This post is written for NHibernate 3.1 version. And this version has no default proxy factory with it. So you need to execute one more NuGet command for Data project: Install-Package NHibernate.LinFu. 3.2 version will be delivered with default proxy factory.

Now to the mappings. Its so easy, that you even wouldn’t believe:

public class DomainMapper
{
    public HbmMapping GenerateMappings()
    {
        IEnumerable<Type> domainEntities = GetDomainEntities();

        ObjectRelationalMapper relationalMapper = new ObjectRelationalMapper(); 
        relationalMapper.TablePerConcreteClass(domainEntities); // each concrete class should have its own table in DB
        relationalMapper.Patterns.PoidStrategies.Add(new NativePoidPattern()); // primary keys are generated by DB with identity field
        relationalMapper.Patterns.Sets.Add(new UseSetWhenGenericCollectionPattern()); // ICollection when met in classes should use Set in mappings
        relationalMapper.ManyToMany<Post, Tag>(); // Many to many association by some reasons cant be picked by ConfOrm. Need in set it manually
        relationalMapper.Cascade<Post, Tag>(Cascade.Persist); // when post is saved, tag also needs to be saved

        var patternsAppliers = new CoolPatternsAppliersHolder(relationalMapper); // this is set of column naming packs it used to get nice column names in FKs like PostId in Comments table
        patternsAppliers.Merge(new ClassPluralizedTableApplier(new EnglishInflector())); // means that Comment entity will have Comments table in DB
        Mapper mapper = new Mapper(relationalMapper, patternsAppliers);

        HbmMapping mapping = mapper.CompileMappingFor(domainEntities);
        File.WriteAllText(@"D:\mapping.xml", Serialize(mapping));

        return mapping;
    }

    private static IEnumerable<Type> GetDomainEntities()
    {
        Assembly domainAssembly = typeof(Entity).Assembly;
        IEnumerable<Type> domainEntities = from t in domainAssembly.GetTypes()
                                           where t.BaseType == typeof(Entity) && !t.IsGenericType
                                           select t;
        return domainEntities;
    }

    /// <summary>
    /// Generates XML string from <see cref="NHibernate"/> mappings. Used just to verify what was generated by ConfOrm to make sure everything is correct.
    /// </summary>
    protected static string Serialize(HbmMapping hbmElement)
    {
        var setting = new XmlWriterSettings { Indent = true };
        var serializer = new XmlSerializer(typeof(HbmMapping));
        using (var memStream = new MemoryStream())
        {
            using (var xmlWriter = XmlWriter.Create(memStream, setting))
            {
                serializer.Serialize(xmlWriter, hbmElement);
                memStream.Flush();
                byte[] streamContents = memStream.ToArray();

                string result = Encoding.UTF8.GetString(streamContents);
                return result;
            }
        }
    }
}

This code block contains one utility method called Serialize. It is used just to verify mappings. You can skip it. But its useful. That’s all. Some details I’ve added in comments. So now we are ready to start working with NHibernate.

Now we need provide generated mappings for our ISessionFactory. So add next two lines to the BuildSessionFactory method of NHibernateConfiguratorClass:

HbmMapping generateMappings = new DomainMapper().GenerateMappings();
cfg.AddDeserializedMapping(generateMappings, "domain");

Now we are ready to use NHibernate. First thing we need is actually DB schema. We can generate it from mappings with the help of SchemaExport class:

new SchemaExport(cfg).Execute(true, true, false);

This command is executed in NHibernateConfigurator class and creates all required tables for us:

Data base schema

In order to work properly NHibernate requires transactions to wrap all queries. I’ve described asp.net mvc integration in previous post. So I won’t describe it here.

The last thing you need to see here is a way to work with data. Here is how we can save new blog with post and some comments:

ISessionFactory sessionFactory = new NHibernateConfigurator().BuildSessionFactory();
using (ISession session = sessionFactory.OpenSession())
{
    using (ITransaction transaction = session.BeginTransaction())
    {
        Post post = new Post
                        {
                            Content = "test"
                        };
        post.Tags.Add(new Tag
                          {
                              Name = "test"
                          });
        post.Comments.Add(new Comment
                              {
                                  Author = "me",
                                  Content = "NH is awesome",
                                  Post = post
                              });
        session.SaveOrUpdate(post);
        transaction.Commit();
    }
}

Notice that we save only Post object, all associations are going to be saved by cascade. Generated SQL is straight forward:

begin transaction with isolation level: Unspecified

INSERT INTO Posts
           (Title, Content)
VALUES     (NULL /* @p0 */,
            'test' /* @p1 */)
select SCOPE_IDENTITY()

INSERT INTO Comments
           (Author, Content, PostId)
VALUES     ('me' /* @p0 */,
            'NH is awesome' /* @p1 */,
            1 /* @p2 */)
select SCOPE_IDENTITY()

INSERT INTO Tags
           (Name)
VALUES     ('test' /* @p0 */)

select SCOPE_IDENTITY()

INSERT INTO PostToTag
           (PostId,
            TagId)
VALUES     (1 /* @p0 */,
            1 /* @p1 */)
commit transaction

NHibernate has mature Querying API that was improved in 3.0 version. Also it has LINQ implemented with session.Query<T> extension method. This post has no intention do describe them.

So you are almost NHibernate Guru already ;). What to read next:

  • Overriding GetHashCode and Equals methods for your domain entities (link)
  • Primary keys generations and consequences of using them (link)
  • Lazy, eager loading (link) and select N+1 problem (link)
  • Transactions and Sessions lifetime management (link)
  • Inverse attribute for mapping collections (link)

Happy NHibernating!

Apr 9, 2011

Lightweight NHibernate and ASP.NET MVC integration with Autofac

Many times when new project stars and we want to use NHibernate relatively a lot of work need to be done. Among them are:

  • Mapping of entities (I prefer automapping)
  • ISessionFactory singleton
  • ISession lifetime management (Per web request)
  • Transaction management

Sharp architecture project has all of them done and ready to use. But as for me this project has become too big and hard to understand. I wanted to have full control on what is happening in my app and didn’t want to have such a lot of abstractions. For example Repository and Entity objects that have inheritance chain about to 5 or 6 objects.

So I decided to show how very simple integration can be made with a minimum amount of code. To get all mentioned libraries I will use NuGet. We will need:

  • NHibernate
  • Fluent NHibernate
  • Autofac
  • Autofac.Mvc3

Lets start with mapping / session factory configuration:

public Configuration Configure()
{
    var configuration = new Configuration();
    configuration.SessionFactory()
                 .Proxy.Through<ProxyFactoryFactory>()
                 .Integrate.Using<MsSql2005Dialect>()
                 .Connected.ByAppConfing("dbConnection");


    FluentConfiguration fluentConfiguration = Fluently.Configure(configuration);
    fluentConfiguration.Mappings(map => map.AutoMappings.Add(
                                            new ModelGenerator().Generate()));

    return fluentConfiguration.BuildConfiguration();
}

public ISessionFactory GetSessionFactory()
{
    var configuration = Configure(); 
    return configuration.BuildSessionFactory();
}

Few things to note here. Combination of Loquacious and Fluent configuration is used because first one is supporting all NHibernate features, second one handles mappings integration. Also Model generator class is used:

private class ModelGenerator
{
    public AutoPersistenceModel Generate()
    {
        AutoPersistenceModel automap = new AutoPersistenceModel();

        automap.Conventions.AddFromAssemblyOf<ModelGenerator>();
        automap.UseOverridesFromAssemblyOf<ModelGenerator>();
        automap.AddEntityAssembly(Assembly.GetAssembly(typeof (Entity)))
            .Where(objectType => objectType.IsSubclassOf(typeof(Entity)));

        return automap;
    }
}

Here we setup location for the conventions, overriding's and entities. All classes that are inherited from Entity will be mapped. For conventions I’m using sharp architecture’s with small tweaks to have nice constraints names when generating schema from mappings:

public class ReferenceConvention : IReferenceConvention
{
    public void Apply(FluentNHibernate.Conventions.Instances.IManyToOneInstance instance)
    {
        string fkName = string.Format("{0}_{1}_FK", 
                                      instance.Name, instance.EntityType.Name);
        instance.ForeignKey(fkName);

        instance.Column(instance.Property.Name + "Fk");
    }
}

public class HasManyToManyConvention : IHasManyToManyConvention
{
    public void Apply(IManyToManyCollectionInstance instance)
    {
        string fkName = string.Format("{0}_{1}_FK", 
                                      instance.Member.Name, instance.EntityType.Name);
        instance.Key.ForeignKey(fkName);

        instance.Cascade.SaveUpdate();
    }
}

Now to the web part. In global asax on Application_Start event we need to setup Autofac and change the default controllers factory. To do this:

var builder = new ContainerBuilder();
builder.RegisterControllers(Assembly.GetAssembly(typeof (AuthorizationController)));
builder.Register(x => new NHibernateConfigurator().GetSessionFactory())
    .SingleInstance();
builder.Register(x => x.Resolve<ISessionFactory>().OpenSession())
    .InstancePerHttpRequest();

builder.RegisterModule(new AutofacWebTypesModule());
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

Code here is pretty clear. We setup ISessionFactory to be singleton, ISession instance is resolved by container and has PerHttpRequest lifestyle. Notice call of builder.RegisterModule that is going to add all the required http modules to support per web request lifestyle and change default controller factory to the one that uses Autofac. So now we are able to write code like this:

public class AuthorizationController : Controller
{
      private ISession session;

      public AuthorizationController(ISession session)
      {
          this.session = session;
      }

      public ActionResult Index()
      {
          var users = this.session.QueryOver<User>().List();
          return View(users);
      }
}

So we have controller that depends on ISession, which depends on ISessionFactory, which depends on our Nhibernate configurator class. Isn’t it kind from Autofac to handle all this? Smile

One last but important thing we need to do. Each call to the data base should be wrapped to correct transaction. You can read here why. The easiest way to handle this is to create action filter:

public class TransactionAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        DependencyResolver.Current.GetService<ISession>().BeginTransaction(IsolationLevel.ReadCommitted);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ITransaction currentTransaction = DependencyResolver.Current.GetService<ISession>().Transaction;

        if (currentTransaction.IsActive)
        {
            if (filterContext.Exception != null && filterContext.ExceptionHandled)
            {
                currentTransaction.Rollback();
            }
        }
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        ITransaction currentTransaction = DependencyResolver.Current.GetService<ISession>().Transaction;

        base.OnResultExecuted(filterContext);
        try
        {
            if (currentTransaction.IsActive)
            {
                if (filterContext.Exception != null && !filterContext.ExceptionHandled)
                {
                    currentTransaction.Rollback();
                }
                else
                {
                    currentTransaction.Commit();
                }
            }
        }
        finally
        {
            currentTransaction.Dispose();
        }
    }
}

Original implementation is taken from Sharp architecture and small changes made due to Autofac. So now out Index method should be marked with transaction attribute:

[Transaction]
public ActionResult Index()
{
    return View();
}

That's probably all we need to have NHibernate. As always source code is attached:

Nov 2, 2010

ConfOrm nice column naming conventions

I have decided to write all further posts in English. The reason for that is simple – when I try to find something related to programming topics I always use English. I think most people do the same.

The next step of learning ConfOrm for me was creation of sharp-architecture like conventions for table and column mappings.  I even wrote some pattern appliers, but when ManyToMany turn has came I had to google a bit. And the result was that Conform.Shop already implemented everything that I wanted so this post will be about usage of this library and the next one about implementation of your own appliers.

To create your own naming conventions we need to understand overall process. Basically mapping in ConfOrm  consists of two major steps:

  1. Getting the domain entities. This is done with the help of ObjectRelationalMapper class. This class is responsible for creating all the entities and associations between them. So is you want to change association type, or remove some property from the mappings this a place where you should do it.
  2. Creating mappings. Mapper class is responsible here. If you want to provide tables, columns and other conventions this is the right place.

Lets begin with domain model:

domain model 

Simple but with some associations.

Probably the easiest way to understand how NHibernate is configured is to look at XML (ConfOrm doesn't generate XML, but you can get it with this approach). So lets check User entity map:

<class name="User">
  <id name="Id" type="Int32">
    <generator class="native" />
  </id>
  <property name="FirstName" />
  <property name="LastName" />
  <property name="VeryLongProperty" />
  <set name="Orders" inverse="true" cascade="all,delete-orphan">
    <key column="User" on-delete="cascade" />
    <one-to-many class="Order" />
  </set>
</class>

The first thing that I want to change is the table name. I want it to be plural. To do this all that is required to do the following:

var englishInflector = new EnglishInflector();
mapper.PatternsAppliers.Merge(
                       new ClassPluralizedTableApplier(englishInflector));

There are also SpanishInflector and ItalianInflector if somebody wants them. After adding this one we get directive table=Users and so on for all other entities.

Next thing that I don’t like is names for column in the <key property. To change it I need to do the following:

mapper.PatternsAppliers.Merge(
new OneToManyKeyColumnApplier(relationalMapper));

Now the mapping for the set is:

<set name="Orders" inverse="true" cascade="all,delete-orphan">
  <key column="UserId" on-delete="cascade" /> 
  <one-to-many class="Order" /> 
</set>

That is probably all that I wanted to change in User mapping. Now Lets look at the Product map:

<class name="Product" table="Products">
  <id name="Id" type="Int32">
    <generator class="native" />
  </id>
  <property name="Price" />
  <property name="Name" />
  <property name="Description" />
  <set name="Categories" table="CategoryProduct" inverse="true">
    <key column="product_key" />
    <many-to-many class="Category" column="category_key" />
  </set>
</class>

There is definitely a better way of naming columns in the joining table. To change them we need to add next appliers:

mapper.PatternsAppliers.Merge(
                      new ManyToManyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(
                      new ManyToManyKeyIdColumnApplier(relationalMapper));

Those will give:

<set name="Categories" table="CategoryProduct" inverse="true">
  <key column="ProductId" />
  <many-to-many class="Category" column="CategoryId" />
</set>

That is what I really wanted. So all the mappings for the domain:

var relationalMapper = new ObjectRelationalMapper();
relationalMapper.TablePerConcreteClass(domainEntities);
relationalMapper.Patterns.PoidStrategies.Add(new NativePoidPattern());
relationalMapper.Cascade<Category, Product>(Cascade.Persist);
relationalMapper.ManyToMany<Category, Product>();
relationalMapper.Cascade<Order, Product>(Cascade.Persist);

var mapper = new Mapper(relationalMapper);
var englishInflector = new EnglishInflector();
mapper.PatternsAppliers.Merge(new ClassPluralizedTableApplier(englishInflector));
mapper.PatternsAppliers.Merge(new OneToManyKeyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(new ManyToManyColumnApplier(relationalMapper));
mapper.PatternsAppliers.Merge(new ManyToManyKeyIdColumnApplier(relationalMapper));

So not a lot of code and settings. Also this assembly contains a lot of other very nice patterns for example ManyToManyPluralizedTableApplier after applying it table name for joining products and categories becomes  CategoriesToProducts. Just beautiful Smile.

Here is the source code for this post

So use ConfOrm!

Sep 8, 2010

Про ConfORM

ConfORM это еще один способ маппинга NHibernate сущностей используя код. Главное его отличие от FluentNHibernate в том, что ConfORM вообще не генерирует XML, а работает с открытым  в NHibernate 3 API (более подробно об API можно почитать тут).

Ознакомится с этим фреймворком оказалось довольно не просто. Во первых его название практически не поддается поиску, т.е. вбивая confORM в гугле вы врядли найдете что-то связанное с ним. Во вторых у проекта нет wiki, нет документации, найдено было только следующее:

  1. блог Fabio Maulo – менеджер проекта NHibernate, ConfOrm и еще нескольких OSS проектов.
  2. Блог testdrivendevelopment.wordpress.com.
  3. Страница на google code.
  4. И гугло-группа где обсуждается текущее положение вещей.

Вот в общем то и все что мне удалось накопать за 2-3 часа поисков. Если кто-то найдет еще что-то милости прошу добавляйте в коменты.

Теперь касательно его применения. Насколько я понял ConfORM поддерживает только автомаппинг основываясь на Ваших объектах. Для примера рассмотрим следующую модель:

domain_model

Все классы наследуют EntityBase который содержит единственное свойство Id типа int.

Теперь попробуем замапить эту модель. Для этого ConfORM использует класс ObjectRelationalMapper. Использовать его можно следующим образом:

public HbmMapping GenerateMappigs()
{
    IEnumerable<Type> domainEntities = this.GetDomainEntities();
    
    ObjectRelationalMapper relationalMapper = new ObjectRelationalMapper();
    relationalMapper.TablePerConcreteClass(domainEntities); // каждый не абстрактный объкт будет замаплен на свою таблицу.

    Mapper mapper = new Mapper(relationalMapper);
    HbmMapping mapping = mapper.CompileMappingFor(domainEntities); // создание самих маппингов.

    File.WriteAllText(@"d:\mappings.xml", Serialize(mapping)); // сохраняем маппинги в файл.
    return mapping;
}

/// <summary>
/// Gets all objects that are inherited from EntityBase.
/// </summary>
private IEnumerable<Type> GetDomainEntities()
{
    Assembly domainAssembly = typeof (EntityBase).Assembly;
    IEnumerable<Type> domainEntities = from t in domainAssembly.GetTypes()
                                       where t.BaseType == typeof(EntityBase) && !t.IsGenericType
                                       select t;
    return domainEntities;
}

Наверно что-то замапилось :). Чтобы это проверить можно сохранить конфигурацию в XML виде. Для того используется метод Serialize, его реализация такова:

/// <summary>
/// Generates XML string from NHibernate mappings
/// </summary>
protected static string Serialize(HbmMapping hbmElement)
{
    var setting = new XmlWriterSettings { Indent = true };
    var serializer = new XmlSerializer(typeof(HbmMapping));
    using (var memStream = new MemoryStream())
    {
        using (var xmlWriter = XmlWriter.Create(memStream, setting))
        {
            serializer.Serialize(xmlWriter, hbmElement);
            memStream.Flush();
            byte[] streamContents = memStream.ToArray();

            string result = Encoding.UTF8.GetString(streamContents);
            return result;
        }
    }
}

Выполнив этот код, получим следующий xml:

<?xml version="1.0" encoding="utf-8"?>
<hibernate-mapping xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" namespace="Conf.Entities" assembly="Conf" xmlns="urn:nhibernate-mapping-2.2">
  <class name="User">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Name" />
    <property name="BirthDate" />
    <bag name="Blogs" inverse="true" cascade="all,delete-orphan">
      <key column="Owner" on-delete="cascade" />
      <one-to-many class="Blog" />
    </bag>
  </class>
  <class name="Blog">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Name" />
    <many-to-one name="Owner" />
  </class>
  <class name="Comment">
    <id name="Id" type="Int32">
      <generator class="hilo" />
    </id>
    <property name="Text" />
    <many-to-one name="Author" />
    <many-to-one name="Blog" />
  </class>
</hibernate-mapping>

В общем то для существующей базы с такими маппингами уже можно работать. Как видите по умолчанию для первичных ключей используется hilo алгоритм, для коллекций используются Bag тэги, также для связи User-Blog выставляется правило каскадирования all, delete-orphan (толи это breaking change, но кажется раньше эта настройка выглядела как all-delete-orphan) и свойства называются так же как поля в базе.

В следующих постах хочу описать как кастомизировать автоматические маппинги и сделать полностью работающий пример.

Исходники

Feb 19, 2010

Ado net batch size и идентификаторы

Для оптимизации большого количества insert-update команд NHiberante содержит специальную настройку – Ado net batch size. Рассмотрим следующий код:

for (int i = 0; i < 5000; i++)
{
    var testIncrement = new SomeEntity
                            {
                                Field1 = "field" + i, 
                                Field2 = i
                            };

    session.SaveOrUpdate(testIncrement);
} 

Какое количество запросов будет сделано к базе данных? Ответ зависит от двух условий:

  1. Установлен ли параметр ado net batch size
  2. Какой алгоритм используется для генерирования Id сущностей

Ну по поводу первого в общем то ясно – этот параметр отвечает за то, сколько запросов NHibernate объеденит в один и выполнит в один заход.

А вот о Id стоит сказать немного больше. Лично я до последнего времени во всех приложениях использовал автоинкрементные поля в Sql Server. Делалось это как наиболее простой выход для генерирования Id – в маппинге достаточно было указать GenearatedBy.Native() и все. В общем то мне никогда этот поход не мешал и вполне выполнял нужные мне задачи.

Такой же подход применялся в проекте Sharp Arhitecture… До релиза в третьем квартале 2009года :). Там алгоритмом по умолчанию установлен алгоритм hilo. Подробности этого алгоритма можно почитать по соответствующей ссылке. Основная идея заключается в следующем, когда надо получить происходит примерно следующее:

  1. В базе данных существует спец. таблица, которая хранит hi величину, это обычное число. NHiberante получает его.
  2. Nhibernate в зависимости от настроек выбирает следующую hi величину. Предположим текущее hi – 1000, а следующее 2000.
  3. Получив hi=1000 и выставив в базе данных next hi = 2000 Nhibernate в праве создавать сущности в id = 1001, 1002, 1003 и т.д.

Соответственно если каждый клиент будет обновлять hi величину, то никто из клиентов не получит одинаковые id для объектов. Для Oracle хранилищем hi значений возможно могут быть последовательности, которых нет в Sql Server.

Все это длинное описание ради того, чтобы было понятно происходящее в примере в строке 9. Как только NHibernate выполнит команду SaveOrUpdate сущность testIncrement должна получить Id. Если в качестве id используется автоинкрементное поле, то каждое такое сохранение необходимо фиксировать в базе, т.е. выполнять запрос Insert (иначе нельзя получить id). В случае же использования hilo алгоритма NHiberante может сохранить нужное количество объектов, при этом самостоятельно генерируя Id.

В общем выставив ado net batch size = 40 и выполнив код, приведенный в начале получил следующие результаты:

  • autoincrement – 5000 запросов (как и ожидалось),
  • hilo – 225.

Jan 12, 2010

Маппинг деревьев в NHibernate

Доменная модель

В посте о связях в NHibernate поступило пожелание увидеть пост о древовидных структурах и как с ними работать в NHiberante. Как правило такие структуры мне приходилось строить для категорий, на них и будем смотреть. Начнем с чистой доменной модели:

public class Category
{
    private ISet<Category> childCategories;
    private Category parentCategory;

    public Category()
    {
        childCategories = new HashedSet<Category>();
    }

    public virtual int Id { get; protected set; }

    public virtual string Name { get; set; }

    public virtual ReadOnlyCollection<Category> ChildCategories
    {
        get
        {
            return new ReadOnlyCollection<Category>(new List<Category>(childCategories));
        }
    }

    public virtual Category ParentCategory
    {
        get
        {
            return parentCategory;
        }
    }

    public virtual void AddChildCategory(Category category)
    {
        if (category == null) throw new ArgumentNullException("category");
        childCategories.Add(category);
    }

    public virtual void SetParentCategory(Category category)
    {
        if (category == null) throw new ArgumentNullException("category");
        parentCategory = category;
    }
}

Каждая категория содержит ссылку на своего родителя и содержит список дочерних категорий. На данный момент модель не представляет никакой гарантии того, что дерево действительно будет деревом. Что я имею ввиду: если есть следующая вложенность: “Parent” –> “Child” -> “Child of Child”. Доменная модель никаким образом не запретит следующий код: Category(“Child Of Child”).AddChildCategory(“Parent”). Это создаст цикл и никакие рекурсивные алгоритмы без дополнительных действий работать не будут.

Чтобы избежать подобной ситуации в методы SetParentCategory и AddChildCategory можно добавить логику проверки добавляемых категорий. Так добавляя подкатегорию можно проверить что она уже не содержится в данной иерархии и т.д.

Маппинг

Теперь обратимся к Nhibernate. Маппинг для данного класса будет следующим:

public class CategoryMap : ClassMap<Category>
{
    public CategoryMap()
    {
        Id(x => x.Id).GeneratedBy.Native();

        Map(x => x.Name)
            .Length(200)
            .Not.Nullable();

        References(x => x.ParentCategory)
            .Column("ParentCategoryId")
            .Access.CamelCaseField();

        HasMany(x => x.ChildCategories)
            .Cascade.AllDeleteOrphan()
            .AsSet()
            .KeyColumn("ParentCategoryId")
            .Access.CamelCaseField();
    }
}

По маппингу можно сгенерировать следующую таблицу:

Таблица категорий

В общем ничего необычного. Для создания категорий можно использовать следующий код:

using (var transaction = Global.CurrentSession.BeginTransaction())
{
    Category parent = new Category();
    parent.Name = "parent";

    Category child = new Category();
    child.Name = "child";

    parent.AddChildCategory(child);

    Category childOfTheChild = new Category();
    childOfTheChild.Name = "child of the child";

    child.AddChildCategory(childOfTheChild);

    Global.CurrentSession.SaveOrUpdate(parent);
    transaction.Commit();
}

Поскольку в маппингах установлено каскадирование AllDeleteOrphan то достаточно только сохранения родительской категории. Так же это позволяет управлять поведением в случае удаления. Категория, родитель которой в данный момент удаляется будет удалена вместе с родительской. Если такое поведение вам не нужно, смените тип каскадирования на All.

Немного о производительности

Вопросы выборок иерархических структур сложны в самом SQL. Если оставить все как есть, то Nhibernate будет работать в режиме обычного lazy load и вытаскивать каждую коллекцию подкатегорий отдельным запросом. Для примера следующий код:

protected void Button1_Click(object sender, EventArgs e)
{
    ICriteria criteria = Global.CurrentSession.CreateCriteria(typeof (Category));
    criteria.Add(Restrictions.IsNull("ParentCategory"));
    foreach (Category category in criteria.List<Category>())
    {
        EnumerateChilds(category);
    }
}

protected void EnumerateChilds(Category category)
{
    foreach (Category childCategory in category.ChildCategories)
    {
        EnumerateChilds(childCategory);
    }
}

Тут вытаскиваются все родительские категории, и мы пробегаемся по всем дочерним. Если существует следующая вложенность: “Category”->”Child Category”->”Child of child category”, то данный код выполнит запросы:

-- Выбираем родительскую категорию 
SELECT this_.Id as Id0_0_, 
       this_.Name as Name0_0_, 
       this_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] this_ 
WHERE this_.ParentCategoryId is null
-- Выборка подкатегорий каждой из категорий 
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=@p0   /* @p0=4 */

SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=p0    /* @p0=5 */

SELECT childcateg0_.ParentCategoryId as ParentCa3_1_, 
       childcateg0_.Id as Id1_, 
       childcateg0_.Id as Id0_0_, 
       childcateg0_.Name as Name0_0_, 
       childcateg0_.ParentCategoryId as ParentCa3_0_0_ 
FROM [Category] childcateg0_ 
WHERE childcateg0_.ParentCategoryId=@p0   /* @p0=6 */

Как правило категории нужно доставать все сразу, чтобы построить красивое дерево для навигации. На своем блоге Oren Eini показал как можно оптимизировать выборку дерева. Сделать это можно следующим запросом:

var categories = Global.CurrentSession
                       .CreateQuery("select c from Category c join fetch c.ChildCategories")
                       .SetResultTransformer(new DistinctRootEntityResultTransformer())
                       .List<Category>(); 

Данный вариант кода выполнит меньше запросов, но выполнит по одному лишнему на каждую категорию, последнюю в иерархии. Т.е. категория, у которой нет подкатегорий сгенерирует еще один запрос к базе. В частности у меня было 2 такие категории, и были выполнены следующие запросы:

-- statement #1
select category0_.Id                 as Id0_0_,
       childcateg1_.Id               as Id0_1_,
       category0_.Name               as Name0_0_,
       category0_.ParentCategoryId   as ParentCa3_0_0_,
       childcateg1_.Name             as Name0_1_,
       childcateg1_.ParentCategoryId as ParentCa3_0_1_,
       childcateg1_.ParentCategoryId as ParentCa3_0__,
       childcateg1_.Id               as Id0__
from   [Category] category0_
       inner join [Category] childcateg1_
         on category0_.Id = childcateg1_.ParentCategoryId

-- statement #2
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_,
       childcateg0_.Id               as Id1_,
       childcateg0_.Id               as Id0_0_,
       childcateg0_.Name             as Name0_0_,
       childcateg0_.ParentCategoryId as ParentCa3_0_0_
FROM   [Category] childcateg0_
WHERE  childcateg0_.ParentCategoryId = 8 /* @p0 */

-- statement #3
SELECT childcateg0_.ParentCategoryId as ParentCa3_1_,
       childcateg0_.Id               as Id1_,
       childcateg0_.Id               as Id0_0_,
       childcateg0_.Name             as Name0_0_,
       childcateg0_.ParentCategoryId as ParentCa3_0_0_
FROM   [Category] childcateg0_
WHERE  childcateg0_.ParentCategoryId = 10 /* @p0 */

8я и 10я категории не содержат подкатегорий. Данный вопрос обсуждается в группе NHUsers. Ответов почему это происходит, равно как и решения задачи там пока не обнаружилось. Так что не факт что выполнив такой запрос Вы уменьшите нагрузку на базу.

Такое же поведение наблюдается если выключить lazy load и поставить fetch-mode=”join”.

Еще одна проблема с этим это то, что он исключает категории, у которых есть родитель, но нет подкатегорий. Соответственно если вложенность больше одного уровня, вы получите лишние результаты.

Поэтому я бы предпочел использовать стандартный lazy load до тех пор, пока это не станет реальной проблемой для производительности. Как только станет заметно тормозить, то оптимизировать, либо хранимкой, либо еще как.

Пример кода.

Dec 25, 2009

Fluent NHibernate. Маппинг наследования

Гугл подсказал мне, что блог находят в основном при поиске того, как замапить различные виды наследования используя Fluent Nhibernate. Попробую осветить этот вопрос.

Nhibernate поддерживает 3 способа реализации наследования. Различаются они количеством необходимых для этого таблиц. Далее будут рассмотрены 2 из них подробнее.

Table per class hierarchy

В данной схеме для всей иерархии классов используется одна таблица. Рассмотрим пример со следующей моделью:

image

Для хранения такой структуры будет достаточно одной таблицы с полями Id, Name, UploadDate, WhoIsOnPicture, Lenght, Type. Стоит обратить внимание на то, что поле Type используется Nhibernate’ом для того, чтобы узнать какой конкретный тип находится в конкретной строке. Итак маппинг будет выглядеть следующим образом:

public class ContentMapping: ClassMap<Content>
{
    public ContentMapping()
    {
        Id(x => x.Id).GeneratedBy.Native();
        Map(x => x.Name).Length(200).Not.Nullable();
        Map(x => x.UploadDate).Not.Nullable();
        DiscriminateSubClassesOnColumn("ContentType");
    }
}

public class PhotoMapping : SubclassMap<Photo>
{
    public PhotoMapping()
    {
        Map(x => x.WhoIsOnPicture);
        DiscriminatorValue("Photo");
    }
}

public class VideoMapping: SubclassMap<Video>
{
    public VideoMapping()
    {
        Map(x => x.Length);
        DiscriminatorValue("Video");
    }
}

Генерируемый XML довольно громоздкий, и полностью приводить его я не буду. Важно лишь то, что сформирован маппинг, и в нем используются теги subclass для photo и video.

Недостатком данной схемы является то, что в таблице Content колонки WhoIsOnPicture и Length обязательно должны позволять сохранять NULL.

Table per subclass

Очень радует, что для того, чтобы использовать этот подход, достаточно просто убрать упоминания Discriminator из классов, маппинг будет абсолютно таким же:

public class ContentMapping: ClassMap<Content>
{
    public ContentMapping()
    {
        Id(x => x.Id).GeneratedBy.Native();
        Map(x => x.Name).Length(200).Not.Nullable();
        Map(x => x.UploadDate).Not.Nullable();
    }
}
public class PhotoMapping : SubclassMap<Photo>
{
    public PhotoMapping()
    {
        Map(x => x.WhoIsOnPicture);
    }
}
public class VideoMapping : SubclassMap<Video>
{
    public VideoMapping()
    {
        Map(x => x.Length);
    }
}

Этот маппинг приведет к использованию joined-subclass элементов.

Oct 22, 2009

NHibernate mappings. Или каталог маппингов

Тут буду собирать полезные статьи относящиеся к маппингу NHibernate.
  • Наследование.
  • property – подробности про маппинг свойств.
  • component – если есть желание иметь вложенные классы в сущностях. Например чтобы у пользователя был адрес в виде объекта и обращаться  к нему можно было как user.Address.Street.
  • one-to-one – связь один к одному.
  • any – для связи таблиц, которые не связаны явно. (не знаю как это описать более точно, надо читать :) ).
  • many-to-any – расширение any маппинга.
  • join – аналог one-to-one, но позволяет замапить несколько таблиц в один объект.
  • dynamic-component – позволяет вынести набор свойств объекта в Dictionary. Возможно редко используемые свойства есть смысл туда вынести, чтобы не засорять сущность.
  • set – один из наиболее часто используемых элементов для маппинга коллекций.
  • Named queries <query/> and <sql-query/> – если нужно встраивать свой sql код в маппинги.
  • map – никогда не использовал сам. Но это еще один вариант организации связей между сущностями. Когда посмотрю возможно опишу более подробно.
  • list – главное отличие list от set – list может содержать повторяющиеся записи. Это значит что если вы попытаетесь в set добавить элемент, который уже содержится в коллекции (проверяется на равенство при помощи метода Equals()), то элемент не будет добавлен. Если же последнее действие проделать с list, то элемент может быть добавлен.

Далее рассмотрим наиболее часто встречающиеся структуры:

  1. Дерево. По поводу дерева еще один пост про то, как его эффективно вытаскивать из базы

Каталог буду пополнять по мере поступления :).

Oct 20, 2009

NHibernate для начинающих. Часть 3. Связи

Линки на остальные статьи

Изначально этот пост задумывался как описания каждого типа связи. Я собирался описать как мапить many-to-many, many-to-one, one-to-many и т.д. Но начав писать, я понял что такой информации и так достаточно в сети. Поэтому пост больше получился про то, как построить доменную модель для связанных сущностей. Данный подход наверняка можно распространить и на любое другое нормальное ORM средство.

В предидущих постах были рассмотрены базовые возможности NHibernate. В них рассматрился один класс Product, который соответствовал одной таблице в базе данных. Но естественно для полноценных бизнес приложений требуется набор объектов, которые взаимодействуют между собой. Например рассмотрим модель, которую можно было бы увидеть у какого нибуть электронного магазина. Нужен список товаров, категории товаров и возможность выполнить заказ. Пусть это приведет к следующей структуре классов:

  Диаграмма классов

На этой схеме изображено следующее:

В каждой категории может быть много продуктов, и продукт может состоять в любом количестве категорий. Каждый заказ может быть сделан только на один продукт. Каждый продукт может получить список своих заказов.

Для хранения данной доменной модели была построена следующая база данных:

 Схема базы данных

Исходный текст классов таков:

public class Category  
{     
    private ISet<Product> products;     
    public Category()     
    {         
        products = new HashedSet<Product>();     
    }
    public virtual string DisplayName { get; set; }     
    public virtual int Id { get; protected set; }     
    public virtual ReadOnlyCollection<Product> Products     
    {        
        get { return new ReadOnlyCollection<Product>(new List<Product>(products));  }     
    }     
    public virtual void AddProduct(Product product)     
    {        
        if(product == null) throw new ArgumentNullException("product");     
        if(!products.Contains(product))         
        {
            product.AddCategory(this);            
            products.Add(product);        
        }     
    }     
    public virtual void RemoveProduct(Product product)     
    {        
        if(product == null) throw new ArgumentNullException("product");
        if(products.Contains(product))
        {            
            products.Remove(product);
            product.RemoveFrom(this);
        }
     }
}
 

Следует обратить внимание на следующие вещи:

  • Поле Id – protected. Это необходимо для защиты данных. Если нечайно поменять Id то можно обновить данные другого объекта, а не ожиемого
  • Все поля и методы должны быть виртуальными. Это необходимо для работы NHiberante
  • В конструкторе инициализируется колекция продуктов, таким образом никогда не будет NullReferenceException
  • Колекция продуктов представлена как ReadOnlyCollection. Такой подход оставляет нам возможность получить полный контроль над логикой добавления и удаления продуктов внутри категории
  • При добавлении продукта в категорию продукту автоматически добавляется эта категория. Это обеспечивает согласованность доменной модели (не может быть такого случая, что в колекции у категории продукт есть, а у продукта нет информации о том, что он содержится в категории)
  • Проверка приходящих параметров. Нельзя пробовать добавлять пустые ссылки на продукты
public class Product
{
     private ISet<Category> categories;
     public Product()
     {
        categories = new HashedSet<Category>();
        Orders = new HashedSet<Order>();
     }
     public virtual int Id { get; protected set; }
     public virtual string Name { get; set; }
     public virtual string Description { get; set; }
     public virtual double Price { get; set; }
     public virtual ReadOnlyCollection<Category> Categories
     {
        get
        {
           return new ReadOnlyCollection<Category>(new List<Category>(categories));
        }
     }
     public virtual ISet<Order> Orders { get; protected set; }
     public virtual void AddCategory(Category category)
     {
         if(category == null)
             throw new ArgumentNullException("category");
         if(!categories.Contains(category))
         {
             categories.Add(category);
             category.AddProduct(this);
         }
      }
      public virtual void RemoveFrom(Category category)
      {
         if(category == null)
             throw new ArgumentNullException("category");
         if(categories.Contains(category))
         {
             categories.Remove(category);
             category.RemoveProduct(this);
         }
       }
}  

Так же как и у категории мы обеспечиваем полный контроль над добавлением и удалением элементов из колекции. Но мы не делаем коллекцию заказов ReadOnly (но по прежнему инициализируем пустой колекцией для отсутствия NullReferenceException в нашем коде). Далее будет рассмотрено почему.

public class Order
{
      protected Order() { }
      public Order(Product product)
      {
         if(product == null)
             throw new ArgumentNullException("product");
         product.Orders.Add(this);
         Product = product;
      }
      public virtual int Id { get; protected set; }
      public virtual Product Product{get;protected set;}
      public virtual int NumberOfItems { get; set; }
      public virtual string Customer { get; set; }
}  

Важно выделить следующее

  • protected констуктор необходим для работы NHiberante
  • Тут можно увидеть почему колекция заказов осталась открытой. Чтобы добавить туда новый элемент необходимо сначала его создать. Но заказ не может существовать без продукта, что и отражено единственном открытом конструкторе
  • Product для заказа содержит protected сеттер. Это сделано для того, чтобы нельзя было у созданного заказа заменить продукт, который покупался.
  • Заказов без продукта существовать не должно. Соответственно при удалении продукта или же удалении связи между продуктом и заказом заказ должен быть удален. Как этого достичь будет показано далее

Для правильной работы рекомендуется всегда оборачивать запросы в транзацкии. Для этого следует изменить соответствующие методы в global.asax:

private ITransaction Transaction;
protected void Application_BeginRequest(object sender, EventArgs e)
{
      CurrentSession = SessionFactory.OpenSession();
      Transaction = CurrentSession.BeginTransaction();
}
protected void Application_EndRequest(object sender, EventArgs e)
{
      if (Server.GetLastError() == null)
      {
         Transaction.Commit();
      }
      else
      {
         Transaction.Rollback();
      }
      if (CurrentSession != null)
         CurrentSession.Dispose();
}   

И наконец маппинги:

public class CategoryMap: ClassMap<Category>
{
      public CategoryMap()
      {
          Table("Categories");
          Id(x => x.Id).GeneratedBy.Native();
          Map(x => x.DisplayName);
          HasManyToMany(x => x.Products)
                             .Access.CamelCaseField()
                             .Cascade.SaveUpdate()
                             .AsSet()
                             .ParentKeyColumn("CategoryId")
                             .ChildKeyColumn("ProductId")
                             .Table("[Products.Categories]");
      }
 }
public class ProductMap : ClassMap<Product>  
{
      public ProductMap()
      {
         Table("Products");
         Id(x => x.Id).GeneratedBy.Native();
         Map(x => x.Description);
         Map(x => x.Name);
         Map(x => x.Price);
         HasManyToMany(x => x.Categories)
                           .Access.CamelCaseField()
                           .Table("[Products.Categories]")
                           .ParentKeyColumn("ProductId")
                          .ChildKeyColumn("CategoryId")
                          .AsSet()
                          .Inverse()
                          .Cascade.SaveUpdate();
         HasMany(x => x.Orders)     
                      .KeyColumn("ProductId")     
                      .Inverse() 
                      .Cascade.AllDeleteOrphan();
      }
  }
  public class OrderMap:ClassMap<Order>
  {
   public OrderMap()
   {
    Table("Orders");
    Id(x => x.Id).GeneratedBy.Native();
    Map(x => x.NumberOfItems);
    Map(x => x.Customer);
    References(x => x.Product, "ProductId");
   }
  }

На что обратить внимание

  • У продукта и категории стоит Cascade.SaveUpdate() для колекции категорий и продукта соответственно. Это значит что сохранение продукта вызывает сохранение категории и наоборот. Чуть более подробно про каскадирование можно почитать тут
  • У колекций продукта стоит Inverse. Это означает что за сохранение связи между категорией и продуктом будет отвечать категория. Более подробно тут
  • У продукта на список заказов стоит Cascade.AllDeleteOrphan(). Это обязывает Nhibernate удалять записи, которые лишились родителя (дополнительно тут)
  • Для many-to-many установлен Access.CamelCaseField(). Это означает, что NHibernate будет использовать приватное поле для работы, это позволяет нам сделать коллекцию продуктов ReadOnlyCollection

Итак теперь можно поработать с полученной моделью:

Category category = new Category
    {
        DisplayName = "Our first category"
    };
Product product = new Product
    {
        Description = "First product description",
        Name = "First product",
        Price = 1
    };
category.AddProduct(product);
Global.CurrentSession.SaveOrUpdate(category); 

Эта часть кода заставит NHiberante выполнить следующий набор sql запросов:

INSERT INTO Categories (DisplayName) VALUES (@p0); select SCOPE_IDENTITY() @p0=N'Our first category'  
INSERT INTO Products (Description, Name, Price) VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY() @p0=N'First product description',@p1=N'First product', @p2=1  
INSERT INTO [Products.Categories] (CategoryId, ProductId) VALUES (@p0, @p1) @p0=1,@p1=1  

Как можно видеть NHiberante сам позаботился о порядке сохранения объектов, и заполнении связующей таблицы.

Теперь проверим связь продуктов и заказов:

Product product = Global.CurrentSession.Get<Product>(1); 
var order = new Order(product)
 {
    NumberOfItems = 5,
    Customer = "customer"
 }; 
Global.CurrentSession.SaveOrUpdate(product); 

Это заставит NHiberante выполнить следующие запросы:

SELECT product0_.Id as Id2_0_, product0_.Description as Descript2_2_0_, product0_.Name as Name2_0_, product0_.Price as Price2_0_  
FROM Products product0_ 
WHERE product0_.Id=@p0,@p0=1  

SELECT orders0_.ProductId as ProductId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.NumberOfItems as NumberOf2_3_0_, orders0_.Customer as Customer3_0_, orders0_.ProductId as ProductId3_0_  
FROM Orders orders0_  
WHERE orders0_.ProductId=@p0,@p0=1  

INSERT INTO Orders (NumberOfItems, Customer, ProductId) VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY(), @p0=5,@p1=N'customer',@p2=1  

(id могут быть другими).

Проверим сценарий в котором удаление связи между продуктом и заказом должно вызывать удаление заказа (это достигается при помощи all-delete-orphan). Например рассмотрим следующий код:

Product product = Global.CurrentSession.Get<Product>(1);
product.Orders.Clear(); 
Global.CurrentSession.SaveOrUpdate(product);

Он выполнит следующие запросы:

SELECT product0_.Id as Id2_0_, product0_.Description as Descript2_2_0_, product0_.Name as Name2_0_, product0_.Price as Price2_0_  
FROM Products product0_ 
WHERE product0_.Id=@p0, @p0=1  

SELECT orders0_.ProductId as ProductId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.NumberOfItems as NumberOf2_3_0_, orders0_.Customer as Customer3_0_, orders0_.ProductId as ProductId3_0_ 
FROM Orders orders0_  
WHERE orders0_.ProductId=@p0, @p0=1  

DELETE FROM Orders WHERE Id = @p0 @p0=2  
DELETE FROM Orders WHERE Id = @p0 @p0=3 
DELETE FROM Orders WHERE Id = @p0 @p0=4  

Были удалены все заказы, которые потеряли связь с продуктом. Самое приятное в таком подходе что в дальнейшем вы работаете только с доменной моделью вообще не заботясь о том, как NHiberante будет управлять сохранением коллекций.

Пример кода

Oct 7, 2009

NHibernate для начинающих. Часть 2. Варианты маппингов

Линки на остальные статьи

Любое бизнес приложение содержит набор некоторых объектов, комбинация который представляет собой доменную модель приложения. Например для электронного магазина такими доменными объектами могут быть: Product, Category, Order и т.д.

Для NHibernate необходимы знания о вашей доменной модели для сохранения и извлечения соответствующих объектов. Для этого используется подход называемый data mapper.

В этой статье будут рассмотрены доступные на данный момент варианты маппингов  NHibernate. Будут рассмотрены:

  • Xml файлы – базовый способ маппинга, все остальные способы сводятся к получению таких xml файлов.
  • Аттрибуты – каждому полю или классу добавляются специальные аттрибуты, которые в дальшем преобразовываются в xml.
  • Fluent маппинг – маппинг при помощи лямбда выражений.

Рассмотрим каждый из них подробнее.

1 Xml файлы

Базовый способ маппинга был показан в первой статье. Могу выделить лишь следущюее:

Преимущества:

  • Самый первый из разработанных в NHibernate, соответственно в интернете множество можно найти примеров.
  • Достаточно .net 2.0

Недостатки:

  • Отсутствие валидации во время компиляции
  • Невозможность переименовать поля при помощи Refactor (в вижуал студии есть отличная возможность для удобного переименовывания полей и методов. Для этого на нужном методе клацаем правой кнопкой, далее в меню выбираем Refactor->Rename. Эта опция переименует метод и обновит все вывозы этого метода или свойства в проекте)
  • Отсутствеие полноценного intellisense (есть xsd схема для маппингов, но имена полей вашей сущности показывать все равно не будет)

2 Аттрибуты

Рассмотрим пример из первой части, только воспользуемся маппингами с помощью аттрибутов. Скачать нужную для этого библиотеку можно тут. После того, как добавлена ссылка на NHibernate.Mapping.Attributes класс Product (описан в первой части) должен выглядеть следующим образом:

[Class(Table = "Products", Name = "InheritanceCore.Product")]
public class Product
{
 [Id(0, Column = "Id", Type = "int", Name="Id")]
 [Generator(1, Class = "native")]
 public virtual int Id { get; protected set; }

 [Property(Name = "Name", Column = "Name", Type = "String")]
 public virtual string Name { get; set; }

 [Property(Name = "Description", Column = "Description", Type = "String")]
 public virtual string Description { get; set; }

 [Property(Name = "Price", Column = "Price", Type = "Double")]
 public virtual double Price { get; set; }
}

Также надо изменить Global.asax следующим образом:

protected static ISessionFactory CreateSessionFactory()
{
 var config = new Configuration().Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "hibernate.config"));
 HbmSerializer.Default.Validate = true;
 var assembly = Assembly.GetAssembly(typeof (Product));
 HbmSerializer.Default.HbmAssembly = assembly.FullName;
 HbmSerializer.Default.HbmNamespace = typeof(Product).Namespace;
 var stream = HbmSerializer.Default.Serialize(assembly);
 config.AddInputStream(stream);
 return config.BuildSessionFactory();
}

Вот и все, остальное остается с первой части без изменений. Таким образом:

Преимущества:

  • Сущности и маппинги находятся рядом
  • Наличие некоторого intellisense

Недостатки

  • Наличие строковых литералов. (Свойства Name, Type). Из-за этого невозможно использовать Refactor и полноценно переименовывать свойства классов
  • Отсутствие валидации во время компиляции. На самом деле можно все атрибуты повесить на одно свойство, результат будет такой же
  • Доменные объекты засоряются огромным количеством аттрибутов, которые усложняют читабельность кода


3 Fluent маппинг

Это наиболее новый способ маппинга.
Рассмотрим как с его помощью замапить тот же самый класс Product.

В этом случае надо изменить метод в global.asax следующим образом:


protected static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
               .Database(MsSqlConfiguration.MsSql2005.ConnectionString(@"your connection string"))
               .Mappings(m => m.FluentMappings.AddFromAssemblyOf<product>())
               .BuildSessionFactory();
}

Для маппинга в проект добавляется еще один класс. Как правило его называют <ClassName>Map, т.е. у для примера Product это будет класс ProductMap. Его содержимое:

public class ProductMap : ClassMap<Product>
{
 public ProductMap()
 {
    Table("Products");
    Id(x => x.Id).GeneratedBy.Native();
    Map(x => x.Description).Column("Description");
    Map(x => x.Name).Column("Name");
    Map(x => x.Price).Column("Price");
 }
}

Тут мы имеем полный intellisense, и все прелести проверки время компиляции.