32 votes

Lutte contre le produit cartésien (x-join) lors de l'utilisation de NHibernate 3.0.0

Je suis mauvais en maths, mais j'ai en quelque sorte se faire une idée de ce que produit cartésien est.
Voici ma situation (simplifié):

public class Project{
 public IList<Partner> Partners{get;set;}
}
public class Partner{
 public IList<PartnerCosts> Costs{get;set;}
 public IList<Address> Addresses{get;set;}
}
public class PartnerCosts{
 public Money Total{get;set;}
}
public class Money{
 public decimal Amount{get;set;}
 public int CurrencyCode{get;set;}
}
public class Address{
 public string Street{get;set;}
}

Mon but est effectivement de charge tout le Projet.

Le problème est bien sûr:

  • Si j'essaie d'désireux de charge des partenaires et de leurs coûts, la requête renvoie gazillion lignes
  • Si je lazy load Partenaire.Les coûts, db reçoit demande des envois en nombre (qui est un peu plus rapide que la première approche)

Comme je l'ai lu, commune de la solution de contournement consiste à utiliser MultiQueries, mais j'ai en quelque sorte un juste ne pas l'obtenir.
Donc, je suis l'espoir d'apprendre par le biais de cet exemple précis.

Comment charger ensemble du Projet?

P. s. Je suis l'aide de NHibernate 3.0.0.
S'il vous plaît, ne postez pas de réponses avec hql ou chaîne façonné critères api approches.

47voto

Florian Lim Points 3919

Ok, j'ai écrit un exemple pour moi l'expression de votre structure et cela devrait fonctionner:

int projectId = 1; // replace that with the id you want
// required for the joins in QueryOver
Project pAlias = null;
Partner paAlias = null;
PartnerCosts pcAlias = null;
Address aAlias = null;
Money mAlias = null;

// Query to load the desired project and nothing else    
var projects = repo.Session.QueryOver<Project>(() => pAlias)
    .Where(p => p.Id == projectId)
    .Future<Project>();

// Query to load the Partners with the Costs (and the Money)
var partners = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(p => p.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Costs, () => pcAlias)
    .JoinAlias(() => pcAlias.Money, () => mAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// Query to load the Partners with the Addresses
var partners2 = repo.Session.QueryOver<Partner>(() => paAlias)
    .JoinAlias(o => o.Project, () => pAlias)
    .Left.JoinAlias(() => paAlias.Addresses, () => aAlias)
    .Where(() => pAlias.Id == projectId)
    .Future<Partner>();

// when this is executed, the three queries are executed in one roundtrip
var list = projects.ToList();
Project project = list.FirstOrDefault();

Mes classes avaient des noms différents mais reflète exactement la même structure. J'ai remplacé les noms et j'espère qu'il n'y a pas de fautes de frappe.

Explication:

Les alias sont nécessaires pour les jointures. J'ai défini trois requêtes à la charge de l' Project vous le souhaitez, Partners leurs Costs et de la Partners leurs Addresses. En utilisant l' .Futures() en gros, je dis NHibernate à les exécuter dans un aller-retour au moment où je veux en fait les résultats, à l'aide de projects.ToList().

Ce sera résultat en trois SQL qui sont en fait exécutées dans un aller-retour. Les trois états sera de retour les résultats suivants: 1) 1 ligne de votre Projet 2) x lignes avec les Partenaires et de leurs Coûts (et l'Argent), où x est le nombre total des Coûts pour les Partenaires du Projet 3) a des lignes avec les Partenaires et de leurs Adresses, où y est le nombre total d'Adresses pour les Partenaires du Projet

Votre base de données doit retourner 1+x+y lignes, au lieu de x*y rangées, ce qui serait un produit cartésien. J'espère que votre base de données prend en charge cette fonctionnalité.

5voto

Michael Buen Points 20453

Si vous utilisez Linq sur votre NHibernate, vous pouvez simplifier la prévention cartésienne avec ceci:

 int projectId = 1;
var p1 = sess.Query<Project>().Where(x => x.ProjectId == projectId);


p1.FetchMany(x => x.Partners).ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Costs)
    .ThenFetch(x => x.Total)
.ToFuture();

sess.Query<Partner>()
.Where(x => x.Project.ProjectId == projectId)
.FetchMany(x => x.Addresses)
.ToFuture();


Project p = p1.ToFuture().Single();
 

Explication détaillée ici: http://www.ienablemuch.com/2012/08/solving-nhibernate-thenfetchmany.html

2voto

Raffaeu Points 1097

Au lieu de vouloir récupérer plusieurs collections et d'obtenir un produit cartésien désagréable:

 Person expectedPerson = session.Query<Person>()
    .FetchMany(p => p.Phones)
        .ThenFetch(p => p.PhoneType)
    .FetchMany(p => p.Addresses)
    .Where(x => x.Id == person.Id)
    .ToList().First();
 

Vous devez regrouper des objets enfants dans un appel de base de données:

 // create the first query
var query = session.Query<Person>()
      .Where(x => x.Id == person.Id);
// batch the collections
query
   .FetchMany(x => x.Addresses)
   .ToFuture();
query
   .FetchMany(x => x.Phones)
   .ThenFetch(p => p.PhoneType)
   .ToFuture();
// execute the queries in one roundtrip
Person expectedPerson = query.ToFuture().ToList().First();
 

Je viens d'écrire un blog à ce sujet qui explique comment éviter cela en utilisant Linq, QueryOver ou HQL http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies.aspx

1voto

Konstantin Points 1118

J'ai juste voulu contribuer à la vraiment utile de répondre par Florian. J'ai découvert la manière dure que la clé de tout cela est sont les alias. Les alias détermine ce qui se passe dans le sql et sont utilisés comme des "identificateurs" par NHibernate. Le minimum Queryover charger avec succès un trois niveau de l'objet graphique est ceci:

Project pAlias = null;
Partner paAlias = null;

IEnumerable<Project> x = session.QueryOver<Project>(() => pAlias)
 .Where(p => p.Id == projectId)
 .Left.JoinAlias(() => pAlias.Partners, () => paAlias)
 .Future<Project>();


session.QueryOver(() => paAlias).Fetch(partner => partner.Costs).
 .Where(partner => partner.Project.Id == projectId)
 .Future<Partner>();

La première requête des charges du projet et de ses partenaires. L'important, c'est l'alias de Partenaire. Le partenaire alias est utilisé pour le nom de la deuxième requête. La deuxième requête en charge des partenaires et des coûts. Lorsque cette exécutée comme un "Multiquery", Nhibernate sera "sais" que la première et la deuxième requête sont connectés par la paAlias (ou plutôt le sql généré aura des alias de colonnes qui sont "identiques"). Ainsi, la deuxième requête va continuer le chargement des Partenaires qui a déjà commencé dans la première requête.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X