Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NHibernate - HQL "join fetch" with SetMaxResults throws error

I'm trying to run the simplest query:

string queryStr = "select b " +
                  "from Blog b " +
                  "left outer join fetch b.BlogComments bc";

IList<Blog> blogs = Session.CreateQuery(queryStr)
    .SetMaxResults(10)
    .List<Blog>();

But it throws the following error:

System.ArgumentNullException: Value cannot be null.
Parameter name: source

However, if I remove the 'fetch' from the HQL it works fine. Also if I leave fetch in but remove SetMaxResults it also works fine. It's something to do with the fetch + SetMaxResults combination.

I'm trying to eager load child collections to optimise the query and prevent SELECT N+1 problems. I'm using NHibernate 3.3.1.4000 with a MySQL database.


My mapping:

public class BlogMap : ClassMapping<Blog>
{
    public BlogMap ()
    {
        // other properties (snip)....

        Set(x => x.BlogComments, x =>
        {
            x.Inverse(true);
            x.Cascade(Cascade.All | Cascade.DeleteOrphans);
            x.Lazy(CollectionLazy.Extra);
            x.Key(k => { k.Column("BlogId"); });
        }, x => x.OneToMany());
    }
}


public class BlogCommentMap : ClassMapping<BlogComment>
{
    public BlogCommentMap ()
    {
        // other properties (snip)....

        ManyToOne(x => x.Blog, x =>
        {
            x.Column("BlogId");
            x.NotNullable(true);
        });
    }
}

Stacktrace as requested:

[ArgumentNullException: Value cannot be null.
Parameter name: source]
   System.Linq.Enumerable.ToList(IEnumerable`1 source) +4206743
   NHibernate.Engine.QueryParameters.CreateCopyUsing(RowSelection selection) +178
   NHibernate.Hql.Ast.ANTLR.QueryTranslatorImpl.List(ISessionImplementor session, QueryParameters queryParameters) +210
   NHibernate.Engine.Query.HQLQueryPlan.PerformList(QueryParameters queryParameters, ISessionImplementor session, IList results) +369
   NHibernate.Impl.SessionImpl.List(String query, QueryParameters queryParameters, IList results) +301

[GenericADOException: Could not execute query[SQL: SQL not available]]
   NHibernate.Impl.SessionImpl.List(String query, QueryParameters queryParameters, IList results) +351
   NHibernate.Impl.SessionImpl.List(String query, QueryParameters parameters) +282
   NHibernate.Impl.QueryImpl.List() +162
   WebApp.Repositories.BlogRepository.SearchBlogs(SearchBlogs search) in C:\...path...\BlogRepository.cs:43
   WebApp.Controllers.HomepageController.Index(Int32 page) in C:\...path...\Controllers\HomepageController.cs:54
   lambda_method(Closure , ControllerBase , Object[] ) +101
   System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +17
   System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +208
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +27
   System.Web.Mvc.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() +55
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +263
   System.Web.Mvc.<>c__DisplayClass17.<InvokeActionMethodWithFilters>b__14() +19
   System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +191
   System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +343
   System.Web.Mvc.Controller.ExecuteCore() +116
   System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) +97
   System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) +10
   System.Web.Mvc.<>c__DisplayClassb.<BeginProcessRequest>b__5() +37
   System.Web.Mvc.Async.<>c__DisplayClass1.<MakeVoidDelegate>b__0() +21
   System.Web.Mvc.Async.<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _) +12
   System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62
   System.Web.Mvc.<>c__DisplayClasse.<EndProcessRequest>b__d() +50
   System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f) +7
   System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action) +22
   System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +60
   System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9
   System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +8970061
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184
like image 812
Sunday Ironfoot Avatar asked Dec 15 '12 11:12

Sunday Ironfoot


2 Answers

What you are experiencing is a bug... But in this case it could be a good sign! ;) Because paging (SetFirstResult(), SetMaxResults()) on a query fetching parent and its collection will be returning unexpected results. Let's say, that

DB tables contain this:

  • BlogA
    • BlogCommentA1
    • BlogCommentA2
    • BlogCommentA3
  • BlogB
    • BlogCommentB1
    • BlogCommentB2

QueryOver syntax (which is working) doing the same as the above HQL:

var blogs = session.QueryOver<Blog>()
  .Fetch(c => c.BlogComment).Eager // collection in one SQL
  .Skip(0).Take(2) // paging, but weird...
  .List<Blog>();

.Skip(0).Take(2) - first page, a select resulting in this two rows but ONE BlogA:

| BlogA | BlogCommentA1
| BlogA | BlogCommentA2

.Skip(2).Take(2) - next page, weird... again BlogA

| BlogA | BlogCommentA3
| BlogB | BlogCommentB1

And this is most likely not we want.

Suggestion: go for SELECT 1+1

The most reliable in this case is doing the paging only on the parent object (Blog). And then, when we have in NHibernate session all the blogs (.Skip(2).Take(2)) we will only once call select for all their children (BlogComments).

The easiest way is set the batch-size="x", where x is a number close to the usual page size (e.g. 25 or 50).

.BatchSize(25)

NHibernate documentation 19.1.5. Using batch fetching explains details

like image 160
Radim Köhler Avatar answered Nov 12 '22 06:11

Radim Köhler


This looks like a bug either in QueryParameters.CreateCopyUsing(), or something else is not creating the original QueryParameters correctly. Anyway, using SetMaxResults() with collection joins will force the paging to be applied client side, not in the database, which may not be what you want.

You propably want to rewrite that to use a IN on a subquery that yields 10 blog id and have the join fetching in the outer query.

like image 28
Oskar Berggren Avatar answered Nov 12 '22 05:11

Oskar Berggren