RavenDB: How to create MapReduce index to return a filtered list of child objects - ravendb

Following this question
I have the following document structure:
Game
- Id
- Teams
- Team 1
- list of players
- Team 2
- list of players
- Events
- Event 1
- type: Pass
- teamId
- PlayerId
- Event 2
- type: Goal
- teamId
- PlayerId
How do I build an index that gives me all the events for a player for a given game?
Here is how far I got and RavenDB says it can't understand my query?
public class Games_PlayerEvents : AbstractIndexCreationTask<Game, Games_PlayerEvents.ReduceResult>
{
public class ReduceResult
{
public string PlayerId { get; set; }
public IEnumerable<GameEvent> Events { get; set; }
}
public Games_PlayerEvents()
{
Map = games => from game in games
select new
{
PlayerId = "",
Events = game.Events
};
Reduce = results => from result in results
from #event in result.Events
group #event by #event.PlayerId into playerEvents
select new
{
PlayerId = playerEvents.Key,
Events = playerEvents.Select(g => g)
};
}
}

Marto, in case you really want to model it this way, you don't need to create an index, as you can load the whole game document and do the rest using standard linq-to-objects aggregations.
Anyway, it sounds as if you want to have Game, Player and Event as independant documents as these are your aggregate roots or in other words - they have a meaning on their own.

Related

MR index calculating average with denormalized properties

