NHibernate выбирает верхний N с условием для детей, использующих выборку

У меня есть простая сущность пользователя:

public class User
{
    public virtual int Id { get; set; }
    public virtual DateTime CreationDate { get; set; }
    public virtual DateTime ModifiedDate { get; set; }

    public virtual string Email { get; set; }
    public virtual string Name { get; set; }

    public virtual IList<Phone> Phones { get; set; }
}

public class Phone
{
    public virtual string CountryCode { get; set; }
    public virtual string Code { get; set; }
    public virtual string Number { get; set; }
    public virtual string Comment { get; set; }
}

Мои сопоставления определены следующим образом:

public class UserMap : ClassMap<User>
{
    public UserMap ()
    {
        this.Table ("Users");

        this.Id (x => x.Id).CustomSqlType ("bigint").GeneratedBy.HiLo ("1000");
        this.Map (x => x.CreationDate);
        this.Map (x => x.ModifiedDate).Column ("LastUpdatedDate");
        this.Map (x => x.Email).Length (255).Not.Nullable ().Unique ();
        this.Map (x => x.Name).Column ("UserName").Length (255);

        this.HasMany (x => x.Phones).Inverse ();
    }
}

public class PhoneMap : ClassMap<Phone>
{
    public PhoneMap ()
    {
        this.Table ("Phones");

        this.Id ().GeneratedBy.Identity ();
        this.Map (x => x.CountryCode).Length (5);
        this.Map (x => x.Code).Length (10);
        this.Map (x => x.Number).Length (50).Not.Nullable ();
        this.Map (x => x.Comment).Length (255);
    }
}

Дополнительные соглашения здесь:

PrimaryKey.Name.Is (x => "Id"),
ForeignKey.EndsWith ("Id"),
DefaultAccess.Property (),
DefaultCascade.All ()

Мне нужно выбрать 100 лучших пользователей с телефонами и именами, начинающимися с буквы "А". Но мне нужно загрузить пользовательские объекты с телефонами в них.

Итак, я делаю этот запрос:

var users =
(
    from user in session.Query<User> ()
    where
        user.Name.StartsWith ("a")
        &&
        user.Phones.Any ()
    select user
)
    .Fetch (x => x.Phones)
    .Take (100)
    .ToArray ();

А у меня всего 72 пользователя.

Почему? Ну, потому что NHibernate генерирует один выбор TOP N с левым внешним соединением, а SQL возвращает несколько записей для одного и того же объекта пользователя, потому что у некоторых пользователей есть более одного телефона. Но все это идет в счет TOP N — так что я получаю 100 записей о пользователях, присоединенных к телефонам, но только 72 из них являются уникальными объектами.

Есть ли правильный способ сделать это?


person Michael Logutov    schedule 23.05.2012    source источник


Ответы (3)


Вы должны разделить запросы на подзапросы. Где внутренний подвыбор должен выполнять разбиение на страницы, а внешний - выборку:

var top100users =
(
    from user in session.Query<User>()
    where user.Name.StartsWith("a") &&
          user.Phones.Any()
    select user
)
.Take(100);

var users =
(
    from user in session.Query<User>()
    where top100users.Contains(user)
    select user
)
.Fetch (x => x.Phones)
.ToArray();

И это сгенерирует одиночный SQL-запрос, который будет вести себя так, как вы ожидаете.

person hazzik    schedule 29.05.2012
comment
System.InvalidOperationException: последовательность содержит более одного элемента - person Michael Logutov; 29.05.2012
comment
@FuriCuri, какую версию NHibernate вы используете? - person hazzik; 29.05.2012
comment
Ах я вижу. Он будет работать в NH 3.3.1GA. Для этого nhibernate.jira.com/browse/NH-3147 возникает проблема с JIRA. - person hazzik; 29.05.2012
comment
System.InvalidOperationException: последовательность содержит более одного элемента ‹package id=Iesi.Collections version=3.2.0.4000 /› ‹package id=NHibernate version=3.3.0.4000 /› ‹package id=FluentNHibernate version=1.3.0.727 /› Проба в .NET 3.5 и 4.0. - person Michael Logutov; 30.05.2012
comment
@FuriCuri NH3.3.1 еще не выпущен. - person hazzik; 30.05.2012
comment
Извините, мой плохой - думал, что вы говорите о последней публичной сборке. Буду ждать нового и протестировать его, прежде чем принять ваш ответ. - person Michael Logutov; 30.05.2012
comment
Последняя версия NHibernate 3.3.1.4000 отлично работает с вашим решением. Я отмечу ваш пост как ответ. Спасибо. - person Michael Logutov; 12.06.2012

