nhibernate eager-loading cartesian-product

Lucha contra el producto cartesiano(x-join) cuando se usa NHibernate 3.0.0



eager-loading cartesian-product (4)

En lugar de ansioso, busca varias colecciones y obtén un desagradable producto cartesiano:

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 lotear objetos secundarios en una llamada a la 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 publicación en el blog que explica cómo evitar eso usando Linq, QueryOver o HQL http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies.aspx

Soy malo en matemáticas, pero me gustaría tener una idea de qué es el producto cartesiano .
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 intento impactar a los socios de carga y sus costos, la consulta devuelve un montón de filas
  • Si cargo perezoso Partner.Costs, db recibe una solicitud de spam (que es un poco más rápido que el primer enfoque)

Mientras leo, la solución común es usar MultiQueries, pero me gusta simplemente no obtenerlo.
Así que espero aprender a través de este ejemplo exacto.

¿Cómo cargar efectivamente todo el proyecto?

Ps Estoy usando NHibernate 3.0.0.
Por favor, no publique respuestas con hql o string fashioned criteria api.


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 diferentes nombres pero reflejaban la misma estructura. Reemplacé los nombres y espero que no haya errores tipográficos.

Explicación:

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

Esto dará como resultado tres sentencias SQL que, de hecho, se ejecutan en una ida y vuelta. Las tres declaraciones arrojará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 la cantidad total de Costos para los Socios del Proyecto 3) y filas con el Socios y sus Direcciones, donde y es el número total de Direcciones para los Socios del Proyecto

Su db debería devolver 1 + x + y filas, en lugar de x * y filas, lo que sería un producto cartesiano. Espero que su DB realmente sea compatible con esa característica.


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


Solo quería contribuir con la respuesta realmente útil de Florian. Descubrí de la peor manera que la clave de todo esto son los alias. Los alias determinan qué entra en el sql y NHibernate los utiliza como "identificadores". El Queryover mínimo para cargar con éxito un gráfico de objetos de tres niveles es este:

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 de Partner. El alias de socio se usa para nombrar la segunda consulta. La segunda consulta carga socios y costos. Cuando esto 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 sqls generados tendrán alias de columna que son "idénticos"). Por lo tanto, la segunda consulta continuará la carga de Partners que ya se inició en la primera consulta.