views:

52

answers:

1

Working on the data access / model layer in this little MVC2 project and trying to think things out to future projects.

I have a database with some basic tables and I have classes in the model layer that represent them. I obviously need something to connect the two. The easiest is to provide some sort of 'provider' that can run operations on the database and return objects.

But this is for a website that would potentially be used "a lot" (I know, very general) so I want to cache results from the data layer and keep the cache updated as new data is generated.

This question deals with how best to approach this problem of dual DALS. One that returns cached data when possible and goes to the data layer when there is a cache miss. But more importantly, how to integrate the core provider (thing that goes into database) with the caching layer so that it too can rely on cached objects rather than creating new ones.

Right now I have the following interfaces:

IDataProvider is used to reach the database. It doesn't concern itself with the meaning of the objects it produces, but simply the way to produce them.

interface IDataProvider{
    // Select, Update, Create, et cetera access
    IEnumerable<Entry> GetEntries();

    Entry GetEntryById(int id);
}

IDataManager is a layer that sits on top of the IDataProvider layer and manages the cache

interface IDataManager : IDataProvider{
    void ClearCache();
}

Note that in practice the IDataManager implementation will have useful helper functions to add objects to their related cache stores. (In the future I may define other functions on the interface)

I guess what I am looking for is the best way to approach a loop back from the IDataProvider implementations so that they can access the cache. Or a different approach entirely may be in order? I am not very interested in 3rd party products at the moment as I am interested in the design of these things much more than this specific implementation.

Edit: I realize the title may be a bit misleading. I apologize for that... not sure what to call this question.

A: 

Personally, I currently use NHibernate which handles caching for you. In the past, I have done something similar to what you are trying to do, however I never had need to separate the caching layer from the dao layer. Is there some reason that you need to separate these? I suppose it would make sense if you have significantly different caching needs dependent on the object type. Anyway, assuming that it is necessary, here is how I would go about doing it based on past experience. Firstly, lets assume we have the following classes: PersistentObject, which is a base object that has at least an integer property called "ID"

public class PersistentObject
{
   public int ID { get; set; }
}

Now say you have a sample type that you would like to persist:

public class SampleObject : PersistentObject 
{ 
    public string SomeValue { get; set; }
}

In place of "DataProvider", I have a simple repository interface:

public interface IRepository<T> where T : PersistentObject
{
    T Get(int id);        
    void Save(T e);
    void Delete(T e);
}

How about, instead of your IDataManager interface, you have an abstract class that you can implement that is a repository that handles caching (I have my own "CacheManager" class that I'm using here, it could be implemented any number of ways):

public abstract class CacheRepository<T> : IRepository<T> where T : PersistentObject
{
    private const string CacheKeyPrefix = "RepoCache-";
    private string GetCacheKey(int id)
    {
        return CacheKeyPrefix + typeof(T).FullName + "-" + id.ToString();
    }

    public T Get(int id)
    {
        string cacheKey = GetCacheKey(id);
        T obj = CacheManager.GetItemFromCache<T>(cacheKey);
        if (obj == null)
        {
            obj = this.GetData(id);
            if (obj != null)
                CacheManager.AddItemToCache(obj, cacheKey);
        }
        return obj;
    }

    public void Save(T obj)
    {
        string cacheKey = GetCacheKey(obj.ID);
        this.SaveData(obj);
        CacheManager.AddItemToCache(obj, cacheKey);
    }

    public void Delete(T obj)
    {
        string cacheKey = GetCacheKey(obj.ID);
        this.DeleteData(obj);
        CacheManager.RemoveItemFromCache(cacheKey);
    }

    protected abstract T GetData(int id);
    protected abstract void SaveData(T obj);
    protected abstract void DeleteData(T obj);
}

You'll notice that the only members that are publicly exposed are the methods that implement IRepository. There are 3 abstract methods that must be implemented for data access by the individual repository. So we can do that here for our SampleObject:

public class SampleObjectRepository : CacheRepository<SampleObject>
{
    protected override SampleObject  GetData(int id)
    {
        //do some loading
        return new SampleObject();
    }

    protected override void  SaveData(SampleObject obj)
    {
        //do some saving
    }

    protected override void  DeleteData(SampleObject obj)
    {
        //do some deleting
    }
}

Lastly, this is how you would go about utilizing this code:

public class UsageSample
{
    public UsageSample()
    {
        //save a new object
        SampleObjectRepository repo = new SampleObjectRepository();
        SampleObject sampleObj = new SampleObject();
        repo.Save(sampleObj);

        //load an object by ID
        int id = sampleObj.ID;
        sampleObj = repo.Get(id);

        //delete an object
        repo.Delete(sampleObj);
    }
}

It gets significantly more complex when you start to talk about returning collections and querying, but this is at least a simplistic start. Are you sure you're not interested in 3rd party solutions? :-) NHibernate does a pretty great job of handling all of this for you.

Luke
Thanks. One of the reasons for separating the two into different classes was that of encapsulation. Regardless of where the data is coming from, I still need to cache it. But this can be handled in other ways.After reading this and speaking with others I think a single object for data access and caching makes more sense, because in general it is a single package. The objects directly relate to stuff in the data layer so removing that inherent context seems silly. I can also provide an abstraction for the caching if necessary (moving to a desktop app for example).
Krisc