Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing IPagedList<T> on my models using NHibernate

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

like image 975
John_ Avatar asked May 18 '09 09:05

John_


2 Answers

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.

like image 189
John_ Avatar answered Oct 28 '22 01:10

John_


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.

like image 23
jrista Avatar answered Oct 28 '22 01:10

jrista