Search code examples
nhibernatefluent-nhibernatequeryover

When does NHibernate JoinQueryOver load full collection?


I've got a model where a Person has a lazy-loaded collection of Registrations. My NHibernate 3.3.3 query uses JoinQueryOver across that relation and some more:

var reg = Session.QueryOver<Person>()
    .Where(p => p.ID == pid)
    .JoinQueryOver<Registration>(p => p.Registrations)
    .Where(r => r.IsDropped == false)
    .JoinQueryOver<Slot>(r => r.Slot)
    .JoinQueryOver<Position>(s => s.Position)
    .JoinQueryOver<AcademicTerm>(p => p.Term)
    .Where(t => t.ID == currentTerm.ID)
    .SingleOrDefault();

In the log, I see the expected SQL for the query, and I get the expected Person instance, but when I access the Registrations collection, the full collection of registrations gets (lazy) loaded. I was expecting it would just be a partial collection of the registrations that meet the criteria.

In a previous question, I had the opposite problem, and fixed it by adding a Fetch() clause to the query.

When does a JoinQueryOver criterion cause the root entity to have a partial collection, and when does it have an unloaded collection that will get lazy loaded? I feel like I should understand this.

My fluent mapping for Person:

public class PersonMapping : ClassMap<Person>
{
    public PersonMapping()
    {
        Id(x => x.ID).GeneratedBy.Guid();
        Map(x => x.Username).Length(20).Unique();
        Map(x => x.HashedPassword).Length(Security.HashedPasswordEncodedLength);
        Map(x => x.PasswordSalt).Length(Security.PasswordSaltEncodedLength);
        Map(x => x.LastName).Length(25).Index("FullName");
        Map(x => x.FirstName).Length(20).Index("FullName");
        Map(x => x.StudentID).Length(12).Nullable();
        Map(x => x.EmployeeID).Length(12).Nullable();
        Map(x => x.EmailAddress).Length(72);
        Map(x => x.IsAdmin).Nullable();
        Map(x => x.IsAgencyStaff).Nullable();
        Map(x => x.IsCenterStaff).Nullable();
        Map(x => x.IsFaculty).Nullable();
        Map(x => x.IsRegistrant).Nullable();
        Map(x => x.IsStudent).Nullable();

        Map(x => x.Type);
        Map(x => x.AuthenticationMode);

        HasMany<Registration>(x => x.Registrations).KeyColumn("Registrant_id")
            .LazyLoad()
            .AsBag()
            .Inverse()
            .Access.CamelCaseField(Prefix.Underscore)
            .Cascade.SaveUpdate();
    }
}

Update Comparing this with my other question (linked above), where I had the opposite problem, that one uses a Left.JoinQueryOver to get from the base entity to the collection. I tried that here, and then got the expected partial collection. (I don't actually need the left join in the other case; I don't remember why it's there.)

The records fetched are the same with or without the left join, but NHibernate populates the collection in one case, and not in the other. I doubt that's behavior I can depend on, so I've changed my strategy to query for the registrations separately.


Solution

  • This condition in the QueryOver:

    ...
    .JoinQueryOver<Registration>(p => p.Registrations)
    .Where(r => r.IsDropped == false)
    ...
    

    Is in fact a WHERE clause for a Person. I mean, this condition is deciding if there is such a Person, having at least one Registration which is NOT dropped. It is not a way, how to filter the Registration colleciton. BUT:

    NHibernate has two nice ways how to filter collections themselves. These are where clause and filter.

    1. where clause

    Easily said: we can extend the collection mapping, whit a SQL Statement, which will be always added as a where condition. See where 6.2. Mapping a Collection, extract:

    where="arbitrary sql where condition" == (optional) specify an arbitrary SQL WHERE condition to be used when retrieving or removing the collection (useful if the collection should contain only a subset of the available data)

    so in our case the mapping:

    HasMany<Registration>(
      ...
      .Where("IsDropped = 0"); // SQL Statement
    

    2. filter setting

    While where clause is static, baked into the assembly/mapping... filter is a way how to do that dynamically. So just imagine, that we would sometimes like to have a set of Registrations, which are dropped, next time, which are not dropped.

    We would like to be able to change "IsDropped = 0" to "IsDropped = 1" (better scenario could be switch by culture or lang). And that's where the filter could be applied, see 18.1. NHibernate filters

    There is a small draft of the filter stuff with fluent, see the links below to get a complete picture.

    HasMany
      ...
      .ApplyFilter<MyFilter>()
    

    And filter could be like:

    public class MyFilter: FilterDefinition
    {
        public MyFilter()
        {
            WithName("dropped")
              .WithCondition("IsDropped == :isDropped")
              .AddParameter("isDropped", NHibernate.NHibernateUtil.Boolean);
        }
    }
    

    And finally, we can enable the Filter for a whole session (coool, all collections will be filtered the same way in that transaction/session, all collection mapped with MyFilter)

    session.EnableFilter("dropped").SetParameter("isDropped", false);
    

    See more here: