การใช้ LINQ ในการจัดการข้อมูลอย่างง่าย สำหรับมือใหม่(Ep.2)

          ความเดิมตอนที่แล้ว… ผู้เขียนได้ทิ้งท้ายไว้เกี่ยวกับเรื่องการใช้งาน LINQ ในการจัดการข้อมูลในเบื้องต้น ได้แก่ วิธีการดึงข้อมูลโดยทั่วไป(Select) การดึงข้อมูลแบบมีเงื่อนไข(Where) และการเรียงลำดับ(OrderBy) เป็นต้น หากใครที่ยังไม่เคยอ่านบทความที่แล้ว และต้องการศึกษาในส่วนดังกล่าวสามารถหาอ่านได้จากลิงค์ “การใช้ LINQ ในการจัดการข้อมูลอย่างง่าย สำหรับมือใหม่(Ep.1)” เพื่อเพิ่มความเข้าใจพื้นฐานในการใช้งานเบื้องต้น LINQ เพิ่มเติม และสำหรับในบทความนี้ ผู้เขียนจะขอพูดถึงการใช้งาน LINQ ในส่วนอื่นๆที่นอกเหนือจากการทำงานทั่วไป ที่คิดว่าน่าจะเป็นประโยชน์กับผู้พัฒนาที่มีความสนใจในการใช้งาน LINQ จัดการข้อมูล ดังนี้

  • การคำนวณค่าร่วม/นับจำนวน

ตัวอย่างที่ 1 : การคำนวณค่าผลรวมของฟิลด์ที่ดึงข้อมูลโดยใช้เมธอด Sum

decimal sumLineTotal = (from od in orderdetailscollection
select od.LineTotal).Sum();

หรือ

decimal sumLineTotal = orderdetailscollection.Sum(od => od.LineTotal);

คำอธิบาย : จากตัวอย่างข้างต้น เป็นการดึงข้อมูลโดยมีการคำนวณค่าผลรวมที่ได้จากการดึงข้อมูลทั้งหมดในฟิลด์ LineTotal ผ่านเมธอด Sum โดยที่ไม่ต้องมาวนค่าเพื่อหาผลรวมของแต่ละฟิลด์ที่ดึงมาอีกครั้งในภายหลัง ซึ่งถือว่าเป็นการอำนวยความสะดวกและประหยัดเวลาในการพัฒนาให้กับผู้พัฒนาที่ต้องการทำงานในกรณีดังกล่าวได้

ตัวอย่างที่ 2 : เป็นการคำนวณค่าเฉลี่ยตามรหัส

double RatingAverage = ctx.Rates.Where(r => r.Id == Id).Average(r => r.Rating);

หรือ

var RatingAverage = (from a in ctx.Rates where a. Id.Equals(id)
select a.Rating).Average();

คำอธิบาย : จากตัวอย่างข้างต้น เป็นการคำนวณหาค่าเฉลี่ยของฟิลด์ Rating โดยใช้เมธอด Average ภายใต้เงื่อนไขรหัส Id ในการดึงข้อมูล

ตัวอย่างที่ 3 : เป็นตัวอย่างการคำนวณผลรวม และการนับจำนวนแถวของการอ่านข้อมูลโดยมีการจัดกลุ่มข้อมูลร่วมด้วย

var ListByOwner = list.GroupBy(l => l.Owner)
.Select(lg => new {
Owner = lg.Key,
Boxes = lg.Count(),
TotalWeight = lg.Sum(w => w.Weight),
AverageVolume = lg.Average(w => w.Volume)
});

คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นได้ว่าเป็นการจัดกลุ่มของข้อมูลตาม Owner โดยใช้เมธอด GroupBy และมีการนับจำนวนแถวเก็บไว้ในฟิลด์ Boxes คำนวณผลรวมของคอลัมน์ Weight และหาค่าเฉลี่ยของคอลัมน์ Volume ใส่ในฟิลด์ TotalWeight และ AverageVolume นั่นเอง

