RavenDB - Flattening Object Graphs and Projections
One of the guys in JabbR RavenDB chat room had a pretty interesting problem that took a while to solve. The problem was that he wasn't trying to return anything to do with the original document, but get a flattened view of some information inside the document.
The scenario was a Game Server which a collection of Connected Users.
public class GameServer
{
public string Id { get; set; }
public string ServerName { get; set; }
public IEnumerable<User> ConnectedUser { get; set; }
public class User
{
public string UserId { get; set; }
public string Name { get; set; }
public DateTimeOffset DateConnected { get; set; }
}
}
Given say 3 servers with a bunch of users on each server, and searching for a user who's name begins with 'b' the expected result was along the lines of:
UserId | Name | DateConnected | ServerName |
---|---|---|---|
users/3 | Bob | 15/03/2012 12:44 | iPGN CS #01 Iceworld |
users/2 | Bill | 15/03/2012 1:23 | 3FL CS #4 |
users/8 | Benny | 15/03/2012 1:18 | 3FL CS #4 |
So basically for each user you get the server he's connected to.
We tried everything under the sun, Reduce, Transform, etc. But couldn't figure out how to get the results. Infact using a Transform we could get the # of results returned, except they were all NULL. :(
Turns out it's rather easy using just a Map and AsProjection<T>
Setup Data
Test data can be viewed here: http://pastie.org/3516113
Index
The index is really easy, it's basically a Select Many map, but we also need to store the results from the Map.
public class GameServers_ConnectedUsers :
AbstractIndexCreationTask<GameServer, GameServers_ConnectedUsers.IndexResult>
{
public GameServers_ConnectedUsers()
{
Map = servers => from s in servers
from y in s.ConnectedUsers
select new
{
ServerName = s.ServerName,
UserName = y.Name,
DateConnected = y.DateConnected,
UserId = y.UserId
};
Store(x => x.ServerName, FieldStorage.Yes);
Store(x => x.UserName, FieldStorage.Yes);
Store(x => x.DateConnected, FieldStorage.Yes);
Store(x => x.UserId, FieldStorage.Yes);
}
public class IndexResult
{
public string ServerName { get; set; }
public string UserName { get; set; }
public DateTimeOffset DateConnected { get; set; }
public string UserId { get; set; }
}
}
The reason for storing the results from the Map is because if we don't, we don't actually get any results back. (will show you soon)
Querying
So querying is the same as always, only we need to provide the 'AsProjection
var results =
session.Query<GameServers_ConnectedUsers.IndexResult, GameServers_ConnectedUsers>()
.Where(x => x.UserName.StartsWith("b"))
.AsProjection<GameServers_ConnectedUsers.IndexResult>()
.ToList();
The Projection just needs to be an object that matches the result, otherwise it will attempt to return the original documents.
If we run the query without the AsProjection<T>
we end up with an exception because the result (the original document) doesn't match the object we were querying against IndexResult
If we set the AsProjection
to dynamic
, or GameServer
, we get the original documents. BUT the funny thing is if you use GameServer
you end up with as many results as the projection has. In this case 3.
If we expand them out, we see we actually get a duplicate.
But if we use dynamic
we get unique documents:
Using the IndexResult
(or an object that matches the projection) we get the projected results that we wanted at the start of this post:
Three results, and all the correct data:
Storing the results
I showed in the index that I was storing the results I wanted in the projection. This is because if we don't, we end up with this:
Not only do we end up with the incorrect number of results, they are missing data that isn't found in the original document.
You can see GameServer
is there, that's because it's found on the document. We need to store
the data so that RavenDB can return it, that means we use more disk space, but without it, RavenDB would have to query and assume the data that you wanted to return. By storing it RavenDB just returns what matches the query, and doesn't do any extra work.
So if you want to return the projected results, then you need to Store them.
I've put a gist here for anyone interested.
https://gist.github.com/1972646
comments powered by Disqus