zoukankan      html  css  js  c++  java
  • LINQ之路13:LINQ Operators之连接(Joining)

    Joining

    IEnumerable<TOuter>, IEnumerable<TInner>→IEnumerable<TResult>

    Operator

    说明

    SQL语义

    Join

    应用一种查询策略来匹配两个集合中的元素,产生一个平展的结果集

    INNER JOIN

    GroupJoin

    同上,但是产生一个层次结果集

    INNER   JOIN,

    LEFT   OUTER JOIN

    Join & GroupJoin Arguments

    参数

    类型

    外层/Outer sequence

    IEnumerable<TOuter>

    内层/Inner sequence

    IEnumerable<TInner>

    外键选择器/Outer key selector

    TOuter => TKey

    内键选择器/Inner key selector

    TInner => TKey

    结果选择器/Result selector

    Join:      (TOuter,TInner) =>   TResult

    GroupJoin    (TOuter,IEnumerable<TInner>) => TResult

    查询表达式语法

                    from outer-var in outer-enumerable
    join inner-var in inner-enumerable on outer-key-expr equals inner-key-expr
    [ into identifier ]

    简介

    Join和GroupJoin通过匹配两个输入sequence来产生单个输出sequence。Join产生平展结果集,而GroupJoin产生层次结果集。Join和GroupJoin提供了Select和SelectMany的替代策略。

    Join和GroupJoin的优点是他们对于本地内存集合的执行更加有效,因为他们开始就把内层sequence装载到一个按键排序的查找器,这样就避免了重复的遍历每一个内层元素。他们的缺点则是他们只提供了inner和left out join的功能,而cross joins和不等连接non-equi joins还是只能通过Select/SelectMany来实现。

    对于LINQ to SQL和Entity Framework查询来讲,Join和GroupJoin并没有提供相对于Select和SelectMany的任何真正优化,因为他们只是生成相应的SQL语句,而执行是在数据库引擎中完成的。

    下表总结了 每种join策略的差异:

    表:连接策略

    策略

    结果形状

    本地查询效率

    Inner   joins

    Left   outer joins

    Cross   joins

    Nonequi   joins

    Select + SelectMany

    Flat

    Bad

    Yes

    Yes

    Yes

    Yes

    Select + Select

    Nested

    Bad

    Yes

    Yes

    Yes

    Yes

    Join

    Flat

    Good

    Yes

    -

    -

    -

    GroupJoin

    Nested

    Good

    Yes

    Yes

    -

    -

    GroupJoin + SelectMany

    Flat

    Good

    Yes

    Yes

    -

    -

    Join

    Join运算符执行一个inner join,产生一个平展的输出sequence。示范Join的最简单方式是使用LINQ to SQL,下面的示例列出所有的Customers和他们的Purchases:

                IQueryable<string> query =
    from c in dataContext.Customers
    join p in dataContext.Purchases on c.ID equals p.CustomerID
    select c.Name + " bought a " + p.Description;

    //Result:
    Tom bought a Bike
    Tom bought a Holiday
    Dick bought a Phone
    Harry bought a Car

    结果与我们使用SelectMany方式查询时是一致的。

    要看到Join相对于SelectMany的优势,我们必须先把查询转为本地查询,如下所示:

                //把所有customers和purchases拷贝到数组,以实现本地查询
    Customer[] customers = dataContext.Customers.ToArray();
    Purchase[] purchases = dataContext.Purchases.ToArray();

    var slowQuery = from c in customers
    from p in purchases
    where c.ID == p.CustomerID
    select c.Name + " bought a " + p.Description;
    var fastQuery = from c in customers
    join p in purchases on c.ID equals p.CustomerID
    select c.Name + " bought a " + p.Description;

    尽管上面两个查询产生相同的结果,但是使用join的查询快得多,因为它的Enumerable实现把内层collection预先装载到一个按键排序的查找器。

    join的查询语法如下:

            join inner-var in inner-sequence on outer-key-expr equals inner-key-expr

     LINQ中的Join运算符会区分对待outer sequence和inner sequence,语法上看:

    • Outer sequence是输入sequence (本例中是customers).
    • Inner sequence是我们引入的新集合 (本例中是purchases).

    Join执行内连接(inner joins),意味着没有任何Purchases的Customers会被排除在结果之外。对于inner joins,我们可以交换查询的inner和outer sequences并得到相同的结果:

                 var fastQuery = from p in purchases
    join c in customers on p.CustomerID equals c.ID
    select c.Name + " bought a " + p.Description;

    我们可以在查询中继续添加join子句。比如,如果每个purchase有一个或多个purchase items,我们可以使用如下查询来join purchase items:

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID // first join
    join pi in purchaseItems on p.ID equals pi.PurchaseID // second join
    ...

    purchases是第一个join的inner sequence和第二个join的outer sequence。我们可以通过嵌套的foreach来获得相同的结果(低性能):

                foreach (Customer c in customers)
    foreach (Purchase p in purchases)
    if (c.ID == p.CustomerID)
    foreach (PurchaseItem pi in purchaseItems)
    if (p.ID == pi.PurchaseID)
    Console.WriteLine(c.Name + "," + p.Price + "," + pi.Detail);

    在查询语法中,前一个join引入的变量会保持在作用域之内,就像SelectMany样式查询中的外部范围变量那样,我们还可以在join子句中间插入where和let子句。

    对多个键值进行Join

    我们可以使用匿名类型来对多个键值进行Join,如下所示:

                    from x in sequenceX
    join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
    equals new { K1 = y.Prop3, K2 = y.Prop4 }
    ...

    要使上面的查询正确执行,两个匿名类型的结构必须完全一致,这样编译器把他们对应到同一个实现类型,从而使连接键值彼此兼容。

    Join的方法语法

    下面的join查询语法

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID
    select new { c.Name, p.Description, p.Price };

    对应的方法语法如下:

                    customers.Join(         // outer collection
    purchases, // inner collection
    c => c.ID, // outer key selector
    p => p.CustomerID, // inner key selector
    (c, p) => new { c.Name, p.Description, p.Price } // result selector
    );

    结果选择器表达式为输出sequence创建每个element。如果我们需要在数据转换之前添加其他子句比如orderby:

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID
    orderby p.Price
    select c.Name + " bought a " + p.Description;

    那么在方法语法中,我们必须在结果选择器中手动构建一个临时的匿名类型,在该匿名类型中保存c和p:

                    customers.Join(         // outer collection
    purchases, // inner collection
    c => c.ID, // outer key selector
    p => p.CustomerID, // inner key selector
    (c, p) => new { c, p }) // result selector
    .OrderBy(x => x.p.Price)
    .Select(x => x.c.Name + " bought a " + x.p.Description);

    通常情况下,join的查询表达式语法更加简洁。

    GroupJoin

    GroupJoin和Join一样执行连接操作,但它不是返回一个平展的结果集,而是一个层次结构的结果集,使用每个外层element进行分组。除了inner joins,GroupJoin还允许outer joins。GroupJoin的查询语法也与Join相似,只是后面紧跟着into关键字。

                // Here’s the most basic example of GroupJoin:
    IEnumerable<IEnumerable<Purchase>> query =
    from c in customers
    join p in purchases on c.ID equals p.CustomerID
    into custPurchases
    select custPurchases; // custPurchases is a sequence

    直接出现在join子句之后的into关键字会被翻译为GroupJoin,而在select或group子句之后的into表示继续一个查询。虽然他们有一个共同的特征:都引入了一个新的查询变量,但是into关键字的这两种使用方式大不相同,必须引起注意。

    其结果是一个包含了多个sequences的sequence,我们可以通过如下方式进行遍历:

                foreach (IEnumerable<Purchase> purchaseSequence in query)
    foreach (Purchase p in purchaseSequence)
    Console.WriteLine(p.Description);

    但是这种方式不是非常有用,原因在于outer sequence并没有到outer customer的引用,所以purchaseSequence虽然是按customer进行分组的,但我们在结果中却失去了customer的相关信息。通常情况下,我们会在数据转换中添加对外部范围变量的引用:

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID
    into custPurchases
    select new { CustName = c.Name, custPurchases };

    这会得到和下面的Select子查询相同的结果(对于本地查询来说,Select效率不如join):

                    from c in customers
    select new
    {
    CustName = c.Name,
    custPurchases = purchases.Where(p => c.ID == p.CustomerID)
    };

    默认情况下,GroupJoin相当于left outer join。要得到inner join(排除没有任何purchases的customers),可以对custPurchases添加过滤条件:

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID
    into custPurchases
    where custPurchases.Any()
    select ...

    GroupJoin的into关键字之后的子句比如where针对subsequence,而不是单个的child elements。如果要对单独的purchases添加条件,必须在join之前调用Where:

                    from c in customers
    join p in purchases.Where(p2 => p2.Price > 1000)
    on c.ID equals p.CustomerID
    into custPurchases ...

    平展的外连接/Flat outer joins

    事情在我们希望得到一个outer join和平展的结果集时会陷入两难的境地。GroupJoin让我们获得outer join;Join给了我们平展的结果集。所以解决方案就是先调用GroupJoin,然后对每个child sequence使用DefaultIfEmpty,最后调用SelectMany来获取平展结果集:

                    from c in customers
    join p in purchases on c.ID equals p.CustomerID into custPurchases
    from cp in custPurchases.DefaultIfEmpty()
    select new
    {
    CustName = c.Name,
    Price = cp == null ? (decimal?)null : cp.Price
    };

    如果custPurchases为空,DefaultIfEmpty将产生一个null值。第二个from子句会被翻译成SelectMany。所以,它会平展输出所有的purchase subsequence,将他们合并到单一的输出sequence。

    使用lookups

    在Enumerable的实现中,Join和GroupJoin的工作分为两个步骤。首先,他们会把内层sequence装载到一个查找器,然后通过该查找器来查询外层sequence。一个查找器(lookup)是一个分组的sequence并可以通过key来直接访问。或者我们可以把它想象成一个dictionary,其中每个元素是一个sequence和对应的key。

    查找器是只读的并通过如下接口定义:

            public interface ILookup<TKey, TElement> :
    IEnumerable<IGrouping<TKey, TElement>>, IEnumerable
    {
    int Count { get; }
    bool Contains(TKey key);
    IEnumerable<TElement> this[TKey key] { get; }
    }

    在处理本地集合时,我们甚至可以手动创建和查询lookups来作为join运算符的替代策略。这么做有如下优点:

    • 我们可以在多个查询之间重用同一个查找器(lookup)
    • 对lookup进行查询可以让我们更好的理解Join和GroupJoin的工作方式

    ToLookup扩展方法创建一个lookup,下面的代码把所有的purchases装载到一个lookup(用CustomerID作为Key):

                ILookup<int?, Purchase> purchLookup =
    purchases.ToLookup(p => p.CustomerID, p => p);

    第一个参数选择键值,第二个参数选择作为value值被装载到lookup中的对象。读取一个lookup就像读取一个dictionary,不同之处在于索引器返回的是一个包含多个匹配元素的sequence。下面的代码遍历所有CustomerID为1的purchases:

                foreach (Purchase p in purchLookup[1])
    Console.WriteLine(p.Description);

    通过使用lookup,我们可以让SelectMany/Select查询运行得像Join/GroupJoin查询一样高效。Join相当于在lookup上使用SelectMany:

                    from c in customers
    from p in purchLookup[c.ID]
    select new { c.Name, p.Description, p.Price };

    通过添加DefaultIfEmpty可以让上面的查询变成一个outer join:

                    from c in customers
    from p in purchLookup[c.ID].DefaultIfEmpty()
    select new
    {
    c.Name,
    Descript = p == null ? null : p.Description,
    Price = p == null ? (decimal?)null : p.Price
    };

    GroupJoin等价于在数据转换时读取lookup:

                    from c in customers
    select new
    {
    CustName = c.Name,
    CustPurchases = purchLookup[c.ID]
    };

    Enumerable实现

    通过前面的介绍,现在我们可以来看看Join和GroupJoin在LINQ中的实现了。

    下面是Enumerable.Join实现的简化版本(没有null checking):

            public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, TInner, TResult> resultSelector)
    {
    ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
    return from outerItem in outer
    from innerItem in lookup[outerKeySelector(outerItem)]
    select resultSelector(outerItem, innerItem);
    }

    GroupJoin的实现与Join类似:

            public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,
    IEnumerable<TInner> inner,
    Func<TOuter, TKey> outerKeySelector,
    Func<TInner, TKey> innerKeySelector,
    Func<TOuter, IEnumerable<TInner>, TResult> resultSelector)
    {
    ILookup<TKey, TInner> lookup = inner.ToLookup(innerKeySelector);
    return from outerItem in outer
    select resultSelector(outerItem, lookup[outerKeySelector(outerItem)]);
    }
  • 相关阅读:
    Luogu P3275 糖果
    Python基础学习
    SharePoint 2013
    Office
    KnockoutJS
    SharePoint 2013
    Bootstrap
    SharePoint 2013
    CSS
    AngularJS
  • 原文地址:https://www.cnblogs.com/qixuejia/p/7284701.html
Copyright © 2011-2022 走看看