Ну, единственный возможный обходной путь, который я придумал, - это сначала удалить Fetch из запроса, чтобы он стал таким:

var users =
    (
        from user in session.Query<User> ()
        where
            user.Name.StartsWith (prefix)
            &&
            user.Phones.Any ()
        select user
    )
    .Take (100)
    .ToList ();

Затем после этого кода добавьте что-то вроде этого, которое заставит загрузить хотя бы один объект:

users.ForEach (x => x.Phones.Any ());

И в сопоставлениях установите размер пакета равным 100 (или не менее 50):

public class UserMap : ClassMap<User>
{
    public UserMap ()
    {
        this.Table ("Users");

        this.Id (x => x.Id).CustomSqlType ("bigint").GeneratedBy.HiLo ("1000");
        this.Map (x => x.CreationDate);
        this.Map (x => x.ModifiedDate).Column ("LastUpdatedDate");
        this.Map (x => x.Email).Length (255).Not.Nullable ().Unique ();
        this.Map (x => x.Name).Column ("UserName").Length (255);

        this.HasMany (x => x.Phones).Inverse ().BatchSize (50);
    }
}

Или с помощью соглашений (хотя это может быть не так изящно для некоторых систем):

PrimaryKey.Name.Is (x => "Id"),
ForeignKey.EndsWith ("Id"),
DefaultAccess.Property (),
DefaultCascade.All (),
DynamicUpdate.AlwaysTrue (),
new CollectionConventionBuilder ().Always (x => x.BatchSize (50))

Кстати, на чистом SQL задача решается довольно просто с "для xml":

select top 100
    u.Id,
    u.CreationDate,
    u.LastUpdatedDate,
    u.Email,
    u.UserName,
    (
        select
            p.CountryCode,
            p.Code,
            p.Number,
            p.Comment
        from
            dbo.Phones as p
        where
            p.UserId = u.Id
        for xml path ('Phone'), root ('Phones'), type
    ) as '*'
from
    dbo.Users as u
where
    u.UserName like @0
    and
    exists (select top 1 p.Id from dbo.Phones as p where p.UserId = u.Id)
for xml path ('User'), root ('Root'), type

Я бы хотел, чтобы NHibernate мог загружать совокупные корни из запросов «для xml», когда это необходимо.

person Michael Logutov    schedule 25.05.2012

Вам нужно использовать подзапрос (для пейджинга) и преобразователь, чтобы получить отдельных пользователей, я не уверен, возможно ли это в провайдере NHibernate Linq, поэтому сделайте это с помощью QueryOver:

var sub_query = QueryOver.Of<User>() 
    .Where (Restrictions.On<User>(x => x.Name).IsLike("a%")) 
    .JoinQueryOver(x => x.Phones, JoinType.InnerJoin) 
    .Take (100)
    .Select(x => x.Id);

var users = session.QueryOver<User>() 
    .WithSubquery.WhereProperty (x => x.Id).In (sub_query) 
    .Fetch (x => x.Phones).Eager
    .TransformUsing (Transformers.DistinctRootEntity) 
    .List ();
person Martin Ernst    schedule 23.05.2012
comment
Он не компилируется в NHibernate 3.3 (по крайней мере, без дополнительных расширений). Я изменил его на это: ‹code›var sub_query = QueryOver.Of‹User› () .Where (Restrictions.On‹User› (x =› x.Name).IsLike (a%)) .JoinQueryOver (x =› x.Phones, JoinType.InnerJoin) .Take (100); var users = session.QueryOver‹User› () .WithSubquery.WhereProperty (x =› x.Id).In (sub_query) .Fetch (x =› x.Phones).Eager.TransformUsing (Transformers.DistinctRootEntity) .List ( );‹/code› И теперь я получил исключение: ‹code›NHibernate.QueryException: невозможно использовать подзапросы по критерию без проекции.‹/code› - person Michael Logutov; 23.05.2012
comment
Подзапросу просто нужен Select(x => x.Id) - я соответственно обновил код - person Martin Ernst; 23.05.2012
comment
Кажется, я не могу найти метод расширения Select. В каком пространстве имен он находится? - person Michael Logutov; 24.05.2012
comment
var sub_query = QueryOver.Of<User> () .Where (Restrictions.On<User> (x => x.Name).IsLike ("a%")) .JoinQueryOver (x => x.Phones, JoinType.InnerJoin) .Select (x => x.Id) .Take (100); var users = session.QueryOver<User> () .WithSubquery.WhereProperty (x => x.Id).In (sub_query) .Fetch (x => x.Phones).Eager.TransformUsing (Transformers.DistinctRootEntity) .List (); Генерирует тот же SQL, что и LINQ в моем вступительном сообщении. - person Michael Logutov; 24.05.2012