Thursday, August 23, 2012

Deep copying an object graph with NHibernate is very easy and simple

You were given a task to clone/copy an existing record, down to its collections and sub-collections, and so forth.


It will involve a great deal of code if you will do it manually with SQL, or even from a run-off-the-mill ORM. But with NHibernate, this is a simple undertaking.


With NHibernate, just resetting the object's Id to 0 can make NHibernate able to persist the object (and its collections and sub-collections) to a new row in the database. And parent object's collections and sub-collections are able to reference the cloned parent's assigned key(e.g. SCOPE_IDENTITY). These are done automatically by NHibernate for you.


It's very simple:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using NHibernateDeepCopyDemo.Models;
using NHibernateDeepCopyDemo.DbMapping;

using NHibernate.Linq;

namespace NHibernateDeepCopyDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var d = new DemoNh();

            

            var q = d.MakeCopy(1);                
                
            if (q.QuestionId == 1) throw new Exception("Failed");

            if (q.QuestionComments[0].QuestionCommentId == 1) throw new Exception("Failed");
            if (q.QuestionComments[1].QuestionCommentId == 2) throw new Exception("Failed");

            if (q.Answers[0].AnswerId == 1) throw new Exception("Failed");
            if (q.Answers[1].AnswerId == 2) throw new Exception("Failed");

            if (q.Answers[0].AnswerComments[0].AnswerCommentId == 1) throw new Exception("Failed");
            if (q.Answers[0].AnswerComments[1].AnswerCommentId == 2) throw new Exception("Failed");

            if (q.Answers[1].AnswerComments[0].AnswerCommentId == 3) throw new Exception("Failed");
            if (q.Answers[1].AnswerComments[1].AnswerCommentId == 4) throw new Exception("Failed");

            Console.WriteLine("\nOK!");

            Console.ReadLine();

        }
    }

    public class DemoNh
    {
        public Question MakeCopy(int id)
        {
            using (var sess = NhMapping.GetSessionFactory().OpenSession())
            {
                var q = sess.Query<Question>().Single(x => x.QuestionId == id);               


                foreach (var item in q.QuestionComments)
                {
                    sess.Evict(item);
                    item.QuestionCommentId = 0;                    
                }

                foreach (var ansItem in q.Answers)
                {                            
                    foreach (var ansCommentItem in ansItem.AnswerComments)
                    {
                        sess.Evict(ansCommentItem);
                        ansCommentItem.AnswerCommentId = 0;
                    }

                    sess.Evict(ansItem);
                    ansItem.AnswerId = 0;
                }

                sess.Evict(q);
                q.QuestionId = 0;
                

                var nq = sess.Merge(q);
                sess.Flush();
                return nq;
            }

        }
    }
}



These are the models(think of Stackoverflow):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace NHibernateDeepCopyDemo.Models
{
    public class Question
    {
        public virtual int QuestionId { get; set; }

        public virtual string QuestionText { get; set; }

        public virtual Person AskedBy { get; set; }
        public virtual Person QuestionModifiedBy { get; set; }

        public virtual IList<QuestionComment> QuestionComments { get; set; }
        public virtual IList<Answer> Answers { get; set; }
    }


    public class QuestionComment
    {
        public virtual Question Question { get; set; }

        public virtual int QuestionCommentId { get; set; }

        public virtual string QuestionCommentText { get; set; }

        public virtual Person QuestionCommentBy { get; set; }
    }


    public class Answer
    {
        public virtual Question Question { get; set; }

        public virtual int AnswerId { get; set; }

        public virtual string AnswerText { get; set; }

        public virtual Person AnsweredBy { get; set; }
        public virtual Person AnswerModifiedBy { get; set; }

        public virtual IList<AnswerComment> AnswerComments { get; set; }
    }


    public class AnswerComment
    {
        public virtual Answer Answer { get; set; }

        public virtual int AnswerCommentId { get; set; }

        public virtual string AnswerCommentText { get; set; }

        public virtual Person AnswerCommentBy { get; set; }
    }


    public class Person
    {
        public virtual int PersonId { get; set; }
        public virtual string PersonName { get; set; }
    }
}

Mapping:

using System;
using System.Collections.Generic;
using System.Linq;

using NHibernate;

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using FluentNHibernate.Automapping;
using FluentNHibernate.Conventions.Helpers;

using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;

using NHibernateDeepCopyDemo.Models;

