Saturday, August 25, 2012

Eager-loading deep object graph from repository object

If your repository component don't have an abstraction layer for eager-loading deep object graph, your ORM will spill to your controller. Caveat: just detect the provider, so your MVC app is still unit-testing-friendly


public class SomeController : Controller
{
    const string EFIndicator = "System.Data.Entity.Internal.Linq.DbQueryProvider";

    IQueryable<Question> _q = null;

    public SomeController (IQueryable<Question> q)
    {
        _q = q;
    }
 
    public ViewResult ShowEverything(int id)
    {

        Question q = null;
        var q1 = _q.Where(x => x.QuestionId == id);


        if (_q.Provider.ToString() == EFIndicator)
        {
            var backToEf = (System.Data.Entity.Infrastructure.DbQuery<Question>)q1;

            q = backToEf
            .Include("AskedBy")
            .Include("QuestionModifiedBy")

            .Include("QuestionComments")

            .Include("Answers")
                .Include("Answers.AnswerComments")

            .Single();
        }

        return View(q);

    }

}


And you have to anticipate different eager-loading mechanism based on the IQueryable ORM provider too, e.g. NHibernate:


public ViewResult ShowEverything(int id)
{


    Question q = null;
    var q1 = _q.Where(x => x.QuestionId == id);



    if (_q.Provider.ToString() == EFIndicator)
    {

        var backToEf = (System.Data.Entity.Infrastructure.DbQuery<question>)q1;

        q = backToEf
        .Include("AskedBy")
        .Include("QuestionModifiedBy")

        .Include("QuestionComments")

        .Include("Answers")
            .Include("Answers.AnswerComments")

        .Single();

    }
    else if (_q.Provider is NHibernate.Linq.DefaultQueryProvider)
    {
        q1
        .FetchMany(x => x.QuestionComments)
        .ToFuture();


        q1
        .Fetch(x => x.AskedBy)
        .Fetch(x => x.QuestionModifiedBy)
        .FetchMany(x => x.Answers)
        .ToFuture();


        //sess.Query<answer>()
        //.Where(x => x.Question.QuestionId == id)
        //.FetchMany(x => x.AnswerComments)
        //.ToFuture();

        q = q1.ToFuture().Single();
    }
    else
    {
        // Repository pattern is unit-testing friendly code ツ
        q = q1.ToSingle();
    }


    return View(q);
   
}




You'll notice in the code above that we cannot eager-load NHibernate's AnswerComments. So you have to pass the Answer repository too. An argument can be made that NHibernate can leak repository abstractions. Following is the repository pattern-using code that supports NHibernate eager-loading mechanism. Though if you have a good dependency injection component, you don't have to worry about changes, laugh in the face of changes when you use empowering tools.


public class SomeController : Controller
{
     const string EFIndicator = "System.Data.Entity.Internal.Linq.DbQueryProvider";


     IQueryable<Question> _q = null;
     IQueryable<Answer> _a = null;

     public SomeController (IQueryable<Question> q, IQueryable<Answer> a)
     {
      _q = q;
      _a = a;
     }
     
    public ViewResult ShowEverything(int id)
    {

        Question q = null;
        var q1 = _q.Where(x => x.QuestionId == id);



        if (_q.Provider.ToString() == EFIndicator)
        {

            var backToEf = (System.Data.Entity.Infrastructure.DbQuery<Question>)q1;

            q = backToEf
                .Include("AskedBy")
                .Include("QuestionModifiedBy")

                .Include("QuestionComments")

                .Include("Answers")
                .Include("Answers.AnswerComments")


            .Single();

        }
        else if (_q.Provider is NHibernate.Linq.DefaultQueryProvider)
        {
            q1
            .FetchMany(x => x.QuestionComments)
            .ToFuture();


            q1
            .Fetch(x => x.AskedBy)
            .Fetch(x => x.QuestionModifiedBy)
            .FetchMany(x => x.Answers)
            .ToFuture();


            _a
            .Where(x => x.Question.QuestionId == id)
            .FetchMany(x => x.AnswerComments)
            .ToFuture();

            q = q1.ToFuture().Single();
        }
        else {} // Very unit-testing-friendly :-)

        q.QuestionText = q.QuestionText + "?";

        return View(q);

        }
    }


Sample unit test:
// Arrange
int id = 1;
string qt = "Answer to life and everything";

var questions = new List<Question> { new Question{ QuestionId = id, QuestionText = qt } }.AsQueryable();
var answers = new List<Answers> { new Answer{ Question = questions[0], AnswerText = "4" } }.AsQueryable();

// Act
Question q = (Question) new SomeController( questions, answers).ShowEverything(id);

// Assert
Assert.AreEqual(qt + "?", q.QuestionText);


Sample integration tests:
using (var db = new EfMapping())
{
        // Arrange
        int id = 1;

        IQueryable<Question> questions = db.Set<Question>();
        IQueryable<Answer> answers = db.Set<Answer>();

        Question qX = db.Set<Question>().Find(id);


        // Act
        Question q = (Question) new SomeController(questions, answers).ShowEverything(id);


        // Assert
        Assert.AreEqual(qX.QuestionText + "?", q.QuestionText);
}

/* for NHibernate:

using (var sess = NhMapping.GetSessionFactory().OpenSession())
{
        // Arrange
        int id = 1;

        IQueryable<Question> questions = sess.Query<Question>();
        IQueryable<Answer> answers = sess.Query<Answer>();

        Question qX = sess.Query<Question>().Load(id);


        // Act
        Question q = (Question) new SomeController(questions, answers).ShowEverything(id);


        // Assert
        Assert.AreEqual(qX.QuestionText + "?", q.QuestionText);
*/


No comments:

Post a Comment