Caching and database operations issue

Topics: Troubleshooting, Writing modules
Developer
Mar 9, 2011 at 2:59 AM
Edited Mar 9, 2011 at 3:09 AM

Hi!

Somehow I cannot get this to work:/ I'm trying to cache data retrieved from the database, but every time I get the "The connection object can not be enlisted in transaction scope." ADOException from NHibernate. When I turn off caching I keep getting "Transaction nested incorrectly" ADOException when doing db stuff.

Maybe this is related to the moment in Orchard lifetime when I try do fetch data from DB (via my service)? I'm doing that inside my IShapeTableProvider's Discover method (my service is injected via constructor). I know the shape table gets cached so possibly caching is responsible for this too (even if I turned off the caching in my service)?

For clarification - I'm not caching the records returned - only the further mapped POCOs. The object type stored is not ContentPartRecord, just an ordinary one, with appropriate migration specified. Everything works perfectly in other scenarios, fails only when there is caching involved.

- Piotr

Coordinator
Mar 9, 2011 at 4:21 AM

That's because you're trying to cache something that you shouldn't, such as something that has a reference to something that is bound to the transaction. You need to make sure the object you stick into cache is a very very plain object.

Developer
Mar 9, 2011 at 1:45 PM

Hmmm I thought about that (read about it in other post) and I'm not caching anything besides simple POCOs. Code for my service's Get() method looks like this:

public IEnumerable Get() {
            return _cache.Get("Some.Cache.Id", ctx =>
            {
                MonitorSignal(ctx);
                return _repository.Table.Where(r => r.Parent == null).Select(MapToModel).ToList();
            });
}

The MapToModel method simply maps the Record object to POCO object.

internal MyPOCO MapToModel(MyRecord source)
{
    if (source != null)
    {
                return new MyPOCO
                {
                    Id = source.Id,
                    Parent = MapToModel(source.Parent),
                    Name = source.Name,
                    Children = source.Children.Select(MapToModel).ToList()
                };
    }
    return null;
}

This all works correctly. The only scenario it doesn't work is when the call to Get() happens inside IShapeProvider's Discover() method (or any other place in code, which executes inside cache context). The Discover() method is called in the cache context. I cannot figure out how to make it work?

- Piotr


Developer
Mar 9, 2011 at 1:55 PM

And it's not only the issue with my code - any database related actions done inside IShapeTableProvider's Discover method end up with ADOException:/

Developer
Mar 9, 2011 at 8:16 PM

I guess I've found what's causing the problem.

In DefaultShapeManager's GetShapeTableMethod, the cached value is:

return new ShapeTable {
                    Descriptors = descriptors.ToDictionary(sd => sd.ShapeType, StringComparer.OrdinalIgnoreCase),
                    Bindings = descriptors.SelectMany(sd => sd.Bindings).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase),
                };

I guess the SelectMany should be replaced to SelectMany(...).ToList() so to fetch and store the real value and not the SelectMany iterator. Am I right?

For now, I found a (quick&dirty) workaround. I'll just override the DefaultShapeTableManager for making my DB calls outside the cache scope in GetShapeTable method.

- Piotr

Coordinator
Mar 9, 2011 at 8:20 PM

Ah, yes, but you are creating a closure here with that repository reference in the Lambda. The Lambda is going to be remembered by the caching engine to be called next time, and that repository reference is carrying the database session with it.

You can avoid that by making the repository reference lazy: just declare it Lazy<IRepository<Whatever>> and it should just work.

Developer
Mar 10, 2011 at 1:28 AM

Ahhh, so the lambda gets cached too - now I have the whole picture of how it works. Thanks so much! A load of problems and such simple solution - beautiful:)

- Piotr

Developer
Mar 10, 2011 at 2:40 AM

Unfortunately that didn't work. When the first time the cache gets hit it's ok, but the repository is still remembered because the lazy value got created at this moment. So the second cache hit doesn't force the Lazy<IRepository<Whatever>> object to reinstantiate as the value is already there, and so the situation stays the same - "Transaction nested incorrectly", bump!:/

My dependency chain looks as follows (starting from the injection inside IShapeTableProvider implementation):

IMyManager --> IEnumerable<IMyProvider> --> IMyService (in one of the previous providers) --> IRepository<MyRecord>

I changed everything of those to Lazy<...>, as you suggested, so that every injected object is lazily instantiated. There is only one thing I cannot change - at the top, the IEnumerable<IShapeTableProvider> in DefaultShapeTableManager looks like this

        public DefaultShapeTableManager(
            IEnumerable<Meta<IShapeTableProvider>> bindingStrategies (...)

and this collection is used inside the cached lambda. One of those providers is my provider with the chained dependencies as I wrote above. Any clues on what am I doing wrong and how to handle that?

- Piotr

Developer
Mar 10, 2011 at 3:56 AM
Edited Mar 10, 2011 at 3:59 AM

Ahhh found the real problem. The IShapeTableManager which the DefaultShapeTableManager implements is a ISingletonDependency:/ So the list of providers (and the underlying repository) is not resolved at every request and it doesn't matter if I make the dependencies Lazy or not. They never get refreshed. I guess it should be definitely changed (the table gets cached after all so the performance impact would be negligible, I guess).

I realized that when trying to change the IDependency to IUnitOfWorkDependency to force instance-per-request behavior.

Any clues?

- Piotr

Coordinator
Mar 10, 2011 at 4:00 AM

Yeah, if you are doing that from a shape table provider, you don't need caching do you? Or did I misunderstand?