views:

676

answers:

3

I am looking for a way to retrieve the "surrounding" rows in a NHibernate query given a primary key and a sort order?

E.g. I have a table with log entries and I want to display the entry with primary key 4242 and the previous 5 entries as well as the following 5 entries ordered by date (there is no direct relation between date and primary key). Such a query should return 11 rows in total (as long as we are not close to either end).

The log entry table can be huge and retrieving all to figure it out is not possible.

Is there such a concept as row number that can be used from within NHibernate? The underlying database is either going to be SQlite or Microsoft SQL Server.

Edited Added sample

Imagine data such as the following:

Id   Time
4237 10:00
4238 10:00
1236 10:01
1237 10:01
1238 10:02
4239 10:03
4240 10:04
4241 10:04
4242 10:04   <-- requested "center" row
4243 10:04
4244 10:05
4245 10:06
4246 10:07
4247 10:08

When requesting the entry with primary key 4242 we should get the rows 1237, 1238 and 4239 to 4247. The order is by Time, Id.

Is it possible to retrieve the entries in a single query (which obviously can include subqueries)? Time is a non-unique column so several entries have the same value and in this example is it not possible to change the resolution in a way that makes it unique!

+1  A: 

"there is no direct relation between date and primary key" means, that the primary keys are not in a sequential order?

Then I would do it like this:

Item middleItem = Session.Get(id);

IList<Item> previousFiveItems = Session.CreateCriteria((typeof(Item))
  .Add(Expression.Le("Time", middleItem.Time))
  .AddOrder(Order.Desc("Time"))
  .SetMaxResults(5);

IList<Item> nextFiveItems = Session.CreateCriteria((typeof(Item))
  .Add(Expression.Gt("Time", middleItem.Time))
  .AddOrder(Order.Asc("Time"))
  .SetMaxResults(5);

There is the risk of having several items with the same time.


Edit

This should work now.

Item middleItem = Session.Get(id);

IList<Item> previousFiveItems = Session.CreateCriteria((typeof(Item))
  .Add(Expression.Le("Time", middleItem.Time)) // less or equal
  .Add(Expression.Not(Expression.IdEq(middleItem.id))) // but not the middle
  .AddOrder(Order.Desc("Time"))
  .SetMaxResults(5);

IList<Item> nextFiveItems = Session.CreateCriteria((typeof(Item))
  .Add(Expression.Gt("Time", middleItem.Time))  // greater 
  .AddOrder(Order.Asc("Time"))
  .SetMaxResults(5);
Stefan Steinegger
I guess that this will work as long as the ordering is on a column that holds unique values. I might have a need to sort on different columns so keeping a sequence column is not going to work.I was looking for a rownum solution for situations where I needed to sort on non-unique columns so more ideas are more than welcome :-)
HakonB
Ok, refined it. See the new section.
Stefan Steinegger
A: 

This should be relatively easy with NHibernate's Criteria API:

List<LogEntry> logEntries = session.CreateCriteria(typeof(LogEntry))
.Add(Expression.InG<int>(Projections.Property("Id"), listOfIds))
.AddOrder(Order.Desc("EntryDate"))
.List<LogEntry>();

Here your listOfIds is just a strongly typed list of integers representing the ids of the entries you want to retrieve (integers 4242-5 through 4242+5 ).

Of course you could also add Expressions that let you retrieve Ids greater than 4242-5 and smaller than 4242+5.

martijn_himself
How am I ever going to get reputation? :) When I posted an answer within 5 mins someone else had posted one as well.
martijn_himself
It's as it is with money. If you have it, you get it. People vote more often for answers from members with high reputation. Life is not fair. But don't care, providing good answers is always appreciated and should be the main intention.
Stefan Steinegger
As Stefan noted above there is no direct relationship between date and primary key so just selecting by primary key is not going to work. I might have a solution where I also need to filter by category which invalidates the primary key solution.
HakonB
Ah yes.. sorry.. I should maybe have read the question before I posted an answer. Give Stefan the money! :)
martijn_himself
A: 

Stefan's solution definitely works but better way exists using a single select and nested Subqueries:

ICriteria crit = NHibernateSession.CreateCriteria(typeof(Item));

        DetachedCriteria dcMiddleTime =
            DetachedCriteria.For(typeof(Item)).SetProjection(Property.ForName("Time"))
            .Add(Restrictions.Eq("Id", id));

        DetachedCriteria dcAfterTime =
            DetachedCriteria.For(typeof(Item)).SetMaxResults(5).SetProjection(Property.ForName("Id"))
            .Add(Subqueries.PropertyGt("Time", dcMiddleTime));
        DetachedCriteria dcBeforeTime =
            DetachedCriteria.For(typeof(Item)).SetMaxResults(5).SetProjection(Property.ForName("Id"))
                .Add(Subqueries.PropertyLt("Time", dcMiddleTime));

        crit.AddOrder(Order.Asc("Time"));
        crit.Add(Restrictions.Eq("Id", id) || Subqueries.PropertyIn("Id", dcAfterTime) ||
                 Subqueries.PropertyIn("Id", dcBeforeTime));

        return crit.List<Item>();

This is NHibernate 2.0 syntax but the same holds true for earlier versions where instead of Restrictions you use Expression.

I have tested this on a test application and it works as advertised

Jaguar
It works great as long as time is unique but it fails when the "middle" time is shared between multiple rows because of the PropertyGt/Lt restrictions. However, just changing to PropertyGe/Le does not work either. Try using the time values from my question in your sample and see what I mean. Btw, your code certainly taught me something new about subqueries :-)
HakonB
bleh, temporary blindness made me miss the non-unique requirement. since there is no kind of sequential ordering in the data set i cannot see any way to provide the answer required
Jaguar