Борьба с декартовым продуктом (x-join) при использовании NHibernate 3.0.0
Я плохо разбираюсь в математике, но я доволен тем, что декартовый продукт есть.
Вот моя ситуация (упрощенная):
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;}
}
Моя цель - эффективно загрузить весь проект.
Проблема, конечно, такова:
- Если я пытаюсь найти партнеров по загрузке и их затраты, запрос возвращает gazillion rows
- Если я ленивую загрузку Partner.Costs, db получает запрос спам (что немного быстрее, чем первый подход).
Как я читал, обычным обходным решением является использование MultiQueries, но я вроде как просто не понимаю.
Поэтому я надеюсь узнать на этом конкретном примере.
Как эффективно загрузить весь проект?
P.s. Я использую NHibernate 3.0.0.
Пожалуйста, не отправляйте ответы с hql или строковыми критериями api.
Ответы
Ответ 1
Хорошо, я написал пример для себя, отражающий вашу структуру, и это должно работать:
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();
Мои классы имели разные имена, но отражали ту же структуру. Я заменил имена, и я надеюсь, что нет опечаток.
Объяснение:
Алиасы необходимы для соединений. Я определил три запроса для загрузки Project
, которые вы хотите, Partners
со своими Costs
и Partners
с их Addresses
. Используя .Futures()
, я в основном говорю NHibernate, чтобы выполнить их в одном обратном направлении в тот момент, когда я действительно хочу получить результаты, используя projects.ToList()
.
Это приведет к появлению трех операторов SQL, которые действительно выполняются в одном обратном направлении. Три заявления вернут следующие результаты:
1) 1 строка с вашим проектом
2) x строк с Партнерами и их расходами (и деньгами), где x - общее количество затрат для партнеров по проекту
3) y строк с Партнерами и их Адресами, где y - общее количество Адресов для Партнеров проекта
Ваш db должен возвращать строки 1 + x + y вместо x * y строк, которые были бы декартовым произведением. Я надеюсь, что ваша БД действительно поддерживает эту функцию.
Ответ 2
Если вы используете Linq в своем NHibernate, вы можете упростить декартовое предупреждение с помощью этого:
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();
Подробное объяснение здесь: http://www.ienablemuch.com/2012/08/solving-nhibernate-thenfetchmany.html
Ответ 3
Вместо того, чтобы жаждать выборку нескольких коллекций и получить отвратительное декартово произведение:
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();
Вы должны размещать дочерние объекты в одном вызове базы данных:
// 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();
Я просто написал сообщение в блоге об этом, в котором объясняется, как избежать использования Linq, QueryOver или HQL
http://blog.raffaeu.com/archive/2014/07/04/nhibernate-fetch-strategies.aspx
Ответ 4
Я просто хотел внести свой вклад в действительно полезный ответ Флориана. Я обнаружил трудный путь
что ключом ко всему этому являются псевдонимы. Алиасы определяют, что входит в sql
и используются как "идентификаторы" NHibernate. Минимальный Queryover для успешной загрузки
трехуровневый граф объектов:
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>();
Первый запрос загружает проект и его дочерние партнеры. Важной частью является псевдоним для Партнера.
Алиас партнера используется для обозначения второго запроса. Второй запрос загружает партнеров и затраты.
Когда это выполняется как "Multiquery", Nhibernate "знает", что первый и второй запрос связаны
by paAlias (или, скорее, сгенерированные sqls будут иметь псевдонимы столбцов, которые являются "идентичными" ).
Таким образом, второй запрос продолжит загрузку партнеров, которые уже были запущены в первом запросе.