views:

1320

answers:

3

I have found when using NHibernate and creating a one to many relationship on an object that when the many grows very large it can slow down dramatically. Now I do have methods in my repository for collecting a paged IList of that type, however I would prefer to have these methods on the model as well because that is often where other developers will look first to gather the list of child objects.

e.g.

RecipientList.Recipients will return every recipient in the list.

I would like to implement a way to add paging on all of my oen to many relationships in my models using preferably an interface but really anything that won't force a typed relationship onto the model. For example it would be nice to have the following interface:

public interface IPagedList<T> : IList<T>
{
    int Count { get; }
    IList<T> GetPagedList(int pageNo, int pageSize);
    IList<T> GetAll();
}

Then being able to use it in code...

IList<Recipient> recipients = RecipientList.Recipients.GetPagedList(1, 400);

I have been trying to think of ways to do this without giving the model any awareness of the paging but I'm hitting my head against a brick wall at the moment.

Is there anyway I can implement the interface in a similar way that NHibernate does for IList and lazyloading currently? I don't have enough knowledge of NHibernate to know.

Is implementing this even a good idea? Your thoughts would be appreciated as being the only .NET developer in house I have no-one to bounce ideas off.

UPDATE

The post below has pointed me to the custom-collection attribute of NHibernate, which would work nicely. However I am unsure what the best way is to go around this, I have tried to inherit from PersistentGenericBag so that it has the same basic functionality of IList without much work, however I am unsure how to gather a list of objects based on the ISessionImplementor. I need to know how to either:

  • Get some sort of ICriteria detail for the current IList that I am to be populating
  • Get the mapping details for the particular property associated with the IList so I can create my own ICriteria.

However I am unsure if I can do either of the above?

Thanks

A: 

If you are going to do something like that, I can not think of a way, you would be able to "write" to the paged collection for NH to persist. The paged collection will be read only.

If that is ok, then you could use an approach like this: http://www.acceptedeclectic.com/2007/12/generic-custom-nhibernate-collections.html

He's wrapping the PersistentGenericBag and is adding some ekstra methods, just like you describe. GetPagedList() could then be implemented with a criteria, that return an ReadOnlyCollection, as could Count - returning a long of course. The GetAll() method won't be neccessary, it will just be returning the collection it self, as far as I can see.

As for if it's a good idea, I do think it is, if you have a lot of collections, where this is an actual problem. If it's just one or two collctions, I would go with just having a method on the entity it self, that returned the collection in pages.

asgerhallas
Thanks Asgerhallas, the collection-type attribute is definately promising. I've created an interface similar to the above and currently creating a class to use within NHibernate using the collection-type. However I am unsure how I can get details of the parent query? Ideally I need to some how get the query that is run to initialize the query and page it; one so I know the parentID and two so that any other attributes for the child (like IsDeleted for soft deletes) are already created. Any ideas?
John_
+1  A: 

Ok I'm going to post this as an answer because it is doing mostly what I wanted. However I would like some feedback and also possibly the answer to my one caveat of the solution so far:

I've created an interface called IPagedList.

public interface IPagedList<T> : IList<T>, ICollection
{

    IList<T> GetPagedList(int pageNo, int pageSize);

}

Then created a base class which it inherits from IPagedList:

public class PagedList<T> : IPagedList<T>
{

    private List<T> _collection = new List<T>();

    public IList<T> GetPagedList(int pageNo, int pageSize)
    {
        return _collection.Take(pageSize).Skip((pageNo - 1) * pageSize).ToList();
    }

    public int IndexOf(T item)
    {
        return _collection.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        _collection.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        _collection.RemoveAt(index);
    }

    public T this[int index]
    {
        get
        {
            return _collection[index];
        }
        set
        {
            _collection[index] = value;
        }
    }

    public void Add(T item)
    {
        _collection.Add(item);
    }

    public void Clear()
    {
        _collection.Clear();
    }

    public bool Contains(T item)
    {
        return _collection.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _collection.CopyTo(array, arrayIndex);
    }

