Model one to many in RavenDb for better performance

408 views Asked by At

I'm approaching to document database and I'm little bit confused how to map documents relationship, in a situation as follow

public class Person
{
    public Person()
    {
    }
    public int Id { get; set; }
    public string Name { get;set;}
    public string Surname { get; set; }
    public DateTime? BirthDate { get; set; }
}


public class Car
{
    public Car() { }
    public int Id { get; set; }
    public string Name { get; set; }
    public int PersonId { get; set;}
}

A person has one or more cars for example in this way I can query the db as follow

public Car Get(int id)
    {
        Car car = null;
        using (IDocumentSession session = store.OpenSession())
        {
            car = session.Include<Car, Person>(x => x.PersonId).Load<Car>(id);
            bool isLoaded = session.Advanced.IsLoaded("people/" + car.PersonId); // true!
        }
        return car;
    }

and it's everything ok, the client makes just one request, but if I have a person and I want to show all his cars how can I query the db to do just a request? I think tha I must modify the model putting a List<int> Cars in Person for reference his cars. Note that I don't want to embed Cars in the Person document because Cars can be referenced from others document.

Thanks.

2

There are 2 answers

0
Andrej Krivulčík On BEST ANSWER

You can index the Cars collection and load all the cars from the index.

The index would look like this:

public class CarIndex : AbstractIndexCreationTask<Car, CarView>
{
    public CarIndex()
    {
        Map = cars => from car in cars
                      select new
                      {
                          car.Id,
                          car.Name,
                          car.PersonId,
                      };
    }
}

The CarView class is identical to the Car class, but can be changed to better fit the indexing needs.

public class CarView
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int PersonId { get; set; }
}

You'll need to execute the index before being able to use it:

new CarIndex().Execute(store);

Loading the cars for a certain person would look like this:

using (IDocumentSession session = store.OpenSession())
{
    session.Store(new Person { Id = 1, Name = "A", Surname = "A" });
    session.Store(new Car { Id = 1, Name = "A", PersonId = 1 });
    session.Store(new Car { Id = 2, Name = "B", PersonId = 1 });
    session.Store(new Car { Id = 3, Name = "C", PersonId = 2 });
    session.SaveChanges();
}

WaitForIndexing(store); // from RavenTestBase

using (IDocumentSession session = store.OpenSession())
{
    var resultsForId1 = session
        .Query<CarView, CarIndex>()
        .ProjectFromIndexFieldsInto<CarView>()
        .Where(x => x.PersonId == 1);
    Assert.Equal(2, resultsForId1.Count());
    var resultsForId2 = session
        .Query<CarView, CarIndex>()
        .ProjectFromIndexFieldsInto<CarView>()
        .Where(x => x.PersonId == 2);
    Assert.Equal(1, resultsForId2.Count());
}

If you want to load the person and their cars in a single database request, use lazy loading:

var resultsForId1 = session
    .Query<CarView, CarIndex>()
    .ProjectFromIndexFieldsInto<CarView>()
    .Where(x => x.PersonId == 1).Lazily();
var person = session.Advanced.Lazily.Load<Person>(1);

var personValue = person.Value;
var resultsValue = resultsForId1.Value;

Complete test (needs xunit and RavenDB.Tests.Helpers nugets):

using Raven.Client;
using Raven.Client.Indexes;
using Raven.Tests.Helpers;
using System;
using System.Linq;
using Xunit;

namespace SO41547501Answer
{
    public class SO41547501 : RavenTestBase
    {
        [Fact]
        public void SO41547501Test()
        {
            using (var server = GetNewServer())
            using (var store = NewRemoteDocumentStore(ravenDbServer: server))
            {
                new CarIndex().Execute(store);

                using (IDocumentSession session = store.OpenSession())
                {
                    session.Store(new Person { Id = 1, Name = "A", Surname = "A" });
                    session.Store(new Car { Id = 1, Name = "A", PersonId = 1 });
                    session.Store(new Car { Id = 2, Name = "B", PersonId = 1 });
                    session.Store(new Car { Id = 3, Name = "C", PersonId = 2 });
                    session.SaveChanges();
                }

                WaitForAllRequestsToComplete(server);
                WaitForIndexing(store);

                using (IDocumentSession session = store.OpenSession())
                {
                    var resultsForId1 = session
                        .Query<CarView, CarIndex>()
                        .ProjectFromIndexFieldsInto<CarView>()
                        .Where(x => x.PersonId == 1);
                    Assert.Equal(2, resultsForId1.Count());
                    var resultsForId2 = session
                        .Query<CarView, CarIndex>()
                        .ProjectFromIndexFieldsInto<CarView>()
                        .Where(x => x.PersonId == 2);
                    Assert.Equal(1, resultsForId2.Count());
                }

                using (IDocumentSession session = store.OpenSession())
                {
                    server.Server.ResetNumberOfRequests();
                    var resultsForId1 = session
                        .Query<CarView, CarIndex>()
                        .ProjectFromIndexFieldsInto<CarView>()
                        .Where(x => x.PersonId == 1).Lazily();
                    var person = session.Advanced.Lazily.Load<Person>(1);

                    var personValue = person.Value;
                    var resultsValue = resultsForId1.Value;
                    Assert.Equal("A", personValue.Name); // person data loaded
                    Assert.Equal("A", resultsValue.First().Name); // cars data loaded
                    Assert.Equal(1, server.Server.NumberOfRequests); // only one request sent to the server
                }
            }
        }
    }

    public class CarIndex : AbstractIndexCreationTask<Car, CarView>
    {
        public CarIndex()
        {
            Map = cars => from car in cars
                          select new
                          {
                              car.Id,
                              car.Name,
                              car.PersonId,
                          };
        }
    }

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Surname { get; set; }
        public DateTime? BirthDate { get; set; }
    }

    public class Car
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int PersonId { get; set; }
    }

    public class CarView
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int PersonId { get; set; }
    }
}
0
Ayende Rahien On

You can do it like this:

using (IDocumentSession session = store.OpenSession())
{
    var carsForOne = session.Query<Car>()
             .Include(x=>x.PersonId)
             .Where(x=>x.PersonId == "people/1")
             .ToList();

    var person = session.Load<Person>("people/1");
}

This make just a single db request.