I have a C# service that responds to clients periodically requesting an array of actions to perform and each action is stored in a RavenDB Action document with these properties where the last 2 properties are denormalized for performance:
Id (String - PK)
ClientId (String - FK)
RequestId (String - Unique Id for the request. We don't store a Request entity)
RequestDateTime (Date & Time - Date & time that request was made)
RequestDuration (Time Span - How long the request took to determine the list of actions)
I want to create an MR index that provides hourly request statistics per client so that I can see statistics for Client #1, 01/02/22 09:00-10:00 etc. I'm struggling to calculate AvgRequestDuration because the group contains duplicate RequestDuration(s) due to the data being denormalized. Obviously min & max are not affected with duplicates.
public class Result
{
public string ClientId { get; set; }
public DateTime PeriodStart { get; set; }
public TimeSpan MinRequestDuration { get; set; }
public TimeSpan MaxRequestDuration { get; set; }
public TimeSpan AvgRequestDuration { get; set; }
}
public ClientStatsByPeriodStartDateTime()
{
Map = action => from ac in actions
let period = TimeSpan.FromHours(1)
select new
{
ClientId = ac.ClientId,
PeriodStart = new DateTime(((ac.RequestDateTime.Ticks + period.Ticks - 1) / period.Ticks) * period.Ticks, DateTimeKind.Utc),
ac.RequestDuration
};
Reduce = results => from result in results
group result by new
{
result.ClientId,
result.PeriodStart
}
into agg
select new
{
ClientId = agg.Key.ClientId,
PeriodStart = agg.Key.PeriodStart,
AvgRequestDuration = agg.Avg(x => x.RequestDuration), // This is wrong
MinRequestDuration = agg.Min(x => x.RequestDuration),
MaxRequestDuration = agg.Max(x => x.RequestDuration)
};
}
Consider using the timeSeries feature to calculate avg, min & max.
Create a time series entry for each request.
The entry value can hold the duration.
You can then query for data at specific times, and get min,max,avg info for the values.
You can even index time series data.
This blog post can also be useful to start.
I've decided to normalize the structure and have a single document named Request that contains an array of the Action entity. The Duration property can then be stored against the Request document.

RavenDB: How to properly query/filter a nested value from a MultiMapIndex?

My application has a requirement that is should be able to filter/search for Pairs by the Number of the related Contact.
A Pair always has a reference to a Contact stored, but the number of the contact is not, and will not, be stored in the reference. So I tried to create a custom index for this, because the Pair and Contact are stored in different collections.
A simplified example of the index looks like this.
public class Pairs_Search : AbstractMultiMapIndexCreationTask<Pairs_Search.Result>
{
public class Result
{
public string Id { get; set; }
public string Workspace { get; set; }
public ContactResult Contact { get; set; }
public bool HasContactDetails { get; set; }
}
public class ContactResult
{
public string Id { get; set; }
public string Name { get; set; }
public int Number { get; set; }
}
public Pairs_Search()
{
AddMap<Pair>(pairs => pairs
.Select(p => new
{
p.Id,
p.Workspace,
Contact = new
{
p.Contact.Id,
p.Contact.Name,
Number = 0
},
// Mark this items as WITHOUT contact details.
HasContactDetails = false,
}
)
);
AddMap<Contact>(contacts => contacts
.Select(c => new
{
Id = (string) null,
Workspace = (string) null,
Contact = new
{
c.Id,
Name = c.DisplayName,
c.Number
},
// Mark this items as WITH contact details.
HasContactDetails = true,
}
)
);
Reduce = results => results
// First group by the contact ID. This will
// create a group with 2 or more items. One with the contact
// details, and one or more with pair details.
// They are all marked by a boolean flag 'HasContactDetails'.
.GroupBy(x => x.Contact.Id)
// We are going to enrich each item in the current group, that is
// marked as 'HasContactDetails = false', with the contact number.
// We need that so that we can filter on it later.
.Select(group =>
group
.Select(i => new
{
i.Id,
i.Workspace,
Contact = new
{
i.Contact.Id,
i.Contact.Name,
// Does the current item have the contact details?
Number = i.HasContactDetails
// Yes, in this case we use the previously set contact number.
? i.Contact.Number
// No, find the item with the contact details and grab the number.
: group.Single(x => x.HasContactDetails).Contact.Number
},
// Pass on the flag that indicates wheter or not
// this item has the contact details. We are going
// to need it later.
i.HasContactDetails
}
)
// We don't need the items with the contact details
// anymore, so filter them out.
.Where(x => !x.HasContactDetails)
)
// Flatten all the small lists to one big list.
.SelectMany(x => x);
// Mark the following fields of the result as searchable.
Index(x => x.Contact.Number, FieldIndexing.Search);
}
}
I've setup a full example that reproduces the issues I am having. You can find the example here.
Creating the index works fine. Querying the index works fine also as it properly matched the pair and contact and enriched the index result with the number of the contact. But when I try to use a .Where() or .Search() on the nested Number property it fails to properly filter the result dataset from the index.
The index without any filtering works as you can see in below code example (also available in the full example).
private static async Task ThisOneWorks()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.ToListAsync();
LogResults("ThisOneWorks()", results);
}
// Output:
// ThisOneWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWorks(): Pair 'Hermione Granger' with number '71'
// ThisOneWorks(): Pair 'Albus Dumbledore' with number '72'
}
Filtering on a non-nested value also works (also available in the full example). As you can see it filters out the one with a different workspace.
private static async Task ThisOneWithWorkspaceFilterWorks()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.Where(x => x.Workspace == "hogwarts")
.ToListAsync();
LogResults("ThisOneWithWorkspaceFilterWorks()", results);
}
// Output:
// ThisOneWithWorkspaceFilterWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWithWorkspaceFilterWorks(): Pair 'Harry Potter' with number '70'
// ThisOneWithWorkspaceFilterWorks(): Pair 'Hermione Granger' with number '71'
}
When I try to filter/search on the Workspace and Number properties I would expect two results that are related to the contact Harry Potter. But instead I just get an empty dataset back.
private static async Task ThisOneWithWorkspaceAndNumberFilterDoesntWork()
{
using (var session = Store.OpenAsyncSession())
{
var results = await session
.Query<Pairs_Search.Result, Pairs_Search>()
.Where(x => x.Workspace == "hogwarts")
.Where(x => x.Contact.Number == 70)
.ToListAsync();
LogResults("ThisOneWithWorkspaceAndNumberFilterDoesntWork()", results);
}
// Output:
// ThisOneWithWorkspaceAndNumberFilterDoesntWork(): EMPTY RESULTS!
}
Can anyone tell me what I am doing wrong here? Any help would be greatly appreciated!
The way to go about it is to store ContactResult in a different collection,
which is what is called a related document in this case,
and when you create the index then you 'Index the Related Document'
Learn from the demo example in:
https://demo.ravendb.net/demos/csharp/related-documents/index-related-documents
The example is for a basic map index but the principle is the same for Multi-Map.
Remove the public class ContactResult from the index class
and define the index with something like:
select new Result
{
....
Number = LoadDocument<Contact>(Pair.Contact).Number
....
}

LongListSelector Date Grouped Lists

