4

given a query in the form of an ICriteria object, I would like to use NHibernate (by means of a projection?) to find an element's order, in a manner equivalent to using

SELECT ROW_NUMBER() OVER (...)

to find a specific item's index in the query. (I need this for a "jump to page" functionality in paging) any suggestions?

NOTE: I don't want to go to a page given it's number yet - I know how to do that - I want to get the item's INDEX so I can divide it by page size and get the page index.

Yonatan Karni
  • 977
  • 2
  • 13
  • 32
  • This is something I've been looking for as well. When you posted this question, I eagerly awaited all the answers... After looking at the sources for NHibernate, I'm fairly sure that there exists no such functionality. – Christoffer Lette Jan 13 '10 at 10:34
  • Also, the somewhat awkward syntax for the row_number() function probably makes it quite challenging to implement said functionality. – Christoffer Lette Jan 13 '10 at 10:38
  • Would you consider a solution that is not based on ICriteria and is SQL Server specific? (The solution does not require input of magic strings, is somewhat limitied in functionality but takes advantage of your domain entities...) – Christoffer Lette Jan 15 '10 at 16:51
  • at the moment I'm considering creating a new Projection, so I can use it in my numerous ICriteria objects, and so that it integrates well with my framework. however I'm always happy to learn of other solutions. – Yonatan Karni Jan 17 '10 at 07:27

3 Answers3

6

After looking at the sources for NHibernate, I'm fairly sure that there exists no such functionality.

I wouldn't mind, however, for someone to prove me wrong.

In my specific setting, I did solve this problem by writing a method that takes a couple of lambdas (representing the key column, and an optional column to filter by - all properties of a specific domain entity). This method then builds the sql and calls session.CreateSQLQuery(...).UniqueResult(); I'm not claiming that this is a general purpose solution.

To avoid the use of magic strings, I borrowed a copy of PropertyHelper<T> from this answer.

Here's the code:

public abstract class RepositoryBase<T> where T : DomainEntityBase
{
    public long GetIndexOf<TUnique, TWhere>(T entity, Expression<Func<T, TUnique>> uniqueSelector, Expression<Func<T, TWhere>> whereSelector, TWhere whereValue) where TWhere : DomainEntityBase
    {
        if (entity == null || entity.Id == Guid.Empty)
        {
            return -1;
        }

        var entityType = typeof(T).Name;

        var keyField = PropertyHelper<T>.GetProperty(uniqueSelector).Name;
        var keyValue = uniqueSelector.Compile()(entity);

        var innerWhere = string.Empty;

        if (whereSelector != null)
        {
            // Builds a column name that adheres to our naming conventions!
            var filterField = PropertyHelper<T>.GetProperty(whereSelector).Name + "Id";

            if (whereValue == null)
            {
                innerWhere = string.Format(" where [{0}] is null", filterField);
            }
            else
            {
                innerWhere = string.Format(" where [{0}] = :filterValue", filterField);
            }
        }

        var innerQuery = string.Format("(select [{0}], row_number() over (order by {0}) as RowNum from [{1}]{2}) X", keyField, entityType, innerWhere);

        var outerQuery = string.Format("select RowNum from {0} where {1} = :keyValue", innerQuery, keyField);

        var query = _session
            .CreateSQLQuery(outerQuery)
            .SetParameter("keyValue", keyValue);

        if (whereValue != null)
        {
            query = query.SetParameter("filterValue", whereValue.Id);
        }

        var sqlRowNumber = query.UniqueResult<long>();

        // The row_number() function is one-based. Our index should be zero-based.
        sqlRowNumber -= 1;

        return sqlRowNumber;
    }

    public long GetIndexOf<TUnique>(T entity, Expression<Func<T, TUnique>> uniqueSelector)
    {
        return GetIndexOf(entity, uniqueSelector, null, (DomainEntityBase)null);
    }

    public long GetIndexOf<TUnique, TWhere>(T entity, Expression<Func<T, TUnique>> uniqueSelector, Expression<Func<T, TWhere>> whereSelector) where TWhere : DomainEntityBase
    {
        return GetIndexOf(entity, uniqueSelector, whereSelector, whereSelector.Compile()(entity));
    }
}

public abstract class DomainEntityBase
{
    public virtual Guid Id { get; protected set; }
}

And you use it like so:

...

public class Book : DomainEntityBase
{
    public virtual string Title { get; set; }
    public virtual Category Category { get; set; }
    ...
}

public class Category : DomainEntityBase { ... }

public class BookRepository : RepositoryBase<Book> { ... }

...

var repository = new BookRepository();
var book = ... a persisted book ...

// Get the index of the book, sorted by title.
var index = repository.GetIndexOf(book, b => b.Title);

// Get the index of the book, sorted by title and filtered by that book's category.
var indexInCategory = repository.GetIndexOf(book, b => b.Title, b => b.Category);

As I said, this works for me. I'll definitely tweak it as I move forward. YMMV.

Now, if the OP has solved this himself, then I would love to see his solution! :-)

Community
  • 1
  • 1
Christoffer Lette
  • 14,346
  • 7
  • 50
  • 58
3

ICriteria has this 2 functions:

 SetFirstResult()

and

 SetMaxResults()

which transform your SQL statement into using ROW_NUMBER (in sql server) or limit in MySql.

So if you want 25 records on the third page you could use:

 .SetFirstResult(2*25) 
 .SetMaxResults(25)
dmonlord
  • 1,370
  • 9
  • 16
  • 1
    thanks, but what I want to do is a bit different - I have an object, and I want to find it's index, so that I can find the object's page. then I can use these suggested methods to fetch that page. – Yonatan Karni Jan 11 '10 at 08:44
0

After trying to find an NHibernate based solution for this myself, I ultimately just added a column to the view I happened to be using:

CREATE VIEW vw_paged AS
SELECT ROW_NUMBER() OVER (ORDER BY Id) AS [Row], p.column1, p.column2
FROM paged_table p

This doesn't really help if you need complex sorting options, but it does work for simple cases.

A Criteria query, of course, would look something like this:

    public static IList<Paged> GetRange(string search, int rows)
    {
        var match = DbSession.Current.CreateCriteria<Job>()
            .Add(Restrictions.Like("Id", search + '%'))
            .AddOrder(Order.Asc("Id"))
            .SetMaxResults(1)
            .UniqueResult<Paged>();

        if (match == null)
            return new List<Paged>();
        if (rows == 1)
            return new List<Paged> {match};

        return DbSession.Current.CreateCriteria<Paged>()
            .Add(Restrictions.Like("Id", search + '%'))
            .Add(Restrictions.Ge("Row", match.Row))
            .AddOrder(Order.Asc("Id"))
            .SetMaxResults(rows)
            .List<Paged>();
    }
Kevin Stricker
  • 17,178
  • 5
  • 45
  • 71