Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to activate the second level cache on a lazy loaded property with own user type?

Preface:
In my application, I store raw WAV data in the database as byte[]. In my domain model there is a class PcmAudioStream that represents that raw WAV data. I created an implementation of NHibernate's IUserType to convert between my class and byte[].
There are several classes that use the PcmAudioStream class, all of which are mapped to database tables. To avoid always loading all WAV data when retrieving a row from such a table, I created an implementation of Fluent NHibernate's IUserTypeConvention that specifies that those properties should always be lazy loaded.
All of this works like a charm.

Question:
Because the content of these PcmAudioStreams rarely ever changes, I want to put retrieved instances in the second level cache. Now, I know how to activate the second level cache for a complete class, but how do I achieve this only for a lazy loaded property?


The relevant part of my domain model looks like this:

public class User : Entity
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual PcmAudioStream FullNameRecording { get; set; }
    // ...
}

The mapping is simple (note: that is not my mapping, I am using a convention, but it is equivalent):

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Id(x => x.Id);
        Map(x => x.FirstName);
        Map(x => x.LastName);
        Map(x => x.FullNameRecording).CustomType<PcmAudioStreamAsByteArray>();
    }
}
like image 588
Daniel Hilgarth Avatar asked Nov 12 '11 20:11

Daniel Hilgarth


2 Answers

I'm not sure about caching only a single property, but my guess would be that it's not the way the NH caching infrastructure is built. IMHO you can put entire class instances or the results from queries into the 2nd level cache.

But I will try to sketch out a solution.

Prior to NH 3 and the support for lazy properties, if you didn't want to load the entire entity from the DB (and in your case this makes perfectly sense!) you had to keep such "expensive" data in a referenced, lazy loaded table. At least that's how I solved it.

It might seem like a step back, but using this approach, I'm pretty sure you will be able to cache this data.

On a seperate note, there seems to be a problem with caching and QueryOver in NH3+: https://nhibernate.jira.com/browse/NH-2740

like image 45
kay.herzam Avatar answered Nov 14 '22 02:11

kay.herzam


You could use a private static cache to accomplish this. It's a little more work to set up but doesn't require an additional class or public changes to your domain model. A big drawback is that entries are not removed from the cache, but you could use a custom collection or a "global" cache that limits the number of entries.

public class Entity
{
    public virtual int Id { get; protected set; }
}

public class PcmAudioStream
{}

public class User : Entity
{
    private static readonly IDictionary<int, PcmAudioStream> _fullNameRecordingCache;

    private PcmAudioStream _fullNameRecording;

    static User()
    {
        _fullNameRecordingCache = new Dictionary<int, PcmAudioStream>();
    }

    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual PcmAudioStream FullNameRecording
    {
        get
        {
            if (_fullNameRecordingCache.ContainsKey(Id))
            {
                return _fullNameRecordingCache[Id];
            }
            // May need to watch for proxies here
            _fullNameRecordingCache.Add(Id, _fullNameRecording);
            return _fullNameRecording;
        }
        set
        {
            if (_fullNameRecordingCache.ContainsKey(Id))
            {
                _fullNameRecordingCache[Id] = value;
            }
            _fullNameRecording = value;
        }
    }
    // ...
}

Mapping:

public class UserMap : ClassMap<User>
{
    public UserMap()
    {
        Id(x => x.Id);
        Map(x => x.FirstName);
        Map(x => x.LastName);
        Map(x => x.FullNameRecording).CustomType<PcmAudioStreamAsByteArray>()
            .Access.CamelCaseField(Prefix.Underscore);
    }
}

Edited in response to comments:

I don't see that it's possible to achieve this in a user type because the IDataReader is already open in NullSafeGet. I think you could do it in a listener implementing IPreLoadEventListener but that doesn't allow you to invalidate the cache. I don't think either option is viable.

After thinking about it some more I still think my original solution (or a variant) is the best option. I understand (and share) your desire for a clean domain model but sometimes compromises are necessary and my solution does not change the public members of the model or require any additional references. Another justification is that the object is the first to know that the recording has changed and needs to be replaced in or added to the cache.

like image 115
Jamie Ide Avatar answered Nov 14 '22 03:11

Jamie Ide