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>