Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ROW_NUMBER() and nhibernate - finding an item's page

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.

like image 455
Yonatan Karni Avatar asked Jan 23 '23 19:01

Yonatan Karni


1 Answers

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! :-)

like image 196
Christoffer Lette Avatar answered Feb 16 '23 08:02

Christoffer Lette