Lucha contra el producto cartesiano (x-join) al usar NHibernate 3.0.0


Soy malo en matemáticas, pero tengo una idea de lo que producto cartesiano es.
Aquí está mi situación (simplificada):

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

Mi objetivo es cargar efectivamente todo el proyecto.

El problema por supuesto es:

  • Si trato de socios de carga ansiosos y sus costos, la consulta devuelve miles de filas
  • Si yo load perezoso Socio.Costos, db recibe solicitudes de spam (que es un poco más rápido que el primer enfoque)

Como he leído, la solución común es usar MultiQueries, pero no lo entiendo.
Así que espero aprender a través de este ejemplo exacto.

¿Cómo cargar efectivamente todo el proyecto?

P. S. Estoy usando NHibernate 3.0.0.
Por favor, no publique respuestas con hql o string formed criteria api approaches.

Author: Arnis Lapsa, 2011-03-11

4 answers

Ok, escribí un ejemplo para mí reflejando tu estructura y esto debería funcionar:

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();

Mis clases tenían nombres diferentes pero reflejaban exactamente la misma estructura. He reemplazado los nombres y espero que no haya errores tipográficos.

Explicación:

Los alias son necesarios para las uniones. He definido tres consultas para cargar el Project desea, el Partners con su Costs y el Partners con su Addresses. Al usar el .Futures() básicamente le digo a NHibernate que los ejecute en uno ida y vuelta en el momento en que realmente quiero los resultados, usando projects.ToList().

Esto resultará en tres sentencias SQL que de hecho se ejecutan en un viaje de ida y vuelta. Las tres declaraciones devolverán los siguientes resultados: 1) 1 fila con su Proyecto 2) x filas con los Socios y sus Costos (y el Dinero), donde x es el número total de Costos para los Socios del Proyecto 3) y filas con los Socios y sus Direcciones, donde y es el número total de Direcciones para el Proyecto Asociados

Su base de datos debe devolver 1+x+y filas, en lugar de x*y filas, que sería un producto cartesiano. Espero que su base de datos realmente soporte esa característica.

 45
Author: Florian Lim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-10-14 07:28:25

Si está utilizando Linq en su NHibernate, puede simplificar la prevención cartesiana con esto:

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();

Explicación Detallada aquí: http://www.ienablemuch.com/2012/08/solving-nhibernate-thenfetchmany.html

 5
Author: Michael Buen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-22 23:58:19

En lugar de buscar varias colecciones y obtener un producto cartesiano desagradable:

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();

Debería llamar por lotes a los objetos secundarios de una base de datos:

// 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();

Acabo de escribir una entrada de blog sobre ello que explica cómo evitar que el uso de Linq, QueryOver o HQL http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies.aspx

 2
Author: Raffaeu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-04 06:20:04

Solo quería contribuir a la respuesta realmente útil de Florian. Me enteré de la manera difícil que la clave de todo esto es son los alias. Los alias determinan lo que entra en el sql y son utilizados como "identificadores" por NHibernate. El mínimo Queryover para cargar con éxito un el gráfico de objetos de tres niveles es el siguiente:

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 primera consulta carga el proyecto y sus socios secundarios. La parte importante es el alias para el Socio. El alias de socio se utiliza para nombrar la segunda consulta. El la segunda consulta carga socios y costes. Cuando se ejecuta como una "Multiquery", Nhibernate "sabrá" que la primera y la segunda consulta están conectadas por los paAlias (o más bien los sql generados tendrán alias de columna que son "idénticos"). Por lo tanto, la segunda consulta continuará la carga de socios que ya se inició en la primera consulta.

 1
Author: Konstantin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-07 12:25:22