Борьба с декартовым произведением (x-join) при использовании NHibernate 3.0.0 - PullRequest
32 голосов
/ 11 марта 2011

Я плохо разбираюсь в математике, но я понимаю, что такое декартово произведение .
Вот моя ситуация (упрощенно):

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 строк
  • Если я лениво загружаю Partner.Costs, БД получает запрос со спамом (что немного быстрее, чем при первом подходе)

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

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

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

Ответы [ 4 ]

46 голосов
/ 13 марта 2011

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

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) х строк с партнерами и их затратами (и деньгами), где х - общее количество затрат для партнеров проекта 3) y строк с партнерами и их адресами, где y - общее количество адресов для партнеров проекта

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

5 голосов
/ 25 августа 2012

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

2 голосов
/ 04 июля 2014

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

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

1 голос
/ 07 февраля 2014

Я просто хотел внести свой вклад в действительно полезный ответ Флориана.Я обнаружил, что ключом ко всему этому являются псевдонимы.Псевдонимы определяют, что входит в sql и используются NHibernate в качестве «идентификаторов».Минимальный запрос для успешной загрузки трехуровневого графа объектов таков:

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 будет «знать», что первый и второй запросы связаны с помощью paAlias ​​(или, скорее, сгенерированные sqls будут иметь псевдонимы столбцов, которые «идентичны»).Таким образом, второй запрос продолжит загрузку партнеров, уже начатых в первом запросе.

...