Thursday, January 13, 2011

NHibernate saves your whole object graph even on cross-process scenario. NHibernate+WCF

One of the nicest thing with NHibernate is that it let you concentrate on your problem domain instead of doing low-level plumbing of saving your table(s)


And this is your ever-efficient and succint data component in action:


public int SaveQuestion(Question q)
{
 using (var s = SessionFactory.OpenSession())
 using (var tx = s.BeginTransaction())
 {                
  var qx = (Question)s.Merge(q);
  tx.Commit();
  return qx.QuestionId;
 }
}


And that will work even your data relationships are two or more level deep and still maintain its five-lines-ness regardless of how deep the rabbit hole goes table relationships are. Here are the entities and values, visualize Stackoverflow's database design, a question has many comments, a question has many answers, a given answer has many comments:




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TheEntities;
using System.Runtime.Serialization;

namespace TheEntities
{

    [DataContract(IsReference=true)] 
    [KnownType(typeof(Question))]
    [KnownType(typeof(Answer))]
    [KnownType(typeof(QuestionComment))]
    public class Question
    {
        [DataMember] public virtual int QuestionId { get; set; }

        [DataMember] public virtual string TheQuestion { get; set; }
        [DataMember] public virtual string Poster { get; set; }

        [DataMember] public virtual IList<QuestionComment> Comments { get; set; }
        [DataMember] public virtual IList<Answer> Answers { get; set; }
    }

    [DataContract]    
    [KnownType(typeof(Question))]
    [KnownType(typeof(QuestionComment))]
    public class QuestionComment
    {

        [DataMember] public virtual Question Question { get; set; }

        [DataMember] public virtual int QuestionCommentId { get; set; }

        [DataMember] public virtual string TheQuestionComment { get; set; }
        [DataMember] public virtual string Poster { get; set; }
    }


    [DataContract(IsReference=true)]
    [KnownType(typeof(Question))]
    [KnownType(typeof(Answer))]
    [KnownType(typeof(AnswerComment))]
    public class Answer
    {
        [DataMember] public virtual Question Question { get; set; }

        [DataMember] public virtual int AnswerId { get; set; }

        [DataMember] public virtual string TheAnswer { get; set; }
        [DataMember] public virtual string Poster { get; set; }

        [DataMember] public virtual IList<AnswerComment> Comments { get; set; }
    }


    [DataContract]
    [KnownType(typeof(Answer))]
    [KnownType(typeof(AnswerComment))]
    public class AnswerComment
    {
        [DataMember] public virtual Answer Answer { get; set; }

        [DataMember] public virtual int AnswerCommentId { get; set; }

        [DataMember] public virtual string TheAnswerComment { get; set; }
        [DataMember] public virtual string Poster { get; set; }
    }


}


Following is the mapping (we used Fluent NHibernate instead of XML, so as not to muddle the main point of this topic ;-)



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FluentNHibernate.LowercaseSystem;
using FluentNHibernate.Mapping;

namespace TheEntities.Mapper
{
    public class QuestionMap : ClassMap<Question>
    {
        public QuestionMap()
        {

            Not.LazyLoad();
            Id(x => x.QuestionId).GeneratedBy.Sequence("question_seq");

            Map(x => x.TheQuestion).Not.Nullable();
            Map(x => x.Poster).Not.Nullable();

            HasMany(x => x.Comments).Inverse().Not.LazyLoad().Cascade.AllDeleteOrphan();
            HasMany(x => x.Answers).Inverse().Not.LazyLoad().Cascade.AllDeleteOrphan();
        }
    }

    public class QuestionCommentMap : ClassMap<QuestionComment>
    {
        public QuestionCommentMap()
        {
            Not.LazyLoad();
            References(x => x.Question);    

            Id(x => x.QuestionCommentId).GeneratedBy.Sequence("question_comment_seq");

            Map(x => x.TheQuestionComment).Not.Nullable();
            Map(x => x.Poster).Not.Nullable();
        }
    }

    public class AnswerMap : ClassMap<Answer>
    {
        public AnswerMap()
        {
            Not.LazyLoad();
            References(x => x.Question);

            Id(x => x.AnswerId).GeneratedBy.Sequence("answer_seq");

            Map(x => x.TheAnswer).Not.Nullable();
            Map(x => x.Poster).Not.Nullable();

            HasMany(x => x.Comments).Inverse().Not.LazyLoad().Cascade.AllDeleteOrphan();
        }
    }

    public class AnswerCommentMap : ClassMap<AnswerComment>
    {
        public AnswerCommentMap()
        {
            Not.LazyLoad();
            References(x => x.Answer).Not.Nullable();

            Id(x => x.AnswerCommentId).GeneratedBy.Sequence("answer_comment_seq");

            Map(x => x.TheAnswerComment).Not.Nullable();
            Map(x => x.Poster).Not.Nullable();
        }
    }

}


Everything are pretty much standard routine (tagging DataContract, KnownType, DataMember attributes) to adhere to when you need to transport your POCO across the wire, those attributes has no bearing on NHibernate. The only point of interest is the IsReference(works out-of-the-box on .NET 4.0 or 3.5 SP1) property of DataContract attribute; you need to use it when your child table need to reference the parent table, failing to do so will result to this error:



Object graph for type 'X.Y.Z' contains cycles and cannot be serialized if reference tracking is disabled


That attribute signals the WCF not to serialize the whole contents of the object referenced by the child table, instead it will persist those multiple object references on parent entity (as referenced from child entities) with internal id, and this internal id has no bearing on database id nor NHibernate.



