Search code examples
c#nhibernateeager-loadingfetching-strategyselect-n-plus-1

Session.Get loads with N+1 even when lazy=false


I have entities like these:

public class User
{
     public virtual int Id { get; set;}
    public virtual Iesi.Collections.Generic.ISet<Character> Characters { get; set; }
}

public class Character
{
     public virtual int Id { get; set;}
     public virtual User User { get; set;}
    public virtual Iesi.Collections.Generic.ISet<UserCharacterSmartChallengeTracker> SmartChallengeTrackers { get; set; }
}

public class UserCharacterSmartChallengeTracker
{
    public virtual int Id { get; set; }
    public virtual int? CharacterId { get; set; }
}

They all are NOT lazy loaded.

When I do session.Get<User>() I see such queries:

SELECT smartchall0_.character_id                   as character3_1_,
       smartchall0_.id                             as id1_,
       smartchall0_.id                             as id51_0_,
       smartchall0_.character_id                   as character3_51_0_,
FROM   public.user_character_smart_challenge_trackers smartchall0_
WHERE  smartchall0_.character_id = 48176 /* :p0 */

SELECT smartchall0_.character_id                   as character3_1_,
       smartchall0_.id                             as id1_,
       smartchall0_.id                             as id51_0_,
       smartchall0_.character_id                   as character3_51_0_,
FROM   public.user_character_smart_challenge_trackers smartchall0_
WHERE  smartchall0_.character_id = 48175 /* :p0 */

-- and others

I tried to preload all of them to the session cache:

 var ids = session.Query<Character>().Where(x => x.User.Id == id)
            .Select(x => x.Id)
            .ToArray();
 session.Query<UserCharacterSmartChallengeTracker>().Where(x => ids.Contains(x.Id)).ToArray();

with query

select character0_.id as col_0_0_
from   public.characters character0_
where  character0_.user_id = 9602 /* :p0 */
select usercharac0_.id                             as id51_,
       usercharac0_.character_id                   as character3_5
from   public.user_character_smart_challenge_trackers usercharac0_
where  usercharac0_.id in (48176 /* :p0 */, 48175 /* :p1 */, 48174 /* :p2 */, 48173 /* :p3 */,
                           48172 /* :p4 */, 48171 /* :p5 */, 48170 /* :p6 */, 48169 /* :p7 */)

but NHibernate ignores the fact that they all are already loaded into the session cache and generates the same N+1 queries! How to fix this?


Update: preloading with

session.QueryOver<Character>().Where(x => x.User.Id == id)
                .Fetch(x => x.User).Lazy
                .Fetch(x=>x.SmartChallengeTrackers).Eager
                .List();

removes N+1 but makes NHibernate load characters second time when I do session.Get<User> which I want to avoid!


Solution

  • I used futures:

            var q = session.Query<User>().Where(x => x.Id == id);
    
            var lst = new List<IEnumerable>
            {
                q.FetchMany(x => x.Characters).ToFuture(),
                q.Fetch(x=>x.UpdateableData).ToFuture(),
                session.QueryOver<User>().Where(x => x.Id == id)
                    .Fetch(x=>x.Characters).Eager
                    .Fetch(x => x.Characters.First().SmartChallengeTrackers).Eager
                    .Future()
            };
    
            var r = session.QueryOver<User>().Where(x => x.Id == id)
                .TransformUsing(Transformers.DistinctRootEntity)
                .Future();
    
            foreach (IEnumerable el in lst)
            {
                foreach (object o in el)
                {
    
                }
            }
    
            return r.ToArray();