ตัวอย่างที่ 4 : เป็นการนับจำนวนข้อมูลตามเงื่อนไขที่กำหนดโดยใช้เมธอด Count

int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };

int oddNumbers = numbers.Count(n => n % 2 == 1);

คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นว่า เป็นการดึงค่าจากตัวแปรอาร์เรย์ที่ชื่อว่า numbers และนับจำนวนตัวเลขเฉพาะที่เป็นเลขคี่(หารด้วยสองและมีค่าเศษ 1)เท่านั้นด้วยเมธอด Count ซึ่งในส่วนของเงื่อนไขดังกล่าว ผู้อ่านสามารถนำไปประยุกต์ใช้กับงานของตนในการนับจำนวนข้อมูลได้

ผลลัพธ์ : จากการอ่านข้อมูลจากตัวแปร numbers ส่งผลให้ oddNumbers มีค่าเท่ากับ 5 นั่นคือในการดึงข้อมูลพบเลขที่มีค่าเป็นเลขคี่ดังนี้ 5,1,3,9,7

  • การค่าที่น้อยที่สุดและมากที่สุดโดยใช้เมธอด Min/Max

ตัวอย่างที่ 1 : การหาค่าที่น้อยที่สุดโดยใช้เมธอด Min

string[] words = { "cherry", "apple", "blueberry" };

int shortestWord = words.Min(w => w.Length);

คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นว่า เป็นการอ่านค่าจากตัวแปรอาร์เรย์ที่ชื่อว่า words และหาค่าที่น้อยที่สุดของจำนวนความยาวตัวอักษรจากค่าที่อ่านได้ด้วยเมธอด Min

ผลลัพธ์shortestWord มีค่าเท่ากับ 5 ซึ่งก็คือค่าความยาวตัวอักษรของคำว่า “apple” ที่มีค่าเท่ากับ 5 ซึ่งเป็นค่าที่น้อยที่สุดนั่นเอง

ตัวอย่างที่ 2 : เป็นการหาราคาที่ถูกที่สุดจากรายการสินค้าที่อ่านได้แต่ละประเภท(จัดกลุ่มตามประเภทสินค้า) โดยใช้เมธอด Min

List<Product> products = GetProductList();
var categories =
from p in products
group p by p.Category into g
select new { Category = g.Key, CheapestPrice = g.Min(p => p.UnitPrice) };

คำอธิบาย : จากตัวอย่างข้างต้น เป็นการหาค่าที่น้อยที่สุดของราคาในคอลัมน์ UnitPrice จาก list ของคลาส Product ที่ดึงมาได้ และมีการจัดกลุ่มตามประเภทของสินค้า นั่นหมายถึง ค่าที่ได้จากการอ่านข้อมูลนี้จะเป็นค่าของราคาสินค้าต่อหน่วยที่น้อยที่สุดในแต่ละประเภทสินค้านั่นเอง

