1

I've been seeing some strange behavior in NHibernate regarding lazy loading and several filters that are set up in my application. It seems that if I filter on a lazy-loaded association, that association only has the members that apply to the filter for the remainder of the NHibernate session. I know that sounds confusing, so I've reproduced it in a fairly simple example.

I've made a "PetFun" web application that uses Spring MVC 1.3.2 and NHibernate 3.3.3.4001. This dummy app just has a HomeController, along with database handling for both Pets and Toys. The Pet object has a many-to-many relationship with Toy. The hbm.xml mapping for these two objects:

<class name="Pet" table="PETS">
  <id name="Id" column="ID">
    <generator class="identity"/>
  </id>
  <property name="name" column="NAME" not-null="true" access="field"/>
  <set name="toys" table="PETS_TOYS" lazy="true" access="field">
    <key column ="PET_ID" />
    <many-to-many class="Toy, PetFun.Domain" column="TOY_ID" />
  </set>
</class>

<class name="Toy" table="TOYS">
  <id name="Id" column="ID">
    <generator class="identity"/>
  </id>
  <property name="name" column="NAME" not-null="true" access="field"/>
</class>

I have a Pets repository (autowired into HomeController) that has two functions: GetByName(string name) and GetByToy(Toy toy). Their implementation:

public IEnumerable<Pet> GetByName(string name)
  {
  return databaseSessionManager.GetCurrentSession().CreateCriteria<Pet>()
    .Add(Restrictions.Eq("name", name))
    .List<Pet>();
  }

public IEnumerable<Pet> GetByToy(Toy toy)
  {
  return databaseSessionManager.GetCurrentSession().CreateCriteria<Pet>()
    .CreateAlias("toys", "toys", NHibernate.SqlCommand.JoinType.LeftOuterJoin)
    .Add(Restrictions.Eq("toys.Id", toy.Id))
    .List<Pet>();
  }

I also have a Toys repository with a GetName(string name) function that's the same as what's found in Pets. Finally, I have the following database schema and data set:

IF EXISTS (SELECT 1 FROM sysobjects WHERE xtype='u' AND name='PETS_TOYS') DROP TABLE PETS_TOYS;
IF EXISTS (SELECT 1 FROM sysobjects WHERE xtype='u' AND name='TOYS') DROP TABLE TOYS;
IF EXISTS (SELECT 1 FROM sysobjects WHERE xtype='u' AND name='PETS') DROP TABLE PETS;

CREATE TABLE PETS (
  ID int IDENTITY(1,1) NOT NULL, 
  NAME NVARCHAR(50) NOT NULL 
  CONSTRAINT PK_PETS PRIMARY KEY CLUSTERED 
  ( ID ASC ) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY] ) ON [PRIMARY];

CREATE TABLE TOYS (
  ID int IDENTITY(1,1) NOT NULL, 
  NAME NVARCHAR(50) NOT NULL 
  CONSTRAINT PK_TOYS PRIMARY KEY CLUSTERED 
  ( ID ASC ) WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY] ) ON [PRIMARY]; 

CREATE TABLE PETS_TOYS (
  PET_ID INT NOT NULL, 
  TOY_ID INT NOT NULL, 
  CONSTRAINT FK_PETSTOYS_PETID FOREIGN KEY (PET_ID) REFERENCES PETS(ID) ON DELETE CASCADE, 
  CONSTRAINT FK_PETSTOYS_TOYID FOREIGN KEY (TOY_ID) REFERENCES TOYS(ID) ON DELETE CASCADE);


INSERT INTO PETS (NAME) VALUES('Fido');
INSERT INTO PETS (NAME) VALUES('Einstein');
INSERT INTO PETS (NAME) VALUES('Cujo');

INSERT INTO TOYS (NAME) VALUES('Ball');
INSERT INTO TOYS (NAME) VALUES('Rope');
INSERT INTO TOYS (NAME) VALUES('Frisbee');
INSERT INTO TOYS (NAME) VALUES('Blood');

