3

私は単純な User エンティティを持っています:

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 ()

電話番号があり、名前が「A」で始まる上位 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 のみです。

それを行う適切な方法はありますか?

4

3 に答える 3

4

クエリをサブセレクトに分割する必要があります。内側の副選択がページネーションを実行し、外側がフェッチを実行する場所:

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 クエリが生成されます。

于 2012-05-29T07:02:17.320 に答える
1

さて、私が思いついた唯一の回避策は、最初にクエリから Fetch を削除することで、次のようになりました。

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

次に、そのコードの後に​​、少なくとも 1 つのエンティティを強制的にロードする次のようなものを追加します。

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 では、タスクは「for 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 が、"for xml" クエリからの集約ルートを、命令されたときにロードできたらいいのにと思います。

于 2012-05-25T07:13:27.110 に答える
0

個別のユーザーを取得するには、サブクエリ (ページング用) とトランスフォーマーを使用する必要があります。これが 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 ();
于 2012-05-23T16:19:53.917 に答える