エンティティフレームワークの左結合


84

このクエリを変更して、すべてのu.usergroupsを返すようにするにはどうすればよいですか?

from u in usergroups
from p in u.UsergroupPrices
select new UsergroupPricesList
{
UsergroupID = u.UsergroupID,
UsergroupName = u.UsergroupName,
Price = p.Price
};

1
多分これは助けることができます。ここSO
メナヘム2011

回答:


135

MSDNから採用、EF4を使用して左結合する方法

var query = from u in usergroups
            join p in UsergroupPrices on u.UsergroupID equals p.UsergroupID into gj
            from x in gj.DefaultIfEmpty()
            select new { 
                UsergroupID = u.UsergroupID,
                UsergroupName = u.UsergroupName,
                Price = (x == null ? String.Empty : x.Price) 
            };

2
ここでxを使用したり、選択したりできるので、最後のwhere gj.DefaultIfEmpty()よりもこれが好きです。
ゲイリー

1
'from x in gj.DefaultIfEmpty()'行を説明できますか?
Alex Dresko 2014年

@AlexDreskoこの部分は、結合からすべての結果を取得し、右側の値がないものについては、nullを返します(オブジェクトのデフォルトはnullです)。hth
メナヘム

2
3つ以上のテーブルがある場合はどうなりますか?
MohammadHosseinR19年

1
これはefcoreでわずかに変更されました。from x in gj.DefaultIfEmpty()になりfrom p in gj.DefaultIfEmpty()ます。docs.microsoft.com/en-us/ef/core/querying/...
carlin.scott

30

少しやり過ぎかもしれませんが、拡張メソッドを作成したのでLeftJoinJoin構文を使用して(少なくともメソッド呼び出し表記で)実行できます。

persons.LeftJoin(
    phoneNumbers,
    person => person.Id,
    phoneNumber => phoneNumber.PersonId,
    (person, phoneNumber) => new
        {
            Person = person,
            PhoneNumber = phoneNumber?.Number
        }
);

私のコードは、Aの追加より多くの何もしませんGroupJoinし、SelectMany現在の式ツリーへの呼び出しを。それでも、式を自分で作成し、ユーザーがresultSelectorパラメーターで指定した式ツリーを変更して、ツリー全体をLINQ-to-Entitiesで変換できるようにする必要があるため、かなり複雑に見えます。

public static class LeftJoinExtension
{
    public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        MethodInfo groupJoin = typeof (Queryable).GetMethods()
                                                 .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] GroupJoin[TOuter,TInner,TKey,TResult](System.Linq.IQueryable`1[TOuter], System.Collections.Generic.IEnumerable`1[TInner], System.Linq.Expressions.Expression`1[System.Func`2[TOuter,TKey]], System.Linq.Expressions.Expression`1[System.Func`2[TInner,TKey]], System.Linq.Expressions.Expression`1[System.Func`3[TOuter,System.Collections.Generic.IEnumerable`1[TInner],TResult]])")
                                                 .MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), typeof (LeftJoinIntermediate<TOuter, TInner>));
        MethodInfo selectMany = typeof (Queryable).GetMethods()
                                                  .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] SelectMany[TSource,TCollection,TResult](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Collections.Generic.IEnumerable`1[TCollection]]], System.Linq.Expressions.Expression`1[System.Func`3[TSource,TCollection,TResult]])")
                                                  .MakeGenericMethod(typeof (LeftJoinIntermediate<TOuter, TInner>), typeof (TInner), typeof (TResult));

        var groupJoinResultSelector = (Expression<Func<TOuter, IEnumerable<TInner>, LeftJoinIntermediate<TOuter, TInner>>>)
                                      ((oneOuter, manyInners) => new LeftJoinIntermediate<TOuter, TInner> {OneOuter = oneOuter, ManyInners = manyInners});

        MethodCallExpression exprGroupJoin = Expression.Call(groupJoin, outer.Expression, inner.Expression, outerKeySelector, innerKeySelector, groupJoinResultSelector);

        var selectManyCollectionSelector = (Expression<Func<LeftJoinIntermediate<TOuter, TInner>, IEnumerable<TInner>>>)
                                           (t => t.ManyInners.DefaultIfEmpty());

        ParameterExpression paramUser = resultSelector.Parameters.First();

        ParameterExpression paramNew = Expression.Parameter(typeof (LeftJoinIntermediate<TOuter, TInner>), "t");
        MemberExpression propExpr = Expression.Property(paramNew, "OneOuter");

        LambdaExpression selectManyResultSelector = Expression.Lambda(new Replacer(paramUser, propExpr).Visit(resultSelector.Body), paramNew, resultSelector.Parameters.Skip(1).First());

        MethodCallExpression exprSelectMany = Expression.Call(selectMany, exprGroupJoin, selectManyCollectionSelector, selectManyResultSelector);

        return outer.Provider.CreateQuery<TResult>(exprSelectMany);
    }

    private class LeftJoinIntermediate<TOuter, TInner>
    {
        public TOuter OneOuter { get; set; }
        public IEnumerable<TInner> ManyInners { get; set; }
    }

    private class Replacer : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParam;
        private readonly Expression _replacement;

        public Replacer(ParameterExpression oldParam, Expression replacement)
        {
            _oldParam = oldParam;
            _replacement = replacement;
        }

        public override Expression Visit(Expression exp)
        {
            if (exp == _oldParam)
            {
                return _replacement;
            }

            return base.Visit(exp);
        }
    }
}

2
この拡張フェロをありがとう。
ファーガーズ2016

これはまだ素晴らしいです。ありがとう!
TheGeekYouNeed

