Борьба с декартовым произведением (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;}
}

Моя цель - эффективно загрузить весь проект.

Проблема, конечно, в следующем:

  • Если я попытаюсь загрузить партнеров и их затраты, запрос вернет миллионы строк.
  • Если я лениво загружаю Partner.Costs, запрос db будет рассылаться спамом (что немного быстрее, чем первый подход)

Как я читал, обычным обходным путем является использование MultiQueries, но я просто не понимаю.
Итак, я надеюсь изучить этот точный пример.

Как эффективно загрузить проект целиком?

P.s. Я использую NHibernate 3.0.0.
Пожалуйста, не публикуйте ответы с использованием hql или строковых критериев api.


person Arnis Lapsa    schedule 10.03.2011    source источник
comment
Я не думаю, что здесь вы получите декартово произведение. Ваша структура - Project-1: n-Partner-1: n-PartnerCosts-1: 1-Money. Таким образом, количество строк, которые вы получите в своем результате, всегда будет учитываться (PartnerCosts). Вы получили бы декартово произведение, если бы у вас был другой IList ‹Something› в вашем классе Partner и вы попытались бы загрузить его в том же запросе. Тогда вы получите count (Something) * count (PartnerCosts). Поскольку вам не нужны ICriteria или HQL, лучшим вариантом будет QueryOver with Futures. Я напишу пример для этого позже и отправлю его в качестве ответа, если к тому времени никто больше этого не сделает.   -  person Florian Lim    schedule 11.03.2011
comment
@ Флориан, как я уже сказал, я плохо разбираюсь в математике. немного пересмотрел мое понимание и добавил Addresses для партнера. Использование QueryOver было бы идеальным.   -  person Arnis Lapsa    schedule 11.03.2011
comment
Пожалуйста помоги. У меня это не работает, и мне нужно посмотреть, как у вас дела .JoinAlias ​​(p = ›p.Project, () =› pAlias) Когда в классе проекта нет свойства для проекта ??? Были ли классы, которые вы использовали, точно такие же, как те, которые были опубликованы в вопросе? Как вообще p.Project компилируется?   -  person joncodo    schedule 12.01.2012
comment
@JonathanO Если в классе проекта нет свойства для проекта - сомневайтесь, что проект должен ссылаться на себя. Если вы имеете в виду, что партнеры не имеют отношения к проекту, то - я боюсь, что этот подход, борющийся с декартовым продуктом, не сработает. Но уж точно не помню, сомневаюсь, что смогу помочь. :)   -  person Arnis Lapsa    schedule 12.01.2012
comment
Присоединяйтесь ко мне в чате chat.stackoverflow.com/rooms/6667/question   -  person joncodo    schedule 12.01.2012


Ответы (4)


Хорошо, я написал для себя пример, отражающий вашу структуру, и это должно сработать:

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 - общее количество Адресов Партнеров проекта.

Ваша база данных должна возвращать строки 1 + x + y вместо строк x * y, которые были бы декартовым произведением. Я очень надеюсь, что ваша БД действительно поддерживает эту функцию.