หมายเหตุ : การใช้งานเมธอด Max จะมีลักษณะการทำงานเช่นเดียวกับเมธอด Min แต่เป็นการหาค่าที่มากที่สุดจากการอ่านข้อมูล ซึ่งผู้อ่านสามารถนำไปใช้แทนกันได้ตามตัวอย่างการใช้งานเมธอด Min ข้างต้นได้

  • การใช้งาน set operator
    • การกำจัดข้อมูลที่ซ้ำกัน

    ตัวอย่างที่ 1 : เป็นการจำกัดข้อมูลที่ซ้ำกันด้วยเมธอด Distinct/DistinctBy

    List<Product> products = GetProductList();
    var categoryNames = (
    from p in products
    select p.Category).Distinct();
    

    หรือ 

    List<Product> products = GetProductList();
    var categoryNames = products.DistinctBy(x=> x.Category);
    

    คำอธิบาย : เป็นการดึงข้อมูลประเภทสินค้าที่ไม่ซ้ำกันไว้ในตัวแปร categoryNames โดยใช้เมธอด Distinct และ DistinctBy (ที่เรียกใช้จาก MoreLinq Library)
    หมายเหตุ : หากต้องการกำจัดข้อมูลที่ซ้ำกันมากกว่า 1 คอลัมน์ สามารถทำได้ ดังนี้

    List<Product> products = GetProductList();
    var categoryNames = products.DistinctBy(a => new { a.Category, a.Code });
    

    ตัวอย่างที่ 2 :

    List<Product> products = GetProductList();
    var categoryNames = products
     .GroupBy(a => a.Category )
     .Select(g => g.First());
    

    คำอธิบาย : เป็นการดึงข้อมูลประเภทสินค้าที่ไม่ซ้ำกันไว้ โดยใช้เมธอดการจัดกลุ่มข้อมูลตามประเภทสินค้าด้วยเมธอด GroupBy และดึงค่าแรกสุดของแต่ละกลุ่มมาแสดงด้วยเมธอด First ตามลำดับ

    ตัวอย่างที่ 3 :

    var total = items.Select(item => item.Value).Distinct().Count();
    

    คำอธิบาย : เป็นการนับจำนวนของข้อมูลที่ไม่ซ้ำกันโดยใช้เมธอด Distinct และเมธอด Count

    • การผสานข้อมูลโดยใช้ Union

    ตัวอย่างที่ 1 :

    int[] numbersA = { 0, 2, 4, 5, 6, 8, 9 }; 
    int[] numbersB = { 1, 3, 5, 7, 8 }; 
    var uniqueNumbers = numbersA.Union(numbersB); 
    

    คำอธิบาย : เป็นการรวมข้อมูลจาก 2 แหล่ง ด้วยเมธอด Union
    ผลลัพธ์ : uniqueNumbers = {0, 2, 4, 5, 6, 8, 9,1, 3, 7, 8}

    • การเลือกเฉพาะข้อมูลที่ซ้ำกันจากข้อมูล 2 แหล่งมาแสดงด้วยเมธอด Intersect

    ตัวอย่างที่ 1 :

    var infoQuery =
     (from cust in db.Customers
     select cust.Country)
     .Intersect
     (from emp in db.Employees
     select emp.Country);
    

    คำอธิบาย : เป็นการอ่านข้อมูลจาก 2 ส่วน คือ ในตาราง Customers และ Employees เพื่อเปรียบเทียบค่าคอลัมน์ Country จาก 2 แหล่ง หากมีซ้ำกันทั้ง 2 ที่จะดึงมาเก็บไว้ในตัวแปร infoQuery

    ตัวอย่างที่ 2 :

     int[] numbersA = { 0, 2, 4, 5, 6, 8, 9 }; 
     int[] numbersB = { 1, 3, 5, 7, 8 }; 
     
     var commonNumbers = numbersA.Intersect(numbersB); 
    

    คำอธิบาย : เป็นการอ่านข้อมูลจาก 2 ส่วน คือ ในตัวแปรอาร์เรย์ numbersA และ numbersB เพื่อเปรียบเทียบค่า หากมีซ้ำกันทั้ง 2 ที่จะดึงมาเก็บไว้ในตัวแปร commonNumbers
    ผลลัพธ์ : commonNumbers = { 5, 8}

    • การยกเว้นการอ่านข้อมูลโดยใช้ Except

    ตัวอย่างที่ 1 :

     List<Product> products = GetProductList(); 
     List<Customer> customers = GetCustomerList(); 
     
     var productFirstChars = 
     from p in products 
     select p.ProductName[0]; ///ดึงอักษรตัวแรกของชื่อสินค้า
     var customerFirstChars = 
     from c in customers 
     select c.CompanyName[0]; ///ดึงอักษรตัวแรกของชื่อบริษัท
     
     var productOnlyFirstChars = productFirstChars.Except(customerFirstChars); 
    

    คำอธิบาย : เป็นการอ่านข้อมูลจาก 2 ส่วน คือ ในส่วนของลิสต์ products และ customers โดยเอาเฉพาะอักษรตัวแรกของข้อมูลมาเพื่อเปรียบเทียบค่าโดยเทียบจาก productFirstChars หากพบว่าซ้ำกับในตัวอักษรแรกของข้อมูลชื่อบริษัทที่เก็บไว้ในตัวแปร customerFirstChars จะข้ามไป ไม่ดึงมาเก็บไว้ในตัวแปร productOnlyFirstChars แต่หากไม่ซ้ำ จะนำมาเก็บไว้ ผลลัพธ์สุดท้ายที่ได้จึงจะมีเฉพาะที่มีใน productFirstChars และไม่ซ้ำกับในข้อมูล customerFirstChars เท่านั้น
    ตัวอย่างที่ 2 :

     int[] numbersA = { 0, 2, 4, 5, 6, 8, 9 }; 
     int[] numbersB = { 1, 3, 5, 7, 8 }; 
     
     IEnumerable<int> aOnlyNumbers = numbersA.Except(numbersB); 
    

    คำอธิบาย : เป็นการอ่านข้อมูลจาก 2 ส่วน คือ ในตัวแปรอาร์เรย์ numbersA และ numbersB เพื่อเปรียบเทียบค่าโดยเทียบจาก numbersA เทียบกับ numbersB จะเอาเฉพาะที่มีใน numbersB แต่ไม่มีใน numbersB มาเก็บไว้ในตัวแปร aOnlyNumbers
    ผลลัพธ์ : aOnlyNumbers = { 0, 2, 4, 6, 9}

    • การอ่านข้อมูลจาก 2 แหล่งข้อมูลด้วยวิธีการ join
      • การ join แบบ innerjoin

      ตัวอย่างที่ 1 : เป็นการเชื่อมตาราง 2 ตารางโดยจะเอาเฉพาะข้อมูลแถวที่มี key ร่วมกันจากทั้ง 2 แหล่ง

      var result = from p in Person.BuiltPersons()
       join a in Address.BuiltAddresses() ///ระบุตารางที่นำมาเชื่อมกัน
       on p.IdAddress equals a.IdAddress ///กำหนด key ร่วมจาก 2 ตาราง
       select new 
       { ///เลือกคอลัมน์ที่ต้องการนำมาใช้
       Name = a.MyPerson.Name,
       Age = a.MyPerson.Age,
       PersonIdAddress = a.MyPerson.IdAddress,
       AddressIdAddress = a.MyAddress.IdAddress,
       Street = a.MyAddress.Street
       }; 
      

      คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นเป็นการเชื่อมตาราง 2 ตารางเข้าด้วยกัน และเลือกเฉพาะข้อมูลที่มีรหัส IdAddress ตรงกันมาแสดงเท่านั้น

      หรือ 

      var resultJoint = Person.BuiltPersons().Join( /// Source Collection
       Address.BuiltAddresses(), /// Inner Collection
       p => p.IdAddress, /// กำหนด key จากตาราง Person.BuiltPersons()
       a => a.IdAddress, /// กำหนด key จากตาราง Address.BuiltAddresses()
       (p, a) => new { MyPerson = p, MyAddress = a }) /// Result Collection
       .Select(a => new
       {
       Name = a.MyPerson.Name,
       Age = a.MyPerson.Age,
       PersonIdAddress = a.MyPerson.IdAddress,
       AddressIdAddress = a.MyAddress.IdAddress,
       Street = a.MyAddress.Street
       });
      • การ join แบบ leftjoin

      ตัวอย่างที่ 1 :

      string[] categories = new string[]{ 
       "Beverages", 
       "Condiments", 
       "Vegetables", 
       "Dairy Products", 
       "Seafood" }; 
        List<Product> products = GetProductList(); 
        
       var q = 
       from c in categories 
       join p in products on c equals p.Category into ps 
       from p in ps.DefaultIfEmpty() 
      ///ถ้าไม่มีข้อมูลจะแสดงข้อความ (No products)
       select new { Category = c, ProductName = p == null ? "(No products)" : p.ProductName }; 
      

      คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นได้ว่าเป็นการเชื่อมข้อมูล 2 แหล่ง โดยยึดตามแหล่งข้อมูลแรก ในที่นี้คือข้อมูล categories และเทียบกับข้อมูลใน products หากข้อมูลที่เปรียบเทียบมีใน categories แต่ไม่มีใน products ก็จะนำมาแสดง โดยให้ค่าของ ProductName เป็น (No products) แทน(ตามหลักการ join แบบ leftjoin ) หรืออธิบายง่ายๆว่า จะแสดงทุกรายการที่มีใน categories นั่นเอง

      • การ join แบบ rightjoin

      ตัวอย่างที่ 1 :

      var rightOuterJoin = from last in lastNames
       join first in firstNames
       on last.ID equals first.ID
       into temp
       from first in temp.DefaultIfEmpty(new { last.ID, Name = default(string) })
       select new
       {
       last.ID,
       FirstName = first.Name,
       LastName = last.Name,
       };
      

      คำอธิบาย : จากตัวอย่างข้างต้น จะเห็นได้ว่าเป็นการเชื่อมข้อมูล 2 แหล่ง โดยยึดตามแหล่งข้อมูลแรก ในที่นี้คือข้อมูล lastNames และเทียบกับข้อมูลใน firstNames โดยยึดข้อมูลจาก firstNames หากข้อมูลที่เปรียบเทียบมีใน firstNames แต่ไม่มีใน lastNames ก็จะนำมาแสดง โดยให้ค่าของ default(string) แทน(ตามหลักการ join แบบ rightjoin ) หรืออธิบายง่ายๆว่า จะแสดงทุกรายการที่มีใน firstNames นั่นเอง

              จะเห็นได้ว่าจากบทความการใช้งาน LINQ ในการจัดการข้อมูลอย่างง่าย สำหรับมือใหม่ ทั้งใน Ep.1 และ Ep.2 นี้ ผู้เขียนพยายามรวบรวมวิธีที่เป็นการใช้งานพื้นฐานและยกตัวอย่างในกรณีต่างๆ เพื่อให้ผู้อ่านเห็นภาพ และทำความเข้าใจได้โดยง่ายขึ้น สามารถนำไปประยุกต์ใช้กับงานพัฒนาที่กำลังพัฒนาอยู่ได้ แต่ความสามารถของ LINQ ไม่ได้จบเพียงเท่านี้ ยังคงมีเมธอดและวิธีการใช้งานอื่นๆ รวมทั้งการประยุกต์ใช้งานอีกมากมายรอให้ผู้อ่านได้ทำการศึกษาเพิ่มเติมและหยิบมาใช้งานกัน เพื่อเพิ่มประสิทธิภาพและความคล่องตัวในการพัฒนาโปรแกรมกันต่อไป ผู้เขียนเชื่อว่าหากผู้อ่านได้มีการศึกษาและฝึกลองทำหลายๆกรณีก็จะทำให้สามารถใช้งาน LINQ ได้เกิดประโยชน์สูงสุด สุดท้ายนี้ผู้เขียนหวังเป็นอย่างยิ่งว่าสิ่งที่ผู้เขียนได้หยิบยกมาให้ผู้อ่านได้อ่านกันนี้จะเป็นประโยชน์ไม่มากก็น้อย หากผิดพลาดประการใดผู้เขียนขออภัยไว้ ณ ที่นี้ด้วย และขอบคุณที่ติดตามอ่านบทความนี้จนจบนะคะ ขอบคุณค่ะ

    แหล่งข้อมูลอ้างอิง :
    https://code.msdn.microsoft.com/LINQ-Aggregate-Operators-c51b3869

    http://stackoverflow.com/questions/61870/sum-of-items-in-a-collection

    http://stackoverflow.com/questions/17983024/finding-average-of-value-based-on-count-of-rows-in-linq?rq=1

    https://code.msdn.microsoft.com/LINQ-Set-Operators-374f34fe

    http://www.codeproject.com/Articles/535374/DistinctBy-in-Linq-Find-Distinct-object-by-Propert
    https://code.msdn.microsoft.com/LINQ-Join-Operators-dabef4e9

    http://www.codeproject.com/Articles/488643/LinQ-Extended-Joins