2017-01-12 14 views
0

Этот вопрос является побочным эффектом RavenDB: Why do I get null-values for fields in this multi-map/reduce index?, но я понял, проблема была другая.RavenDB: Как я могу правильно индексировать декартово произведение на карте?

Рассмотрим мой очень упрощенный домен, переписанный в магазине сценарий видеопроката абстракции:

public class User 
{ 
    public string Id { get; set; } 
} 

public class Movie 
{ 
    public string Id { get; set; } 
} 

public class MovieRental 
{ 
    public string Id { get; set; } 
    public string MovieId { get; set; } 
    public string UserId { get; set; } 
} 

Это учебник многие-ко-многим. Например,

Индекс Я хочу создать это:

Для данного пользователя, дайте мне список каждого фильма в базе данных (фильтрация/поиск опущены на данный момент), а также с целым числом, описывающим, как много раз (или ноль) пользователь арендовал этот фильм.

В принципе, как это:

Пользователи:

| Id  | 
|--------| 
| John | 
| Lizzie | 
| Albert | 

Фильмы:

| Id   | 
|--------------| 
| Robocop  | 
| Notting Hill | 
| Inception | 

MovieRentals:

| Id  | UserId | MovieId  | 
|-----------|--------|--------------| 
| rental-00 | John | Robocop  | 
| rental-01 | John | Notting Hill | 
| rental-02 | John | Notting Hill | 
| rental-03 | Lizzie | Robocop  | 
| rental-04 | Lizzie | Robocop  | 
| rental-05 | Lizzie | Inception | 

В идеале, я хочу индекс для запроса, который будет выглядеть следующим образом:

| UserId | MovieId  | RentalCount | 
|--------|--------------|-------------| 
| John | Robocop  | 1   | 
| John | Notting Hill | 2   | 
| John | Inception | 0   | 
| Lizzie | Robocop  | 2   | 
| Lizzie | Notting Hill | 0   | 
| Lizzie | Inception | 1   | 
| Albert | Robocop  | 0   | 
| Albert | Notting Hill | 0   | 
| Albert | Inception | 0   | 

Или декларативно:

  • Я всегда нужен полный список всех фильмов (в конце концов я добавит фильтрацию/поиск) - даже при предоставлении пользователю, который никогда не снимал ни одного фильма еще
  • Я хочу, чтобы количество аренды для каждого пользователя, всего лишь целое число
  • Я хочу, чтобы иметь возможность сортировать аренду подсчета - т.е. показать наиболее арендованные фильмы для данного пользователя в верхней части списка

Однако, я не могу найти способ, чтобы сделать «перекрестное соединение» выше и сохранить его в индексе. Вместо этого, я сначала думал, что я получил это право с этим маневром ниже, но это не позволяет мне разобраться (см неисправного тест):

{ "Не поддерживается вычисление: x.UserRentalCounts.SingleOrDefault (rentalCount => (rentalCount.UserId == значение (UnitTestProject2.MovieRentalTests + <> c__DisplayClass0_0) .user_john.Id)). граф. Вы не можете использовать вычисление в RavenDB запросов (только простые выражения члены допускаются). "}

Мой вопрос в основном: Как я могу - или могу я вообще - индексировать так, чтобы мои требования выполнялись?


Ниже мой пример упоминается, что не отвечает моим требованиям, но вот где я сейчас нахожусь.Он использует следующие пакеты (VS2015):

packages.config

<?xml version="1.0" encoding="utf-8"?> 
<packages> 
    <package id="Microsoft.Owin.Host.HttpListener" version="3.0.1" targetFramework="net461" /> 
    <package id="NUnit" version="3.5.0" targetFramework="net461" /> 
    <package id="RavenDB.Client" version="3.5.2" targetFramework="net461" /> 
    <package id="RavenDB.Database" version="3.5.2" targetFramework="net461" /> 
    <package id="RavenDB.Tests.Helpers" version="3.5.2" targetFramework="net461" /> 
</packages> 

MovieRentalTests.cs