person Florian Lim    schedule 12.03.2011
comment
Это похоже на чистое золото. Будет отмечен как ответ после проверки. Большое спасибо! :) - person Arnis Lapsa; 13.03.2011
comment
Спасибо за исправление примера Флориана - это мне очень помогло. - person David McClelland; 20.10.2011
comment
У меня это не работает, и мне нужно посмотреть, как у вас дела .JoinAlias ​​(p = ›p.Project, () =› pAlias) Когда в классе проекта нет свойства для проекта ??? Были ли классы, которые вы использовали, точно такие же, как те, которые были опубликованы в вопросе? Как вообще p.Project компилируется? - person joncodo; 12.01.2012
comment
@JonathanO Извините за поздний ответ. p.Project компилируется, потому что p.Project не является свойством класса Project, а свойством класса Partner. В выражении .JoinAlias(p => p.Project, () => pAlias) p ссылается на класс из .QueryOver<Partner>. - person Florian Lim; 17.01.2012
comment
Я тоже в замешательстве. Класс Partner имеет только два свойства: Costs и Addresses. Проект не является собственностью класса Партнер. - person Jacko; 25.02.2012
comment
@Jacko Этого нет в исходном вопросе, но в моем примере кода у меня была ссылка (в классе и в сопоставлении) с Partner на Project. Короче говоря, в классе Project есть public IList<Partner> Partners{get;set;}, а в классе Partner - public Project Project {get;set;}. Соответствующим образом выглядит файл сопоставления. - person Florian Lim; 28.02.2012
comment
@Florian Если есть условие, например, PartnerCosts.Total.Amount должно быть больше 0 долларов, куда бы вы его положили? В проектах запрос или все три? - person Dharmesh; 07.12.2013
comment
@Dharmesh Если ваша цель - загрузить проект с идентификатором x и только PartnerCosts с Amount больше 0, я бы попытался поместить это во второй запрос (партнеры), но не во все три. У меня больше нет этого тестового кода, поэтому я не могу его проверить. - person Florian Lim; 11.12.2013
comment
Я попробовал это сегодня, и есть необходимость в изменении (возможно, из-за изменений NHibernate, так как это было написано). Первый запрос должен загружать партнеров, иначе NHibernate выдаст дополнительный запрос var projects = repo.Session.QueryOver<Project>(() => pAlias) .Where(p => p.Id == projectId).Fetch(p=>p.Projects()).Eager.Future<Project>(); - person Konstantin; 06.02.2014
comment
@FlorianLim Если я хочу использовать запрос с пропуском и переходом к проектам, повлияет ли это на производительность - person Sajjad Ali Khan; 13.09.2017

Если вы используете 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

person Michael Buen    schedule 25.08.2012
comment
Это работает, но только потому, что у нас есть одна дочерняя коллекция вне проекта (и, следовательно, FetchMany (x = ›x.Partners) в порядке. Если есть вторая дочерняя коллекция (скажем, Contributers или что-то в этом роде), это все равно приведет к декартово произведение, так как вам нужно было бы вызвать FetchMany в Project для обеих дочерних коллекций. Похоже, NH LINQ недостаточно умен, чтобы привязать будущие запросы дочерней коллекции к корневому объекту, как, по-видимому, QueryOver. См.: stackoverflow.com/a/5435464/201308 - person kdawg; 22.02.2013
comment
@kdawg является ли Contributors дочерним элементом Project? или дитя Партнеров? Мне интересно воспроизвести это декартово произведение - person Michael Buen; 23.02.2013
comment
дочерний элемент Project. Собственно, я хочу еще раз вернуться к этому. Изначально у меня было что-то вроде sessions.Query ‹Project› () .FetchMany (x = ›x.Partners) .FetchMany (x =› x.Contributers) .ToFuture (), что, естественно, создавало декартово проект. С тех пор я прошел ускоренный курс по NH Futures и в итоге получил нечто очень похожее на то, что было у вас выше, но с API QueryOver. Первоначально я думал, что LINQ Futures не может делать то, что я хотел (аналогичная проблема), но теперь я уже не совсем уверен! знак равно - person kdawg; 23.02.2013
comment
Первый пример Linq, который я обнаружил, действительно работает. Спасибо!! - person Simon Fox; 01.05.2015

Вместо нетерпеливого поиска нескольких коллекций и получения неприятного декартова продукта:

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/

person Raffaeu    schedule 04.07.2014
comment
Изменено, обновите, пожалуйста, мой пост - person Raffaeu; 04.07.2014
comment
Отличный пост, я застрял с NHibernate 3.x в старом проекте (ради бога, мы используем Visual Studio 2008). Я даже не могу использовать Query с ToFuture из-за неприятной ошибки, вынужденной соответствовать QueryOver. Ваш пост спас мне день. - person Tiago César Oliveira; 05.07.2016

Я просто хотел внести свой вклад в действительно полезный ответ Флориана. Я на собственном горьком опыте выяснил, что ключом ко всему этому являются псевдонимы. Псевдонимы определяют, что входит в 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>();

Первый запрос загружает проект и его дочерние партнеры. Важная часть - это псевдоним для партнера. Псевдоним партнера используется для имени второго запроса. Второй запрос загружает партнеров и затраты. Когда это выполняется как «Мультизапрос», Nhibernate будет «знать», что первый и второй запросы связаны с помощью paAlias ​​(или, скорее, сгенерированные sqls будут иметь «идентичные» псевдонимы столбцов). Таким образом, второй запрос продолжит загрузку партнеров, которая уже была запущена в первом запросе.

person Konstantin    schedule 07.02.2014