Lazy=no-proxy ignores join fetch

Description

I found two issues but I put them both here because I think they are related.

1. Setting the value of a property mapped as lazy="no-proxy" while the session is disconnected triggers a LazyInitializationException. (Also, setting it while the session is open doesn't throw the exception but a subsequent getting of the property with disconnected session does.)

2. Doing join fetch on a lazy="no-proxy" property doesn't seem to have any effect, either in HQL, Criteria API or QueryOver

It seems to me that in both cases the property does get set, but the field interceptor ignores this and throws the exception upon GET although it shouldn't.

All of this was done on .Net 3.5 SP1, SQL Server 2005 and 2008.

Here's a couple of unit tests to demonstrate this - add them to NHibernate.Test/GhostProperty/GhostPropertyFixture.cs:

[Test]
public void AcceptPropertySetWithTransientObject()
{
Order order = null;

using (ISession s = OpenSession())
{
order = s.Get<Order>(1);
}

var newPayment = new WireTransfer();

Assert.DoesNotThrow(() => order.Payment = newPayment);

Assert.AreSame(order.Payment, newPayment);
}

[Test]
public void WillFetchJoinInSingleHqlQuery()
{
Order order = null;

using (ISession s = OpenSession())
{
order = s.CreateQuery("from Order o left join fetch o.Payment where o.Id = 1").List<Order>()[0];
}

Assert.DoesNotThrow(() => { var x = order.Payment; });
}

[Test]
public void WillFetchJoinInAdditionalHqlQuery()
{
Order order = null;

// load the order...
ISession s = OpenSession();
order = s.CreateQuery("from Order o where o.Id = 1").List<Order>()[0];
s.Disconnect();

Assert.Throws(typeof(LazyInitializationException), () => { var y = order.Payment; });

s.Reconnect();
// ... then join-fetch the related payment
s.CreateQuery("from Order o left join fetch o.Payment where o.Id = 1").List<Order>();
s.Close();

Assert.DoesNotThrow(() => { var x = order.Payment; });
}

[Test]
public void WillFetchJoinWithCriteria()
{
Order order = null;

// load the order...
ISession s = OpenSession();

var query = s.CreateCriteria<Order>();
query.Add(NHibernate.Criterion.Expression.Eq("Id", 1));
order = query.List<Order>()[0];
s.Disconnect();

Assert.Throws(typeof(LazyInitializationException), () => { var y = order.Payment; });

s.Reconnect();

// ... then join-fetch the related payment
var query2 = s.CreateCriteria<Order>();
query2.Add(NHibernate.Criterion.Expression.Eq("Id", 1));
query2.SetFetchMode("Payment", FetchMode.Eager);
query2.List();
s.Close();

Assert.DoesNotThrow(() => { var x = order.Payment; });
}

Now, I did some research on the matter and have a couple of ideas why this is... Note that I am by no means an expert on NHibernate internal logic, so take this with a big grain of salt (two grains, even ).

It seems to me that the AbstractFieldInterceptor class (located in NHibernate/Intercept) isn't no-proxy-aware (or not enough), it looks as though it treats the issues I mentioned in a "lazy=proxy" fashion. For one, it doesn't discern between setting and getting properties: with lazy=proxy this makes sense because accessing a property on a proxy triggers the initialization of the proxy, and it doesn't matter if you called GET or SET. With lazy=no-proxy, accessing a property initializes the property and not its owner, and I think setting the property should be allowed at all times. The interceptor should remember that the property was set and treat it as initialized.

I managed to get the first unit test working by adding a new input parameter to the AbstractFieldInterceptor.Intercept method: this parameter states whether it is GET or SET that we're intercepting. Since the method is called only from one class, this isn't too hard or too risky, but I'm unsure how this affects Hibernate compatibility... At the top of the method, I added logic to treat each SET access on a no-proxy property as an initialization:

public object Intercept(object target, string fieldName, object value, bool isSetAccessor)
{
if (this.unwrapProxyFieldNames.Contains(fieldName))
{
if (isSetAccessor)
{
if (!loadedUnwrapProxyFieldNames.Contains(fieldName))
{
loadedUnwrapProxyFieldNames.Add(fieldName);
}
return InvokeImplementation;
}
// get accessor
else if (loadedUnwrapProxyFieldNames.Contains(fieldName))
{
return InvokeImplementation;
}
}

[... the rest of the method ...]

The remaining problem here is that the interceptor is created after the properties are initialized on an object, so it cannot intercept that SET call and join fetch still doesn't work. I worked around this issue by adding !value.IsProxy() to some of the conditions above, but I'm sure it's not a regular solution. If someone can give me a hint towards a proper way to resolve this stuff, I'm willing to give it a try - although I suspect for someone "in the know" it would be easier to implement the solution than to explain it .

Environment

None

Assignee

Unassigned

Reporter

Boris Drajer

Labels

Components

Fix versions

Affects versions

Priority

Major
Configure