using System.Collections.Generic; 
using System.Linq; 
using NUnit.Framework; 
using Raven.Client.Indexes; 
using Raven.Client.Linq; 
using Raven.Tests.Helpers; 

namespace UnitTestProject2 
{ 
    [TestFixture] 
    public class MovieRentalTests : RavenTestBase 
    { 
     [Test] 
     public void DoSomeTests() 
     { 
      using (var server = GetNewServer()) 
      using (var store = NewRemoteDocumentStore(ravenDbServer: server)) 
      { 
       //Test-data 
       var user_john = new User { Id = "John" }; 
       var user_lizzie = new User { Id = "Lizzie" }; 
       var user_albert = new User { Id = "Albert" }; 


       var movie_robocop = new Movie { Id = "Robocop" }; 
       var movie_nottingHill = new Movie { Id = "Notting Hill" }; 
       var movie_inception = new Movie { Id = "Inception" }; 

       var rentals = new List<MovieRental> 
       { 
        new MovieRental {Id = "rental-00", UserId = user_john.Id, MovieId = movie_robocop.Id}, 
        new MovieRental {Id = "rental-01", UserId = user_john.Id, MovieId = movie_nottingHill.Id}, 
        new MovieRental {Id = "rental-02", UserId = user_john.Id, MovieId = movie_nottingHill.Id}, 
        new MovieRental {Id = "rental-03", UserId = user_lizzie.Id, MovieId = movie_robocop.Id}, 
        new MovieRental {Id = "rental-04", UserId = user_lizzie.Id, MovieId = movie_robocop.Id}, 
        new MovieRental {Id = "rental-05", UserId = user_lizzie.Id, MovieId = movie_inception.Id} 
       }; 

       //Init index 
       new Movies_WithRentalsByUsersCount().Execute(store); 

       //Insert test-data in db 
       using (var session = store.OpenSession()) 
       { 
        session.Store(user_john); 
        session.Store(user_lizzie); 
        session.Store(user_albert); 

        session.Store(movie_robocop); 
        session.Store(movie_nottingHill); 
        session.Store(movie_inception); 

        foreach (var rental in rentals) 
        { 
         session.Store(rental); 
        } 

        session.SaveChanges(); 

        WaitForAllRequestsToComplete(server); 
        WaitForIndexing(store); 
       } 

       //Test of correct rental-counts for users 
       using (var session = store.OpenSession()) 
       { 
        var allMoviesWithRentalCounts = 
         session.Query<Movies_WithRentalsByUsersCount.ReducedResult, Movies_WithRentalsByUsersCount>() 
          .ToList(); 

        var robocopWithRentalsCounts = allMoviesWithRentalCounts.Single(m => m.MovieId == movie_robocop.Id); 
        Assert.AreEqual(1, robocopWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_john.Id)?.Count ?? 0); 
        Assert.AreEqual(2, robocopWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_lizzie.Id)?.Count ?? 0); 
        Assert.AreEqual(0, robocopWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_albert.Id)?.Count ?? 0); 