Windows Phone 8 SDK question using the LongListSelector to group on dates.
I am familiar with the AlphaKeyGroup helper approach to grouping on letters.
Has anyone done/seen a similar write up for dates that is similarly locale aware? (Numbers would be a plus as well)
So I struggled with this one a bit too because the AlphaKeyGroup example from MSDN you mentioned is more complicated than it needs to be because of localization. What you are trying to do is create a new List object that that has one extra property, the Key. This Key property is the name that you group on. In the AlphaKeyGroup example it is each letter of the alphabet in your region. So create your own group object that inherits from List.
public class TimeKeyGroup<T> : List<T>
{
/// <summary>
/// The Key of this group.
/// </summary>
public string Key { get; private set; }
public TimeKeyGroup(string key)
{
Key = key;
}
}
Now create a method called CreateGroups that accepts an IEnumerable of the object you want to group and returns a list of you custom list object that you just created. In my implementation I was grouping Workout objects that had a TimeStamp property. In this method create group objects for each type of group key name you want such as "Last 7 Day" or "Last 6 Months". Then fill each group by loooping over the passed in IEnumerable group and evaluating each to determine where they should be grouped. Finally add each grouped list to a master group list and return it. Here is my method:
public static List<TimeKeyGroup<Workout>> CreateGroups(IEnumerable<Workout> workouts)
{
// Create List to hold each item
List<TimeKeyGroup<Workout>> groupedWorkouts = new List<TimeKeyGroup<Workout>>();
// Create a TimeKeyGroup for each group I want
TimeKeyGroup<Workout> LastSeven = new TimeKeyGroup<Workout>("Last Seven Days");
TimeKeyGroup<Workout> LastTwoWeeks = new TimeKeyGroup<Workout>("Last Two Weeks");
TimeKeyGroup<Workout> LastMonth = new TimeKeyGroup<Workout>("Last Month");
TimeKeyGroup<Workout> LastSixMonths = new TimeKeyGroup<Workout>("Last Six Months");
TimeKeyGroup<Workout> LastYear = new TimeKeyGroup<Workout>("Last Year");
TimeKeyGroup<Workout> AllTime = new TimeKeyGroup<Workout>("All Time");
// Fill each list with the appropriate workouts
foreach (Workout w in workouts)
{
if (w.TimeStamp > DateTime.Now.AddDays(-7))
{
LastSeven.Add(w);
continue;
}
else if (w.TimeStamp > DateTime.Now.AddDays(-14))
{
LastTwoWeeks.Add(w);
continue;
}
else if (w.TimeStamp > DateTime.Now.AddMonths(-1))
{
LastMonth.Add(w);
continue;
}
else if (w.TimeStamp > DateTime.Now.AddMonths(-6))
{
LastSixMonths.Add(w);
continue;
}
else if (w.TimeStamp > DateTime.Now.AddMonths(-12))
{
LastYear.Add(w);
continue;
}
else
{
AllTime.Add(w);
}
}
// Add each TimeKeyGroup to the overall list
groupedWorkouts.Add(LastSeven);
groupedWorkouts.Add(LastTwoWeeks);
groupedWorkouts.Add(LastMonth);
groupedWorkouts.Add(LastSixMonths);
groupedWorkouts.Add(LastYear);
groupedWorkouts.Add(AllTime);
return groupedWorkouts;
}
Now you have a nice list of grouped lists. Awesome! The rest is just hooking the itemssource property of your LongListSelector to this new list and defining a JumpListStyle and GroupedHeaderTemplate. The original article you referenced has all that info.
Good luck and happy Windows Phone Development!
I've had success with this example from MSDN after I was stuck on the same example as you are now. The Group.cs file contains an implementation of a group which can be freely used with strings. My guess is, that you could easily add another property of DateTime and then you could try grouping by dates.
Well I use a modified version of the AlphaKeyGroup. This new class I called StringKeyGroup and creates the groups based on the first charachter of the items. So it's simply a matter of replacing AlphaKeyGroup with StringKeyGroup.
This new functionality can be used like:
myLonglistSelector.ItemSource = GroupedItems(myCollection);
....
public ObservableCollection<StringKeyGroup<myClass>> GroupedItems(IEnumerable<myClass> source)
{
return StringKeyGroup<myClass>.CreateGroups(source,
System.Threading.Thread.CurrentThread.CurrentUICulture,
s => s.Name, true);
}
Here's the code for StringKeyGroup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.Globalization;
namespace MyNameSpace
{
public class StringKeyGroup<T> : ObservableCollection<T>
{
public delegate string GetKeyDelegate(T item);
public string Key { get; private set; }
public StringKeyGroup(string key)
{
Key = key;
}
public static ObservableCollection<StringKeyGroup<T>> CreateGroups(IEnumerable<T> items, CultureInfo ci, GetKeyDelegate getKey, bool sort)
{
var list = new ObservableCollection<StringKeyGroup<T>>();
foreach (var item in items)
{
var itemKey = getKey(item).Substring(0, 1).ToLower();
var itemGroup = list.FirstOrDefault(li => li.Key == itemKey);
var itemGroupIndex = itemGroup != null ? list.IndexOf(itemGroup) : -1 ;
if (itemGroupIndex == -1)
{
list.Add(new StringKeyGroup<T>(itemKey));
itemGroupIndex = list.Count - 1;
}
if (itemGroupIndex >= 0 && itemGroupIndex < list.Count)
{
list[itemGroupIndex].Add(item);
}
}
if (sort)
{
foreach (var group in list)
{
group.ToList().Sort((c0, c1) => ci.CompareInfo.Compare(getKey(c0), getKey(c1)));
}
}
return list;
}
}
}
To use LongListSelector with numbers, lets try to group a list of people by age, instead of by the first letter of their first name (in the MSDN PeopleHub example)
They use the mysterious AlphaKeyGroup, which is a list of people with first names starting with the same letter (That letter becomes the Key of the AlphaKeyGroup). AlphaKeyGroups of people can look like this:
Key = A: Amber, Amelia (where both People objects have names that start with A)
Key = B: Bob, Brian (where both People objects have names that start with B)
Key = C: etc etc
We're gonna use the IntegerKeyGroup, which is a list of people who have the same age. IntegerKeyGroups of people can look like this:
Key = 23: Jennifer, Ryan (where both People objects are 23 years old)
Key = 26: Amber, Megan (where both People objects are 26 years old)
Key = 34: etc etc
So to follow GentryRiggen's framework, we have to first define the IntegerKeyGroup, then stick people in the age groups they belong. I put these in ViewModel file.
public class IntegerKeyGroup<T> : List<T>
{
public int Key { get; private set; }
public IntegerKeyGroup(int key)
{
Key = key;
}
}
Notice IntegerKeyGroup is simply a List but with a special integer member called Key. This means we can label a list of people with an age integer as the Key.
Now we need to sort our big list of unsorted people into IntegerKeyGroups of different ages, and finally combine all these IntegerKeyGroups together. This combined list of IntegerKeyGroups is what LongListSelector accepts to be displayed.
public static List<IntegerKeyGroup<Person>> CreateGroups(IEnumerable<Person> UnsortedPeopleList)
{
// Create combined list of IntegerKeyGroups
List<IntegerKeyGroup<Person>> CombinedPeopleList = new List<IntegerKeyGroup<Person>>();
// Create a IntegerKeyGroup for each age group I want,
// The constructor parameters sets the Key to the IntegerKeyGroup
IntegerKeyGroup<Person> Age23s = new IntegerKeyGroup<Person>(23);
IntegerKeyGroup<Person> Age26s = new IntegerKeyGroup<Person>(26);
IntegerKeyGroup<Person> Age34s = new IntegerKeyGroup<Person>(34);
// Populate each IntegerKeyGroup with the appropriate Persons
foreach (Person p in UnsortedPeopleList)
{
switch (p.Age)
{
case 23: Age23s.Add(p); continue;
case 26: Age26s.Add(p); continue;
case 34: Age34s.Add(p); continue;
default: continue; // we don't support ages other than the 3 above
}
}
// Add each IntegerKeyGroup to the overall list
CombinedPeopleList.Add(Age23s);
CombinedPeopleList.Add(Age26s);
CombinedPeopleList.Add(Age34s);
return CombinedPeopleList;
}
Still in the ViewModel file, make the List of IntegerKeyGroups publicly accessible with the CreateGroups function.
public List<IntegerKeyGroup<Person>> AgeGroupedPeople
{
get
{
return CreateGroups(UnsortedPeople);
}
}
Now in the XAML, make 1 change to the original code from the MSDN PeopleHub example:
<phone:LongListSelector Name="peopleLongListSelector"
ItemsSource="{Binding AgeGroupedPeople}" <!-- Change is in this line! -->
JumpListStyle="{StaticResource LongListSelectorJumpListStyle}"
ListHeaderTemplate="{StaticResource LongListSelectorHeaderTemplate}"
GroupHeaderTemplate="{StaticResource LongListSelectorGroupHeaderTemmplate}"
ItemTemplate="{StaticResource LongListSelectorItemTemplate}"
HideEmptyGroups ="true" IsGroupingEnabled ="true" LayoutMode="List">
</phone:LongListSelector>
This should group people by integers, in this case, age.

