Friday, 29 September 2017

Fluent NHibernate and Collections Mapping

You can find some bits and pieces about mapping collections with NHibernate in many different places but yet I decided to write another post about it. What is different about my post? I hope to gather here all (in one place) relevant information regarding the most common mappings: many-to-one/one-to-many and many-to-many. In my examples I'm useing Fluent NHibernate API but also XML counterpart are included. All examples are based on the following schema: (subset of AdventureWorks database)


Bidirectional many-to-one/one-to-many

This is the most common type of association, I have used Product and ProductReview tables to depict how it works and how it can be mapped.

In our case each product (one) can have multiple reviews (many). On ProductReview side association can be mapped like this:

   References(x => x.Product, "ProductID").FetchType.Join();
which is equal to:

   <many-to-one fetch="join" name="Product" column="ProductID" />
What is FetchType doing? Basically FetchType can be Select or Join, we can define how we want NHibernate to load product for us, consider this code:

   1:  var review = session.Get<ProductReview>("1");
   2:  var productName = review.Product.Name;    
For FetchType.Join() NHibernate will call database once with following query:

   SELECT ... FROM ProductReview pr left outer join Product p on pr.ProductID=p.ProductID WHERE ... 
As you can see review and product are loaded with one call. For FetchType.Select() we will get two calls:

   SELECT ... FROM ProductReview pr WHERE ... 
   SELECT ... FROM Product p WHERE ... 
Second call will be executed on demand, it means, only if we try to use Product object like in above example: var productName = review.Product.Name;

In general you have to determine in each case which FetchType is more beneficial for you.

Now, lets check Product side, this is many side of one-to-many association so Product has a collection of reviews, I have chosen to use ISet:

   1:  HasMany(x => x.ProductReview)
   2:      .KeyColumnNames.Add("ProductID")
   3:      .AsSet()
   4:      .Inverse()
   5:      .Cascade.All();
corresponding XML mapping:

   1:      <set name="ProductReview" inverse="true" cascade="all">
   2:        <key column="ProductID" />
   3:        <one-to-many class="AdventureWorksPlayground.Domain.Production.ProductReview, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
   4:      </set>
Here you can find two confusing things:
  • inverse="true" - it tells NHibernate that other side of this association is a parent. I know that it sounds other way round but that's how it is. ProductReview table has foreign key (and ProductID column) therefore ProductReview controls the association.
    What are the implications? In above example review.Product has to be set correctly, as this the property which NHibernate will check to figure our what product is associated with the review. It will ignore collection of reviews on product!
  • cascade="all" - it tells NHibernate that all events (like save, update, delete) should be propagated down. Calling session.SaveOrUpdate(product) will save (or update) the product itself but also the same event will be applied to all depending objects.
We are almost ready to move to many-to-many associations, but before we do that, check this piece of code:

   1:  var product = new Product
   2:                    {
   3:                        Name = "Bike",
   4:                        SellStartDate = DateTime.Today
   5:                    };
   6:  
   7:  product.ProductReview.Add(new ProductReview
   8:                                {
   9:                                    Product = product,
  10:                                    Rating = 4,
  11:                                    ReviewerName = "Bob",
  12:                                    ReviewDate = DateTime.Today
  13:                                });
  14:  
  15:  product.ProductReview.Add(new ProductReview
  16:                                {
  17:                                    Product = product,
  18:                                    Rating = 2,
  19:                                    ReviewerName = "John",
  20:                                    ReviewDate = DateTime.Today
  21:                                });
  22:  
  23:  
  24:  session.SaveOrUpdate(product);

Can you see a potential problem here? Each ProductReview knows about its Product, and thanks to cascade="all" everything is configured correctly but still you may end up with just one review in database ... why? I'm using ISet here, so it guarantees that I have only unique objects in the collection. Most of the people know that NHibernate classes should have Equals() and GetHashCode() methods overridden. It is useful when you want to check that two objects represent the same row in a database. People use primary id column in Equals() implementation, primary id is unique so it fits perfectly isn't it? It does if primary key is defined, and in above example, objects are saved in a last line, before that, they don't have any primary id. That is a reason for using different data to determine equality.