Another point of interest is the way the WCF interpret your collection, which by default is set to System.Array, you need to change that to System.Collections.Generic.List so you can add and delete records. If you just keep it as System.Array, the only thing you can do with records is updating them, adding and deleting record would not be available. This can be done by selecting Configure Service Reference...; this is under the Service References of your project



To visualize the sample entity relationships we are persisting, here's the screenshot:








Here's your data structure:

create table question (question_id int4 not null, the_question varchar(255) not null, poster varchar(255) not null, primary key (question_id));

create table question_comment (question_comment_id int4 not null, the_question_comment varchar(255) not null, poster varchar(255) not null, question_id int4, primary key (question_comment_id));

create table answer (answer_id int4 not null, the_answer varchar(255) not null, poster varchar(255) not null, question_id int4, primary key (answer_id));

create table answer_comment (answer_comment_id int4 not null, the_answer_comment varchar(255) not null, poster varchar(255) not null, answer_id int4 not null,
primary key (answer_comment_id));

alter table question_comment add constraint FK83AC3D002500E3C7 foreign key (question_id) references question;

alter table answer add constraint FK77FA76182500E3C7 foreign key (question_id) references question;

alter table answer_comment add constraint FKD5BEEC96136C8DAF foreign key (answer_id) references answer;

create sequence question_seq;
create sequence question_comment_seq;
create sequence answer_seq;
create sequence answer_comment_seq


Sql Server-compatible:

create table question (question_id INT IDENTITY NOT NULL, the_question NVARCHAR(255) not null, poster NVARCHAR(255) not null, primary key (question_id));
create table question_comment (question_comment_id INT IDENTITY NOT NULL, the_question_comment NVARCHAR(255) not null, poster NVARCHAR(255) not null, question_id INT null, primary key (question_comment_id));
create table answer (answer_id INT IDENTITY NOT NULL, the_answer NVARCHAR(255) not null, poster NVARCHAR(255) not null, question_id INT null, primary key (answer_id));
create table answer_comment (answer_comment_id INT IDENTITY NOT NULL, the_answer_comment NVARCHAR(255) not null, poster NVARCHAR(255) not null, answer_id INT not null, primary key (answer_comment_id));
alter table question_comment add constraint FK83AC3D002500E3C7 foreign key (question_id) references question;
alter table answer add constraint FK77FA76182500E3C7 foreign key (question_id) references question;
alter table answer_comment add constraint FKD5BEEC96136C8DAF foreign key (answer_id) references answer


Download proof of concept for NHibernate WCF



An aside, now I feel dirty I handcrafted SqlDataAdapter, Command, Parameters etc on my projects before. The complexity of persisting your data using the traditional ADO.NET approach is directly proportional to the complexity and number of your tables and its relationships, though the code generator I made have alleviated those problems a bit; but still, the code quality feels brittle. I had a desktop application that has 200 lines of code just to persist a 4 level deep table relationships. With NHibernate, your persistence code can still maintain its five-lines-ness regardless of complexities of your table relationships. I should have used NHibernate(or any good ORM for that matter) long time ago

7 comments:

  1. I'm starting to hate Entity Framework.

    ReplyDelete
  2. Great article!. I just want to comment something. I have a problem by using Merge method because I have a unique key in a relation table. My model is the following: Order, OrderItem and Product. 1 Order has many OrderItem and 1 OrderItem belongs to 1 Product. OrderItem has a unique key composed by "OrderId, ProductId", and when the User deletes and re-inserts items on the client-side application with a ProductId that "already" exists in the same Order and then call Merge method it throws a unique key exception. Apparenttly NHibernate is trying to execute the insert before delete...does anyone have an idea/clue/workaround how to fix this problem?

    Thanks in advance!

    ReplyDelete
    Replies
    1. I encountered the same problem, I don't know why NHibernate team opted to do writes in that sequence. When I tried to mimic NHibernate's Merge functionality on Entity Framework, I made the merge functionality do the writes in this sequence: DELETE, UPDATE, INSERT to avoid "already" exists problem

      Line 569:
      https://github.com/MichaelBuen/ToTheEfnhX/blob/master/Ienablemuch.ToTheEfnhX.EntityFramework/EfRepository.cs


      I'm inclined to modify NHibernate to do writes in proper sequence, so that kind of problem will not happen. I think it can be requested on NHibernate team :-)

      Delete
    2. Thanks a lot for your quick answer! I think this is a "bug" on NHibernate, so (IMHO) it should be fixed since there are a lot of people using that method.

      Delete
  3. I think another approach is better than the one here. Instead of sending the entire modified object graph over the wire, why not instead just send the delta? You wouldn't even need generated DTO classes in that case. If you have an opinion about this either way, please let's discuss is at http://stackoverflow.com/questions/1344066/calculate-object-delta .

    ReplyDelete
    Replies
    1. I concur it's better to use delta especially if the bandwidth is constrained. This post just show that it's possible to send an object graph to NHibernate with ease. An object graph could also carry delta instead of sending the entire graph. Time permitting, I will make a POC on that

      Delete
  4. Nice write up. Extra points for using some generic-well-known domain objects, not something specific to your domain.

    Here is the EF (Entity Framework) d@gger. No .Merge in EF is a deal breaker.

    http://msdn.microsoft.com/en-us/library/ee373856.aspx

    If you are using POCO entities without proxies, you must call the DetectChanges method to synchronize the related objects in the object context.
    -->> If you are working with disconnected objects you must manually manage the synchronization. <<--

    ReplyDelete