2

I have a database fetch that handles a full object graph using multiple future queries. It looks something like this (class names have been changed to protect the innocent):

Foo foo;
var fooFuture = Session.QueryOver<Foo>()
                       .Where(f => f.Id == id)
                       .Future();

// load up the Blah collection for the Foo
Session.QueryOver<Foo>()
       .Fetch(f => f.Blahs).Eager
       .Where(f => f.Id == id)
       .Future();

// do a separate query for the BlahOptions -- this is needed
// because if we did a Join and Join off of Foo, we'd get duplicate Blah
// results and that's not good
Session.QueryOver<Blah>()
       .Fetch(b => b.Options).Eager
       .Where(b => b.Foo.Id == id)
       .Future();

// load up the Attributes for the Foo
Session.QueryOver<Foo>()
       .Fetch(f => f.Attributes).Eager
       .Where(f => f.Id == id)
       .Future();

foo = fooFuture.SingleOrDefault();

NOTE: I can implement this in NHibernate LINQ, but the behavior remains the same.

The weirdness: foo will sometimes be of type FooNamespace.Foo (the correct, concrete type) and other times it will be Castle.Proxies.FooProxy.

Whether I get the real type or a proxied type seems to depend on whether or not NHibernate has been used previously in the session. When this query occurs after other NHibernate queries, it returns a FooProxy. If it's the first use of the session, it returns a Foo.

Why does this happen? How can I prevent it from happening? I'm purposefully fetching the entire object graph of Foo to make sure there are no proxies. And graph itself doesn't contain any proxies, it's just the root Foo reference. The fact that the returned type depends on what NHibernate has done before seems to be the key (and really weird).

A couple closing notes:

  • NHibernateUtil.IsInitialized returns true when fed a FooProxy, so I can't assure it's not a proxy that way
  • I'm proxy-adverse because the graph will be serialized for caching purposes and deserialization croaks when working with FooProxy

Any insight is greatly appreciated!

kdawg
  • 2,019
  • 21
  • 31
  • You should use NH's caching capabilities, NOT serialize persistent entities. – Diego Mijelshon Apr 18 '13 at 13:18
  • I'm doing this in addition to NH's caching capabilities. We want to store queries and full object graphs -- both of which would result in a 2nd level cache hit per entity using NH's default 2nd level behavior. – kdawg Apr 18 '13 at 15:03
  • The default behavior is not to cache anything. But both things can be cached easily. – Diego Mijelshon Apr 18 '13 at 15:50
  • "Both things can be cached easily" -- Caching a query results in a list of IDs of the objects in the query. Each ID in the collection will result in a round-trip fetch to 2nd level caching. Caching a full object graph in NH would result in a 2nd level cache entry (and fetch) for each entity in the graph. Both provide N+1-like behavior when reading from the 2nd level cache. I'm trying to prevent that. – kdawg Apr 18 '13 at 16:02
  • Your assumption is incorrect. If you cache BOTH the queries AND the entities referenced by the ids, there will be no roundtrips. – Diego Mijelshon Apr 18 '13 at 17:45
  • I disagree and it's not what I've seen in practice. Caching the query results in an entry in the 2nd level cache of a collection of ODs, i.e.: {1, 2, 3, 4}. Those entities are NOT resolved at the 2nd level cache, but are instead returned to the server, where each of those IDs are then resolved (either from the session cache (1st level), from 2nd level cache, or from the database). I've got the NHProf results to prove it. I'd love to be proven wrong, though. – kdawg Apr 18 '13 at 18:50
  • What do you mean by "returned to the server [...] then resolved [...] from 2nd level cache"? Returned to what server? Anyway, here's a blog post about this. Just read it: http://ayende.com/blog/4046/nhibernate-beware-of-inadvisably-applied-caching-strategies – Diego Mijelshon Apr 18 '13 at 22:18
  • query cache and entity caching both enabled: http://tinypic.com/view.php?pic=30vkdpu&s=6 -- sure looks like a N+1 Select issue to me, just from the cache, not the database. – kdawg Apr 19 '13 at 01:30
  • You are misunderstanding what N+1 is. In short: from DB bad, from cache good. What you're seeing in that screenshot is exactly what you want to see. – Diego Mijelshon Apr 19 '13 at 10:04
  • I know what N+1 is. And that's the behavior here, regardless of the nature of the data store (DB vs cache). Yes, it *is* the desired behavior (again DB vs cache), but when a cached query returns 500 results, I don't want to be slamming the 2nd level cache 501 times. Under load, it adds up quickly and that's a chatty network i/o. By caching the entire list (or object graph, etc) upfront manually, I can reduce cached hits for a given page request from several hundred plus to just 4 or 5. – kdawg Apr 19 '13 at 14:10
  • Chatty network IO? That would only be the case if you are using a separate server for your cache (something you did not mention). Otherwise, it's just reading from a different memory address. Also, if you are loading hundreds of results for a single page view, something else might be off with the design. – Diego Mijelshon Apr 19 '13 at 16:39
  • Our memcache servers are living on different servers, yes. – kdawg Apr 19 '13 at 19:17
  • "proxy vs real instances" see https://stackoverflow.com/a/59623373/2534462 – Fried Jan 08 '20 at 05:46

1 Answers1

9

The reason why this would generally happen is because a Foo is loaded elsewhere in the session prior to this point and you are getting the proxy created on that previous query.

I have had some success with the following

var fooProxy = foo as INHibernateProxy;
if(fooProxy != null) {
   var context= Session.GetSessionImplementation().PersistenceContext;
   foo = context.Unproxy(fooProxy);
}

Though, In most cases the process of serialization seems to convert the Proxy to the correct type.

sanbornc
  • 766
  • 5
  • 12
  • "The reason why this would generally happen is because a Foo is loaded elsewhere in the session prior to this point and you are getting the proxy created on that previous query." Very useful piece of information, thanks. – ngm Sep 05 '13 at 16:09
  • 3
    Minor point, but you could replace the null check here with a call to `foo.IsProxy()` if you add `using NHibernate.Proxy`. – ngm Feb 20 '14 at 10:21