2017-01-12 3 views
0

Cette question est une spin-off de RavenDB: Why do I get null-values for fields in this multi-map/reduce index?, mais j'ai réalisé, le problème en était une autre.RavenDB: Comment indexer correctement un produit cartésien dans une map-reduce?

Tenir compte de mon domaine extrêmement simplifié, réécrite pour une location de film scénario de magasin pour l'abstraction:

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; } 
} 

Il est un texte-livre beaucoup à de nombreux exemples.

L'indice Je veux créer est ceci:

Pour un utilisateur donné, donne-moi une liste de tous les films dans la base de données (filtrage/recherche gauche pour le moment) avec un entier décrivant comment plusieurs fois (ou zéro) l'utilisateur a loué ce film.

Fondamentalement comme ceci:

utilisateurs:

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

Films:

| 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 | 

Idéalement, je veux un index de requête, qui ressemblerait à ceci:

| 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   | 

Ou déclarative:

  • Je veux toujours une liste complète de tous les films (finalement je ajoutera le filtrage/la recherche) - même en fournissant un utilisateur qui n'a jamais encore loué un seul film
  • Je veux un décompte des locations pour chaque utilisateur, juste le nombre entier
  • Je veux être en mesure de trier par la location-count - à savoir montrer les films les plus loués pour un utilisateur donné en haut de la liste

Cependant, je ne peux pas trouver un moyen de faire la "cross-join" ci-dessus et enregistrez-le dans l'index. Au lieu de cela, je pensais au début, je l'ai eu droit à cette manœuvre ci-dessous, mais il ne me permet pas de trier (voir test à défaut):

{ "Non pris en charge le calcul: x.UserRentalCounts.SingleOrDefault (rentalCount => (rentalCount.UserId == valeur (+ UnitTestProject2.MovieRentalTests <> c__DisplayClass0_0) .user_john.Id)). Count. Vous ne pouvez pas utiliser le calcul dans les requêtes RavenDB (seules expressions simples membres sont autorisés). "}

Ma question est fondamentalement: comment puis-je - ou puis-je indexer ainsi, que mes conditions sont remplies?


Ci-dessous mon exemple mentionné, qui ne répond pas à mes besoins, mais c'est là où je suis en ce moment.Il utilise les packages suivants (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; } 
     } 
    } 
} 

Répondre

1

Étant donné que votre exigence dit "pour un utilisateur donné", si vous ne cherchez vraiment qu'un seul utilisateur, vous pouvez le faire avec un index Multi-Map. Utilisez la table Movies elle-même pour produire les enregistrements de base zéro-count et ensuite mapper dans les enregistrements réels MovieRentals pour l'utilisateur en plus de cela.

Si vous en avez vraiment besoin pour tous les utilisateurs croisés avec tous les films, je ne crois pas qu'il existe un moyen de le faire proprement avec RavenDB car cela serait considéré comme reporting which is noted as one of the sour spots for RavenDB.

Voici quelques options si vous voulez vraiment essayer de le faire avec RavenDB:

1) Créer des enregistrements fictifs dans la base de données pour chaque utilisateur et chaque film et les utiliser dans l'index avec un compte 0. Chaque fois qu'un film ou un utilisateur est ajouté/mis à jour/supprimé, mettez à jour les enregistrements fictifs en conséquence.

2) Générer le compte zéro vous enregistre en mémoire sur demande et fusionner ces données avec les données RavenDB vous donne de retour pour les comptes non nuls. Interrogez tous les utilisateurs, interrogez tous les films, créez les enregistrements de base zéro, puis effectuez la requête réelle pour les comptes non nuls et superposez-les en haut. Enfin, appliquez la logique de pagination/filtrage/tri.

3) Utilisez le faisceau de réplication SQL pour répliquer les utilisateurs, des films et des tables MovieRental vers SQL et utiliser SQL pour cette requête « reporting ».

+0

Merci pour cela, David. Je n'aime pas l'option 2, parce que quand je devrais trier selon le nombre, je devrais charger «tout» en mémoire en considérant également la pagination. L'option 3 est certainement la voie à suivre, et je réalise, la limitation à l'agrégation et tel est le compromis lors du choix de RavenDB. Dans cette situation particulière, en attendant les réponses, j'ai fini par séparer la liste des "films préférés" de la liste "tous les films" (donc un simple index sur "MovieRentals" avec regroupement et somme) - il s'est même avéré être meilleur UX pour l'utilisateur final imo. Merci encore et voici votre prime :) –