namespace NHibernateDeepCopyDemo.DbMapping
{
    public static class NhMapping
    {
        private static ISessionFactory _isf = null;
        public static ISessionFactory GetSessionFactory()
        {
            if (_isf != null) return _isf;

            var cfg = new StoreConfiguration();

            var sessionFactory = Fluently.Configure()
              .Database(MsSqlConfiguration.MsSql2008.ShowSql().ConnectionString(
                  "Server=localhost; Database=NhFetch; Trusted_Connection=true;"
                  ))
              .Mappings(m =>
                m.AutoMappings
                  .Add(AutoMap.AssemblyOf<Person>(cfg)
                  .Conventions.Add<ReferenceConvention>()
                  .Override<Question>(x => x.HasMany(y => y.QuestionComments).KeyColumn("Question_QuestionId").Cascade.AllDeleteOrphan().Inverse())
                  .Override<Question>(x => x.HasMany(y => y.Answers).KeyColumn("Question_QuestionId").Cascade.AllDeleteOrphan().Inverse())
                  .Override<Answer>(x => x.HasMany(y => y.AnswerComments).KeyColumn("Answer_AnswerId").Cascade.AllDeleteOrphan().Inverse())
                  )
                )
              .BuildSessionFactory();


            _isf = sessionFactory;

            return _isf;
        }
    }


    public class StoreConfiguration : DefaultAutomappingConfiguration
    {
        readonly IList<Type> _objectsToMap = new List<Type>()
        {
            // whitelisted objects to map
            typeof(Person), typeof(Question), typeof(QuestionComment), typeof(Answer), typeof(AnswerComment)
        };
        public override bool IsId(FluentNHibernate.Member member)
        {
            // return base.IsId(member);
            return member.Name == member.DeclaringType.Name + "Id";
        }
        public override bool ShouldMap(Type type) { return _objectsToMap.Any(x => x == type); }


    }

    public class ReferenceConvention : IReferenceConvention
    {
        public void Apply(IManyToOneInstance instance)
        {
            instance.Column(
                instance.Name + "_" + instance.Property.PropertyType.Name + "Id");
        }
    }

}


Supporting database:

drop table AnswerComment;
drop table Answer;
drop table QuestionComment;
drop table Question;
drop table Person;

create table Person
(
PersonId int identity(1,1) primary key,
PersonName nvarchar(100) not null
);

create table Question
(
QuestionId int identity(1,1) primary key,
QuestionText nvarchar(100) not null,
AskedBy_PersonId int not null references Person(PersonId),
QuestionModifiedBy_PersonId int null references Person(PersonId)
);

create table QuestionComment
(
Question_QuestionId int not null references Question(QuestionId),
QuestionCommentId int identity(1,1) primary key,
QuestionCommentText nvarchar(100) not null,
QuestionCommentBy_PersonId int not null references Person(PersonId)
);

create table Answer
(
Question_QuestionId int not null references Question(QuestionId),
AnswerId int identity(1,1) primary key,
AnswerText nvarchar(100) not null,
AnsweredBy_PersonId int not null references Person(PersonId),
AnswerModifiedBy_PersonId int null references Person(PersonId)
);

create table AnswerComment
(
Answer_AnswerId int not null references Answer(AnswerId),
AnswerCommentId int identity(1,1) primary key,
AnswerCommentText nvarchar(100) not null,
AnswerCommentBy_PersonId int not null references Person(PersonId)
);


insert into Person(PersonName) values('John');
declare @john int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Paul');
declare @paul int = SCOPE_IDENTITY();

insert into Person(PersonName) values('George');
declare @george int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Ringo');
declare @ringo int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Brian');
declare @brian int = SCOPE_IDENTITY();



insert into Person(PersonName) values('Ely');
declare @ely int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Raymund');
declare @raymund int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Buddy');
declare @buddy int = SCOPE_IDENTITY();

insert into Person(PersonName) values('Marcus');
declare @marcus int = SCOPE_IDENTITY();




insert into Question(QuestionText,AskedBy_PersonId) values('What''s the answer to life and everything?',@john);
declare @question int = SCOPE_IDENTITY();

insert into QuestionComment(Question_QuestionId,QuestionCommentText,QuestionCommentBy_PersonId) values(@question,'what is that?',@paul);
insert into QuestionComment(Question_QuestionId,QuestionCommentText,QuestionCommentBy_PersonId) values(@question,'nice question',@george);

insert into Answer(Question_QuestionId,AnswerText,AnsweredBy_PersonId) values(@question,'42',@ringo);
declare @answer1 int = SCOPE_IDENTITY();
insert into Answer(Question_QuestionId,AnswerText,AnsweredBy_PersonId) values(@question,'9',@brian);
declare @answer2 int = SCOPE_IDENTITY();

insert into AnswerComment(Answer_AnswerId,AnswerCommentText,AnswerCommentBy_PersonId) values(@answer1, 'I think so', @ely);
insert into AnswerComment(Answer_AnswerId,AnswerCommentText,AnswerCommentBy_PersonId) values(@answer1, 'I''m sure', @raymund);


insert into AnswerComment(Answer_AnswerId,AnswerCommentText,AnswerCommentBy_PersonId) values(@answer2, 'Really 9?', @ely);
insert into AnswerComment(Answer_AnswerId,AnswerCommentText,AnswerCommentBy_PersonId) values(@answer2, 'Maybe 10?', @raymund);



select * from Question;
select * from QuestionComment;
select * from Answer;
select * from AnswerComment;


As far as I know, doing this on other ORMs is very tedious.


Happy Computing! ツ

No comments:

Post a Comment