Bidirectional many-to-many association
For this association I have used Product, ProductProductPhoto (link) and ProductPhoto tables. Each product can have multiple photos, but each photo can also be associated with multiple products. ProductProductPhoto is just a link table and doesn't have any representation as a separate class. On both sides mapping looks very similarly.

Product side:

   1:  HasManyToMany(x => x.Photos)
   2:      .AsBag()
   3:      .WithTableName("Production.ProductProductPhoto")
   4:      .WithParentKeyColumn("ProductID")
   5:      .WithChildKeyColumn("ProductPhotoID")
   6:      .Cascade.All();
which produces XML like this:

   1:  <bag name="Photos" cascade="all" table="Production.ProductProductPhoto">
   2:        <key column="ProductID" />
   3:        <many-to-many column="ProductPhotoID" class="AdventureWorksPlayground.Domain.Production.ProductPhoto, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
   4:  </bag>
and ProductPhoto side:

   1:  HasManyToMany(x => x.Products)
   2:      .AsBag()
   3:      .WithTableName("Production.ProductProductPhoto")
   4:      .WithParentKeyColumn("ProductPhotoID")
   5:      .WithChildKeyColumn("ProductID")
   6:      .Inverse();
XML:

   1:  <bag name="Products" inverse="true" table="Production.ProductProductPhoto">
   2:        <key column="ProductPhotoID" />
   3:        <many-to-many column="ProductID" class="AdventureWorksPlayground.Domain.Production.Product, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
   4:  </bag>
In efect Product has a collection of Photos (IList<ProductPhoto> Photos) and ProductPhoto has a collection of products. It's mandatory, in cases like this, to mark one side as inverse="true".

This is fairly straightforward example but unfortunately not very common. In typical case link table has some additional data (like ProductDocument which has ModifiedDate column) and those additional data forces us to use different approach. Among NHibernate best practices you can find general guideline:

Good usecases for a real many-to-many associations are rare. Most of the time you need additional information stored in the "link table". In this case, it is much better to use two one-to-many associations to an intermediate link class. In fact, we think that most associations are one-to-many and many-to-one, you should be careful when using any other association style and ask yourself if it is really necessary.
So, in fact, for tables Product, Document and ProductDocument, we have to create three classes and three mappings. Both Product and Document have a link to each other through ProductDocument object. Interesting part is ProductDocument which has composite primary id (two columns) which can be mapped in a following way:

   1:      public class ProductDocumentMap : ClassMap<ProductDocument>
   2:      {
   3:          public ProductDocumentMap()
   4:          {
   5:              UseCompositeId()
   6:                  .WithKeyReference(x => x.Product, "ProductID")
   7:                  .WithKeyReference(x => x.Document, "DocumentID");
   8:  
   9:              Map(x => x.ModifiedDate).Not.Nullable();
  10:          }
  11:      }
and it generates XML like this:

   1:    <class name="ProductDocument" table="Production.ProductDocument" xmlns="urn:nhibernate-mapping-2.2">
   2:      <composite-id>
   3:        <key-many-to-one class="AdventureWorksPlayground.Domain.Production.Product, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Product" column="ProductID" />
   4:        <key-many-to-one class="AdventureWorksPlayground.Domain.Production.Document, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Document" column="DocumentID" />
   5:      </composite-id>
   6:      <property name="ModifiedDate" column="ModifiedDate" not-null="true" type="DateTime">
   7:        <column name="ModifiedDate" />
   8:      </property>
   9:    </class>
Then we can write code like this:

   1:      var product = CreateNewProduct();
   2:      var photo1 = CreateNewPhoto();
   3:      var photo2 = CreateNewPhoto();
   4:  
   5:      product.Photos.Add(photo1);
   6:      product.Photos.Add(photo2);
   7:  
   8:      // we don't have to save photos because of Cascade.SaveUpdate()
   9:      // INSERT INTO [Production.Product]
  10:      // INSERT INTO [Production.ProductPhoto]
  11:      // INSERT INTO [Production.ProductPhoto]
  12:      // INSERT INTO [Production.ProductProductPhoto]
  13:      // INSERT INTO [Production.ProductProductPhoto]
  14:      session.SaveOrUpdate(product);
And that is all what I think is important in this subject ... anything missing? Leave a comment and I will try to add missing parts.

No comments:

Post a Comment

Note: only a member of this blog may post a comment.

Blog Archive