1
.NET Framework 4.6.2内でこれをテストし、期待どおりに機能します(つまり、LEFT OUTER JOINを生成します)。しかし、それが.NETCoreで動作するかどうか疑問に思っています。ありがとう。
アレクセイ

23

あなたの人生を楽にしてください(グループへの参加を使用しないでください):

var query = from ug in UserGroups
            from ugp in UserGroupPrices.Where(x => x.UserGroupId == ug.Id).DefaultIfEmpty()
            select new 
            { 
                UserGroupID = ug.UserGroupID,
                UserGroupName = ug.UserGroupName,
                Price = ugp != null ? ugp.Price : 0 //this is to handle nulls as even when Price is non-nullable prop it may come as null from SQL (result of Left Outer Join)
            };

2
グループへの参加を避けることは意見の問題ですが、それは確かに有効な意見です。Price = ugp.PricePricenull許容でないプロパティであり、左結合で結果が得られない場合は失敗する可能性があります。

1
上記に同意しますが、3つ以上のテーブルがある場合、このアプローチは非常に読みやすく、保守も簡単です。
Tomasz Skomra 2017年

1
ugp == NULLデフォルト値を確認して設定できますPrice
hp93 2018

完璧です:)
MohammadHosseinR19年

1
驚くばかり!私は読みやすさのためにこのソリューションを好みます。また、これにより、より多くの結合(つまり、3つ以上のテーブルから)がはるかに簡単になります!私はそれを2つの左結合(つまり3つのテーブル)にうまく使用しました。
ジェレミーモレン

4

メソッド呼び出し表記が必要な場合は、SelectManyと組み合わせて使用して左結合を強制できますDefaultIfEmpty。少なくとも、SQLServerにヒットするEntityFramework6では。例えば:

using(var ctx = new MyDatabaseContext())
{
    var data = ctx
    .MyTable1
    .SelectMany(a => ctx.MyTable2
      .Where(b => b.Id2 == a.Id1)
      .DefaultIfEmpty()
      .Select(b => new
      {
        a.Id1,
        a.Col1,
        Col2 = b == null ? (int?) null : b.Col2,
      }));
}

(これMyTable2.Col2はタイプの列であることに注意してくださいint)。生成されたSQLは次のようになります。

SELECT 
    [Extent1].[Id1] AS [Id1], 
    [Extent1].[Col1] AS [Col1], 
    CASE WHEN ([Extent2].[Col2] IS NULL) THEN CAST(NULL AS int) ELSE  CAST( [Extent2].[Col2] AS int) END AS [Col2]
    FROM  [dbo].[MyTable1] AS [Extent1]
    LEFT OUTER JOIN [dbo].[MyTable2] AS [Extent2] ON [Extent2].[Id2] = [Extent1].[Id1]

私にとって、これは「CROSSAPPLY」を含む非常に遅いクエリを生成しています。
Meekohi

2

2つ以上の左結合の場合(creatorUserとinitiatorUserの左結合)

IQueryable<CreateRequestModel> queryResult = from r in authContext.Requests
                                             join candidateUser in authContext.AuthUsers
                                             on r.CandidateId equals candidateUser.Id
                                             join creatorUser in authContext.AuthUsers
                                             on r.CreatorId equals creatorUser.Id into gj
                                             from x in gj.DefaultIfEmpty()
                                             join initiatorUser in authContext.AuthUsers
                                             on r.InitiatorId equals initiatorUser.Id into init
                                             from x1 in init.DefaultIfEmpty()

                                             where candidateUser.UserName.Equals(candidateUsername)
                                             select new CreateRequestModel
                                             {
                                                 UserName = candidateUser.UserName,
                                                 CreatorId = (x == null ? String.Empty : x.UserName),
                                                 InitiatorId = (x1 == null ? String.Empty : x1.UserName),
                                                 CandidateId = candidateUser.UserName
                                             };

1

メインモデルでDefaultIfEmpty()を呼び出すことで、これを行うことができました。これにより、遅延ロードされたエンティティでの左結合が可能になり、読みやすくなりました。

        var complaints = db.Complaints.DefaultIfEmpty()
            .Where(x => x.DateStage1Complete == null || x.DateStage2Complete == null)
            .OrderBy(x => x.DateEntered)
            .Select(x => new
            {
                ComplaintID = x.ComplaintID,
                CustomerName = x.Customer.Name,
                CustomerAddress = x.Customer.Address,
                MemberName = x.Member != null ? x.Member.Name: string.Empty,
                AllocationName = x.Allocation != null ? x.Allocation.Name: string.Empty,
                CategoryName = x.Category != null ? x.Category.Ssl_Name : string.Empty,
                Stage1Start = x.Stage1StartDate,
                Stage1Expiry = x.Stage1_ExpiryDate,
                Stage2Start = x.Stage2StartDate,
                Stage2Expiry = x.Stage2_ExpiryDate
            });

1
ここでは、まったく必要ありません。空の.DefaultIfEmpty()場合にのみ影響しdb.Complainsます。db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... })、がない場合.DefaultIfEmpty()、すでに左結合が実行Memberされます(プロパティがオプションとしてマークされていると仮定)。

1

UserGroupsがUserGroupPricesテーブルと1対多の関係を持っている場合、EFでは、関係が次のようなコードで定義されると、

//In UserGroups Model
public List<UserGroupPrices> UserGrpPriceList {get;set;}

//In UserGroupPrices model
public UserGroups UserGrps {get;set;}

左に結合された結果セットは、次の方法でプルできます。

var list = db.UserGroupDbSet.ToList();

左側のテーブルのDbSetがUserGroupDbSetであると仮定すると、これには、右側のテーブルの関連するすべてのレコードのリストであるUserGrpPriceListが含まれます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.