    int Count
    {
        get
        {
            return _collection.Count;
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(T item)
    {
        return _collection.Remove(item);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _collection.GetEnumerator();
    }

    int ICollection<T>.Count
    {
        get { return _collection.Count; }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _collection.GetEnumerator();
    }

    public void CopyTo(Array array, int index)
    {
        T[] arr = new T[array.Length];
        for (int i = 0; i < array.Length ; i++)
        {
            arr[i] = (T)array.GetValue(i);
        }

        _collection.CopyTo(arr, index);
    }

    int ICollection.Count
    {
        get { return _collection.Count; }
    }

    // The IsSynchronized Boolean property returns True if the 
    // collection is designed to be thread safe; otherwise, it returns False.
    public bool IsSynchronized
    {
        get 
        {
            return false;
        }
    }

    public object SyncRoot
    {
        get 
        {
            return this;
        }
    }
}

I then create an IUserCollectionType for NHibernate to use as the custom collection type and NHPagedList which inherits from PersistentGenericBag, IPagedList as the actual collection itself. I created two seperate classes for them because it seemed like the use of IUserCollectionType had no impact on the actual collection to be used at all, so I kept the two pieces of logic seperate. Code below for both of the above:

public class PagedListFactory<T> : IUserCollectionType
{

    public PagedListFactory()
    { }

    #region IUserCollectionType Members

    public bool Contains(object collection, object entity)
    {
        return ((IList<T>)collection).Contains((T)entity);
    }

    public IEnumerable GetElements(object collection)
    {
        return (IEnumerable)collection;
    }

    public object IndexOf(object collection, object entity)
    {
        return ((IList<T>)collection).IndexOf((T)entity);
    }

    public object Instantiate(int anticipatedSize)
    {
        return new PagedList<T>();
    }

    public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
    {
        return new NHPagedList<T>(session);
    }

    public object ReplaceElements(object original, object target, ICollectionPersister persister, 
            object owner, IDictionary copyCache, ISessionImplementor session)
    {
        IList<T> result = (IList<T>)target;

        result.Clear();
        foreach (object item in ((IEnumerable)original))
        {
            result.Add((T)item);
        }

        return result;
    }

    public IPersistentCollection Wrap(ISessionImplementor session, object collection)
    {
        return new NHPagedList<T>(session, (IList<T>)collection);
    }

    #endregion
}

NHPagedList next:

public class NHPagedList<T> : PersistentGenericBag<T>, IPagedList<T>
{

    public NHPagedList(ISessionImplementor session) : base(session)
    {
        _sessionImplementor = session;
    }

    public NHPagedList(ISessionImplementor session, IList<T> collection)
        : base(session, collection)
    {
        _sessionImplementor = session;
    }

    private ICollectionPersister _collectionPersister = null;
    public NHPagedList<T> CollectionPersister(ICollectionPersister collectionPersister)
    {
        _collectionPersister = collectionPersister;
        return this;
    }

    protected ISessionImplementor _sessionImplementor = null;

    public virtual IList<T> GetPagedList(int pageNo, int pageSize)
    {
        if (!this.WasInitialized)
        {
            IQuery pagedList = _sessionImplementor
                .GetSession()
                .CreateFilter(this, "")
                .SetMaxResults(pageSize)
                .SetFirstResult((pageNo - 1) * pageSize);

            return pagedList.List<T>();
        }

        return this
                .Skip((pageNo - 1) * pageSize)
                .Take(pageSize)
                .ToList<T>();
    }

    public new int Count
    {
        get
        {
            if (!this.WasInitialized)
            {
                return Convert.ToInt32(_sessionImplementor.GetSession().CreateFilter(this, "select count(*)").List()[0].ToString());
            }

            return base.Count;
        }
    }

}

You will notice that it will check to see if the collection has been initialized or not so that we know when to check the database for a paged list or when to just use the current in memory objects.

Now you're ready to go, simply change your current IList references on your models to be IPagedList and then map NHibernate to the new custom collection, using fluent NHibernate is the below, and you are ready to go.

.CollectionType<PagedListFactory<Recipient>>()

This is the first itteration of this code so it will need some refactoring and modifications to get it perfect.

My only problem at the moment is that it won't get the paged items in the order that the mapping file suggests for the parent to child relationship. I have added an order-by attribute to the map and it just won't pay attention to it. Where as any other where clauses are in each query no problem. Does anyone have any idea why this might be happening and if there is anyway around it? I will be disappointed if I can't work away around this.

John_
I've accepted my own answer because it is the closest to what I need, however I am working on the problems it has and will be updating it once those issues have been resolved.
John_
+1  A: 

You should look into one of the LINQ providers for NHibernate. What your looking for is a way to delay-load the results for your query. The greatest power of LINQ is that it does exactly that...delay-loads the results of your queries. When you actually build a query, in reality its creating an expression tree that represents what you want to do, so that it can actually be done at a later date. By using a LINQ provider for NHibernate, you would then be able to do something like the following:

public abstract class Repository<T> where T: class
{
    public abstract T GetByID(int id);
    public abstract IQueryable<T> GetAll();
    public abstract T Insert(T entity);
    public abstract void Update(T entity);
    public abstract void Delete(T entity);
}

public class RecipientRepository: Repository<Recipient>;
{
    // ...

    public override IQueryable<Recipient> GetAll()
    {
        using (ISession session = /* get session */)
        {
            // Gets a query that will return all Recipient entities if iterated
            IQueryable<Recipient> query = session.Linq<Recipient>();
            return query;
        }
    }

    // ...
}

public class RecipientList
{
    public IQueryable<Recipient> Recipients
    {
        RecipientRepository repository = new RecipientRepository();
        return repository.GetAll(); // Returns a query, does not evaluate, so does not hit database
    }
}

// Consuming RecipientList in some higher level service, you can now do:    
public class RecipientService
{
    public IList<Recipient> GetPagedList(int page, int size)
    {
        RecipientList list = // get instance of RecipientList
        IQueryable<Recipient> query = list.Recipients.Skip(page*size).Take(size); // Get your page
        IList<Recipient> listOfRecipients = query.ToList(); // <-- Evaluation happens here!
        reutrn listOfRecipients;
    }
}

With the above code (its not a great example, but it does demonstrate the general idea), you build up an expression representing what you want to do. Evaluation of that expression happens only once...and when evaluation happens, your database is queried with a specific query that will only return the specific subset of rows you actually requested. No need to load up all the records, then filter them down later on to the single page you requested...no waste. If an exception occurs before you evaluate, for whatever reason, you never even hit the database, increasing efficiency even more.

This power can go much farther than querying a single page of results. The extension methods .Skip() and .Take() are available on all IQueryable<T> and IEnumerable<T> objects, along with a whole bunch of others. In addition, you have .Where(), .Except(), .Join(), and many, many more. This gives you the power to, say, .GetAll(), then filter the possible results of that query with one or more calls to .Where(), finishing with a .Skip(...).Take(...), ending in a single evaluation at your .ToList() (or .ToArray()) call.

This would require that you change your domain somewhat, and start passing IQueryable<T> or IEnumerable<T> around in place of IList<T>, and only convert to an IList<T> at your higher-level, 'publicly facing' services.

jrista
Thanks jrista, however I was trying to avoid having to redefine what I already have in my mapping files. For example I may have a parent class with a collection of child classes, the child classes may use soft deletes which means I need the where clause of the collection to filter out those items. This means that when I created my LINQ I would have to create a where clause for each collection where ideally some sort of ICriteria or IQuery could be created by NHibernate based on the mappings.
John_
It would be extremely easy to filter out the soft-deleted items. Assuming your collection tracked those in a list, you can just use the .Except() IQueryable<T> extension. Assuming: private RecipientList.m_deletedItems, then: return repository.GetAll().Except(m_deletedItems); Since .Except() is an extension of IQueryable<T>, calling it just updates the expression, and is evaluated as part of your later iteration of that query (i.e. .ToList()). IQueryable<T> should make it super easy to support soft delete, without any loss in performance.
jrista
Jrista, I know you can amend the IQueryable as you wish however I think you are missing my point. I want to create something that can be applied to any collection as a custom collection type and it will automatically infer the details of that collection based on the mapping. So if, in my mapping files, I had a where clause for soft deletes or anything other clause it will automatically pick this up and do the paging on top of that query. What you are describing sounds like a manual process for each collection which is not what I want / need.
John_
It doesn't necessarily need to be a manual process for each collection. If you have a base collection type, you should be able to implement the IQueryable stuff there...and if its all generic, then you have a single implementation in a single place. Anyway, whatever you like...it was just a suggestion.
jrista
I appreciate the suggestion jrista, I guess I just don't understand how it works because in my head it won't do what I want it to do. Thanks again.
John_
For example above you have the code for the method GetPagedList, in that you say to get the recipient list. However there is no way to automatically infer which recipient list is being requested for.
John_
jrista