Join 運算子的第二篇文章,重點是驗證輸出序列的項目順序,以及怎麼做多重 Join。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
在前一篇「LINQ自學筆記-語法應用-聚合資料-Join-1」,我們提過 Join 運算子一個有趣的特性:
「5. 兩個序列 Join 時,會以 outer 中的項目索引順序加入輸出序列中,例如:outer 有 B、A、C,inner 是 A、B、C,最後 Join 結果是三個項目都要輸出,則預設是以 outer 的 B、A、C 順序輸出。」
現在我們準備來驗證這件事情,不過驗證之前,要先說明一個還沒分享過的運算子,也就是 Reverse 運算子:
public static IEnumerable<TSource> Reverse<TSource>(
this IEnumerable<TSource> source
)
Reverse 運算子非常單純,直接在序列上調用,它會把序列中所有項目反轉過來,請注意,是把索引順序反過來,例如:BCAD 會變成 DACB。Reverse 方法在很多集合中都有出現,包含 List、Array、ArrayList,用途都一樣,就是反轉索引值。在下面的範例中,我們因為要驗證輸出序列會保持 outer 中的順序,所以刻意把 Curstomers 這個 List<Customer> 反轉過來。請注意,使用的是 List<T>.Reverse,並非上述的擴充方法,但因為功能完全相同,所以就一併說明。
下面範例的資料來源 Customers、Orders 請參閱前一篇「LINQ自學筆記-語法應用-聚合資料-Join-1」文章的程式,不想用重覆的程式碼佔篇幅,敬請見諒:
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
var query1 =
from c in Customers
join o in Orders on c.ID equals o.CustomerID
select c.Name + " 買 " + o.Description;
query1.Dump("Standard Customers");
Customers.Reverse();
query1.Dump("Reverse Customers");
var query2 =
from o in Orders
join c in Customers on o.CustomerID equals c.ID
select c.Name + " 買 " + o.Description;
query2.Dump("Orders is outer");
}
上述程式碼,我們定義了兩個 LINQ 查詢,第一個查詢(query1)之 outer 是 Customers,inner 是 Orders,因為 Customers 和 Orders 中的項目之顧客編號都是從小排到大,所以一開始 Dump 查詢結果時,我們可以看到是顧客 Leo 排到最前面,接著我們對 Customers 序列調用 Reverse 方法,然後再 Dump 一次查詢結果,我們可以發現輸出的順序變成顧客 Emy 最先出現,顧客 Leo 排在最後。
接著我們再定義第二個 LINQ 查詢(query2),把 outer 變成 Orders,inner 是 Customers,因為 Orders 序列中的訂單顧客,預設是由 Leo 排到 Emy,所以雖然 Customers 已經執行 Reverse 變成 Emy 排最前面,但是 Join 運算子的特性是以 outer 的順序為優先,所以結果還是從 Leo 排到 Emy。故可證明 Join 運算子預設是以 outer 序列中項目的順序輸出。
本文第二個重點,就是我們一開始描述 Join 運算子時說過:「Join 運算子,可以讓我們把兩個來源序列聚合為一個輸出序列。」但是實務上,Join 三、四個 Table 的狀況挺常見,所以該如何透過 Join 運算子來處理這件事?
答案其實一點都不意外,就是連續調用多個 Join 運算子囉:
public class OrderItem
{
public int ID { get; set; }
public int OrderID { get; set; }
public string Detail { get; set; }
}
void Main()
{
var Customers = DataProvider.getCustomers();
var Orders = DataProvider.getOrders();
List<OrderItem> OrderItems = new List<OrderItem>
{
new OrderItem {ID = 1, OrderID = 4, Detail = "AAA"},
new OrderItem {ID = 2, OrderID = 4, Detail = "BBB"},
new OrderItem {ID = 3, OrderID = 2, Detail = "CCC"},
new OrderItem {ID = 3, OrderID = 2, Detail = "DDD"},
new OrderItem {ID = 3, OrderID = 5, Detail = "EEE"}
};
var query = from c in Customers
join o in Orders on c.ID equals o.CustomerID
join oi in OrderItems on o.ID equals oi.OrderID
select new
{
c.Name, o.Description, oi.Detail
};
query.Dump();
}
上述程式碼,我們為了要展示如何聯結第二組 Join 運算子,所以我們又定義了一個 OrderItem 類別,其中最重要的就是 OrderID 這個屬性,用來和 Order.ID 做對應。我們在程式碼中建立三個資料來源序列,分別是 Customers、Orders、OrderItems,然後用查詢表達式撰寫一個連續 Join 的語法,並指定輸出顧客名稱(c.Name)、訂單描述(o.Description)和訂單項目明細(oi.Detail),最後我們可以看到輸出結果如上圖所示。
請注意,上述程式除了展示多重 Join 的查詢表達式外,我刻意在建立 OrderItems 的項目資料時,把訂單編號打亂,但最後輸出結果,仍然是顧客 Leo 的訂單先顯示,再次呼應本文第一個重點,也就是輸出序列預設會以 outer 序列項目的順序來呈現。
多重 Join,若利用方法架構查詢撰寫,會繁雜許多,不建議使用,但還是把上述的查詢表達式翻譯為方法架構查詢供大家參考:
Customers
.Join (
Orders,
c => c.ID,
o => o.CustomerID,
(c, o) =>
new
{
c = c,
o = o
}
)
.Join (
OrderItems,
ret => ret.o.ID,
oi => oi.OrderID,
(ret, oi) =>
new
{
Name = ret.c.Name,
Description = ret.o.Description,
Detail = oi.Detail
}
)
簡單說明一下上述程式碼在幹嘛:
下一篇文章,我們將繼續說明 Join 運算子如何設定多個欄位來做相等聯結的比較,以及 GroupJoin 運算子。
PS. Join 第二個多載方法的應用,因為和先前 Distinct、Contains 運算子的第二個多載方法應用方式相同,不再贅述。