        var nottingHillWithRentalsCounts = allMoviesWithRentalCounts.Single(m => m.MovieId == movie_nottingHill.Id); 
        Assert.AreEqual(2, nottingHillWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_john.Id)?.Count ?? 0); 
        Assert.AreEqual(0, nottingHillWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_lizzie.Id)?.Count ?? 0); 
        Assert.AreEqual(0, nottingHillWithRentalsCounts.UserRentalCounts.FirstOrDefault(x => x.UserId == user_albert.Id)?.Count ?? 0); 
       } 

       // Test that you for a given user can sort the movies by view-count 
       using (var session = store.OpenSession()) 
       { 
        var allMoviesWithRentalCounts = 
         session.Query<Movies_WithRentalsByUsersCount.ReducedResult, Movies_WithRentalsByUsersCount>() 
          .OrderByDescending(x => x.UserRentalCounts.SingleOrDefault(rentalCount => rentalCount.UserId == user_john.Id).Count) 
          .ToList(); 

        Assert.AreEqual(movie_nottingHill.Id, allMoviesWithRentalCounts[0].MovieId); 
        Assert.AreEqual(movie_robocop.Id, allMoviesWithRentalCounts[1].MovieId); 
        Assert.AreEqual(movie_inception.Id, allMoviesWithRentalCounts[2].MovieId); 
       } 
      } 
     } 

     public class Movies_WithRentalsByUsersCount : 
      AbstractMultiMapIndexCreationTask<Movies_WithRentalsByUsersCount.ReducedResult> 
     { 
      public Movies_WithRentalsByUsersCount() 
      { 
       AddMap<MovieRental>(rentals => 
        from r in rentals 
        select new ReducedResult 
        { 
         MovieId = r.MovieId, 
         UserRentalCounts = new[] { new UserRentalCount { UserId = r.UserId, Count = 1 } } 
        }); 

       AddMap<Movie>(movies => 
        from m in movies 
        select new ReducedResult 
        { 
         MovieId = m.Id, 
         UserRentalCounts = new[] { new UserRentalCount { UserId = null, Count = 0 } } 
        }); 

       Reduce = results => 
        from result in results 
        group result by result.MovieId 
        into g 
        select new 
        { 
         MovieId = g.Key, 
         UserRentalCounts = (
           from userRentalCount in g.SelectMany(x => x.UserRentalCounts) 
           group userRentalCount by userRentalCount.UserId 
           into subGroup 
           select new UserRentalCount { UserId = subGroup.Key, Count = subGroup.Sum(b => b.Count) }) 
          .ToArray() 
        }; 
      } 

      public class ReducedResult 
      { 
       public string MovieId { get; set; } 
       public UserRentalCount[] UserRentalCounts { get; set; } 
      } 

      public class UserRentalCount 
      { 
       public string UserId { get; set; } 
       public int Count { get; set; } 
      } 
     } 

     public class User 
     { 
      public string Id { get; set; } 
     } 

     public class Movie 
     { 
      public string Id { get; set; } 
     } 

     public class MovieRental 
     { 
      public string Id { get; set; } 
      public string MovieId { get; set; } 
      public string UserId { get; set; } 
     } 
    } 
} 

ответ

1

Так как ваше требование говорит "для данного пользователя", если вы действительно ищут только одного пользователя, вы можете сделать это с помощью индекса Multi-Map. Используйте таблицу «Фильмы» для создания базовых записей нулевого отсчета, а затем карту в фактических записях MovieRentals для пользователя поверх этого.

Если вам действительно нужно это для всех пользователей, скрещенных со всеми фильмами, я не верю, что есть способ сделать это с помощью RavenDB, так как это будет считаться reporting which is noted as one of the sour spots for RavenDB.

Вот некоторые варианты, если вы действительно хотите, чтобы попытаться сделать это с RavenDB:

1) Создание фиктивных записей в БД для каждого пользователя и каждого фильма и использовать их в индексе с 0 кол. Всякий раз, когда фильм или пользователь добавляется/обновляется/удаляется, соответствующим образом обновляйте фиктивные записи.

2) Сгенерируйте записи нулевого отсчета самостоятельно в памяти по запросу и объедините эти данные с данными, которые RavenDB возвращает вам для ненулевого отсчета. Запрос для всех пользователей, запрос для всех фильмов, создание базовых записей с нулевым отсчетом, а затем фактический запрос для ненулевого отсчета и уровня, который сверху. Наконец, примените логику подкачки/фильтрацию/сортировку.

3) Используйте пакет репликации SQL для репликации таблиц Users, Movies и MovieRental для SQL и использования SQL для этого «отчета».

+0

Спасибо за это, Дэвид. Мне не нравится вариант 2, потому что когда приходится сортировать по счету, мне нужно будет загружать «все» в память, когда также рассматриваю пейджинг. Вариант 3 - это, безусловно, путь, и я понимаю, что ограничение на агрегирование и таков компромисс при выборе RavenDB. В этой конкретной ситуации, ожидая ответов, я оставил список «любимых фильмов» отдельно от списка «всех фильмов» (таким образом, простой индекс на «MovieRentals» с группировкой и суммой) - он даже оказался лучше UX для конечного пользователя imo. Еще раз спасибо, и вот ваша награда :) –