Join query in Nhibernate criteria or in Nhibernate Linq

I have a query I am trying to run but I'm not getting desired result.
select * from employee_login e
left join employee_attendance ea on
e.emp_id = ea.EmpId and dated = '2012-01-11'
The Linq query which I tried with Nhibernate is
var attendance = from emp in session.Query<Employee>()
join empatten in session.Query<EmployeeAttendance>()
on emp.emp_id equals empatten.EmpId into atts
from ur in atts.DefaultIfEmpty()
select new { ur };
In the var attendance resultview. How can I achieve these two things?
a left join over employee and employeeattendance ( employee is the left table)
a and condition on the join not over the join result .
I'm pretty new to this situation using Linq or detached criteria; a detached criteria would be a preferable answer.
Here are the models:
public class EmployeeAttendance
{
public virtual string No_ { get; set; }
public virtual Employee Employee { get; set; }
}
public class Employee
{
public virtual string emp_id { get; set; }
public virtual ISet<EmployeeAttendance> Attendances { get; set; }
public Employee()
{
Attendances = new HashedSet<EmployeeAttendance>();
}
}
The Mapping is :
public class EmployeeAttendanceMap:ClassMap<EmployeeAttendance>
{
public EmployeeAttendanceMap()
{
Table("Employee_Attendance");
Id(x => x.No_).GeneratedBy.Assigned();
References(x => x.Employee).Column("emp_id");
}
}
public class EmployeeMap : ClassMap<Employee>
{
public EmployeeMap()
{
Table("employee_login");
Id(x => x.emp_id).Column("emp_id").GeneratedBy.Assigned();
HasMany(x => x.Attendances).KeyColumn("No_").Cascade.All();
}
}
The Employee is the primary table and AttendanceLeave has the foreign key as EmpId from Employee Table
Edit : I tried this also in my last attempt:
ICriteria criteria = session.CreateCriteria(typeof(Employee), "emp")
.CreateAlias("EmployeeAttendance", "Attendance", CriteriaSpecification.LeftJoin
, Restrictions.Eq("Attendance.Dated", DateTime.Parse("2012-1-11")));
but I ended up getting error as :
could not resolve property: EmployeeAttendance of: Royal.Data.Core.Domain.Employee
It looks like you want to get employees on leave as of a certain date. I think this would work, though I've never used the between expression in this way before:
var detached = DetachedCriteria.For<AttendanceLeave>("al")
.Add(Expression.Between('2012-01-11', "LeaveFrom", "LeaveTo")) //this should be a DateTime
.Add(Restrictions.EqProperty ("al.EmpId", "e.emp_id")) //make sure to use "e" for employee criteria alias
.SetProjection (Projections.Count ("al.EmpId"));
var employeesOnLeave = session.CreateCriteria<Employee>("e")
.Add(Restrictions.Gt(Projections.Subquery(detached), 0))
.List();
You'll still get the complete set of leaves on each employee, but it should be the employee you want.
update - looking at your comment, it seems something like this could be what you're after:
DateTime dateInQuestion = new DateTime(2012, 1, 11);
var employeesOnLeaveAsOfDateInQuestion =
session.CreateCriteria<Employee>("e")
.CreateCriteria("e.Attendances", "ea"
, NHibernate.SqlCommand.JoinType.LeftOuterJoin
, Restrictions.Between(dateInQuestion, "ea.LeaveFrom", "ea.LeaveTo"))
.List<Employee>();
This seems to work - but you need to make sure the entities you get back are not cached, otherwise cached copies w/ the full collection will be returned. This is what I tested with - not exactly like your situation because collection is maintained through a link table, but I think it will work the same either way - you may need to evict the collection specifically with a straight one to many though (the EvictCollection method is found on the session factory, not the session). You should need this bit for testing only (in my tests, the database only lives as long as the session). There is also a QueryOver example in the gist if you'd prefer to solve it that way.

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.
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! :-)
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)
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>();
}