INSERT INTO PETS_TOYS (PET_ID, TOY_ID) VALUES(1, 1); -- Fido has Ball
INSERT INTO PETS_TOYS (PET_ID, TOY_ID) VALUES(2, 2); -- Einstein has Rope
INSERT INTO PETS_TOYS (PET_ID, TOY_ID) VALUES(3, 1); -- Cujo has Ball
INSERT INTO PETS_TOYS (PET_ID, TOY_ID) VALUES(3, 4); -- Cujo has Blood

The main thing to note about the data itself is that Cujo has two Toys, Ball and Blood.

If I only call something like Pet cujo = pets.GetByName("Cujo").First() , I get what I expect...a Pet object with two Toys associated to it ("Ball" and "Blood"). However, the following yields different results:

Toy ball = toys.GetByName("Ball").First();
IEnumerable<Pet> petsWithBall = pets.GetByToy(ball); // pet.toy.id = ball.id -- causes problem below
Pet cujo = pets.GetByName("Cujo").First();
IEnumerable<Toy> cujoToys = cujo.GetToys(); // cujo's lazy collection only has ball

Now the cujoToys variable only contains one Toy: "Ball". It would seem that executing the query found in Pets#GetByToy (line 2 above) is limiting the Cujo object that's loaded by the second query. The lazy-loaded collection is only being populated by entities that matched the restriction from the first query.

I've noticed that this seems to only be the case when my alias involves a left-outer join. In this particular example, I don't need this join, but my "real" application actually does need the join where the issue is occurring.

The really nasty thing about this is if the lazy-loaded collection gets marked as dirty because I am actually modifying it, NHibernate writes the corrupted/limited form of the data to the database. So if I gave Cujo a Frisbee Toy during this request cycle, he would lose his association to Blood...which would've made for a boring movie.

As a note, if I load Cujo up at the beginning of the next request, he has both of his Toys...so I'm guessing this has something to do with caching (my NHibernate session is scoped to the request cycle).

Has anyone seen anything like this before? I'll be happy to clarify any details where needed. My NHibernate configuration from Web.config, in case it's useful:

<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
  <reflection-optimizer use="false" />
  <session-factory>
    <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>
    <property name="dialect">NHibernate.Dialect.MsSql2005Dialect</property>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <property name="show_sql">true</property>
    <property name="current_session_context_class">managed_web</property>
    <mapping assembly="PetFun.ORM"/>
  </session-factory>
</hibernate-configuration>
iddqd
  • 105
  • 1
  • 10

1 Answers1

1

Well, the way how to fix this scenario, would be to change the: IEnumerable<Pet> GetByToy(Toy toy). This is new implementation:

public IEnumerable<Pet> GetByToy(Toy toy)
{
    // I. we have to firstly create a subquery
    var petsWithToySubquery = DetachedCriteria.For<Pet>("pet")
        // join toys
        .CreateAlias("toys", "toys", NHibernate.SqlCommand.JoinType.LeftOuterJoin)
        // add restriction
        .Add(Restrictions.Eq("toys.Id", toy.Id))
        // and the key to success: select the Pet ID
        .SetProjection(Projections.Property("pet.Id"));

    // II. the correctly initiated list 
    // (ready to properly and lazily load all Toys)
    var list = databaseSessionManager.GetCurrentSession()
        // THE query - to get filtered list of Pets
        .CreateCriteria<Pet>()
        // here we do filtering 
        .Add(Subqueries.PropertyIn("Id", petsWithToySubquery));
        // the list, ready to support lazy loading
        .List<Pet>();

    // THE list of Pets, having searched Toy
    return list;
}

NOTE: In general, I would say, that the many-to-many will always bring you more issues/challenges than benefits.

My suggestion would be, change the pairing table and extend it with its own surrogated key. Explicitly then map this table as the standard Entity. See more here:

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335
  • The ID subquery does indeed appear to fix this. Thanks! It would seem that this is a bug in NHibernate, correct? I can't imagine the impact on a lazy collection to be expected based on the join type... – iddqd May 15 '14 at 17:15
  • My gut feeling is simply: if possible, do not use `many-to-many`. In comparison with other types of mapping, this one is tooo complex and hidding manipulation with a pairing table. So, as I tried to explain in the links I gave you ... maybe other mapping could be *better* working – Radim Köhler May 16 '14 at 12:53