เพิ่มประสิทธิภาพการใช้ Linq to SQL ด้วย Load Option

การใช้ Linq to SQL เป็น Data Access Layer นั้นไม่เหมือนกับการเขียน SQL Query แบบเก่า ที่อยากจะ select ฟิลด์ของตารางที่ไป Join มาอย่างไรก็ได้ ตามแนวทางของ Linq to SQL นั้นเราจำเป็นต้องกำหนด Association ระหว่างแต่ละ Entity ให้ชัดเจน แล้วฟิลด์ที่เราจะนำมาแสดงผลก็มาจากการ Navigate ไปตาม Property ของ Entity Object ที่เป็น Association เหล่านี้

ตัวอย่างเช่น ถ้าเรามี dbml ลักษณะนี้
customer site
มีการกำหนด Association ระหว่าง Customer และ Site เป็นแบบหนึ่งต่อหลาย กล่าวคือหนึ่ง Customer มีได้หลาย Site
ถ้าเราลองไปดูในคลาส Site ที่มัน generate ออกมาให้จะเห็นว่ามี property ชื่อว่า Customer ไว้สำหรับอ้างถึง parent ของ Site นั้นๆ ได้
ในทางกลับกัน ถ้าเราไปดูในคลาส Customer จะเห็นว่ามี property ชื่อ Sites อยู่ เอาไว้สำหรับอ้างถึง Site ลูกๆ ของ Customer นั้นได้
ซึ่งชื่อของ property เหล่านี้เราสามารถกำหนดได้เอง โดยให้ไปคลิกที่เส้น assoication ระหว่างตาราง แล้วดู property
ก็จะเห็นว่ามีการกำหนด “Name” ของ property ที่จะใช้สำหรับ association นี้ดังรูป
customer site ass

จากโครงสร้างข้อมูลดังกล่าว สมมติใน Application เราต้องการ select ข้อมูลจาก Site ขึ้นมา
หากเราต้องการดึงค่าจาก parent ของมันหรือ Customer ก็สามารถเรียกหา property นี้ได้ในทันที ดังเช่นโค้ดต่อไปนี้

        Dim ctx As New MyDataContext
        For Each r In ctx.Sites
            Console.WriteLine("Site=" & r.site_name & ", Customer=", r.Customer.customer_name)
        Next

Linq to SQL จะทำการ Query ข้อมูลใน Assoication ให้โดยอัตโนมัติ นั่นคือ ทุกครั้งที่โค้ด run ผ่านบรรทัดที่ 3
หากพบว่ามีการเรียกหา Customer ที่ยังไม่เคย select ข้อมูลขึ้นมาก่อน Linq to SQL จะไป select Customer ตัวที่ผูกอยู่กับ Site ณ จุดนั้นออกมาให้อัตโนมัติ

การ Binding ข้อมูลก็สามารถทำได้ในลักษณะเดียวกันนี้ ตัวอย่างเช่น การแสดงรายชื่อ Site และ Customer นี้ใน GridView

        <asp:GridView runat="server" ID="gv_Site" AutoGenerateColumns="false"
        DataKeyNames="site_id" >
            <Columns>
                <asp:TemplateField HeaderText="Site">
                    <ItemTemplate>
                        <asp:Label runat="server" Text='<%# Eval("site_name") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
                <asp:TemplateField HeaderText="Customer">
                    <ItemTemplate>
                        <asp:Label runat="server" Text='<%# Eval("Customer.customer_name") %>'></asp:Label>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

ซึ่ง code behind ของ GridView นี้ก็เพียงแค่ select Site ใส่เข้าไปใน datasource เท่านั้น

     Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        If Not IsPostBack Then
            Dim ctx As New MyDataContext
            With gv_Site
                .DataSource = ctx.Sites
                .DataBind()
            End With
        End If
    End Sub

นั่นทำให้การเขียนโค้ดของเราง่ายขึ้น เนื่องจากไม่ต้องไปทำการ join อะไรทั้งนั้นในตอน select
ขอเพียงแต่เรากำหนด association ให้ถูกต้องในทีแรก แล้วที่เหลือ Linq to SQL จะจัดการเอง

แต่ทว่า

ถ้าเราลองมานึกดูดีๆ หากข้อมูล Site มีเป็นร้อยๆ record แล้วมี Customer ที่ไม่ซ้ำกันเลยจะเกิดอะไรขึ้น
สิ่งที่เกิดขึ้นก็คือจะมี Query จำนวนมหาศาลวิ่งพล่านอยู่เพียงแค่สั่ง Bind GridView ตัวเดียว ซี่งไม่ใช่เรื่องที่ดีแน่

นี่คือผลจาก SQL Profiler ที่จับออกมาจากการสั่ง Bind Grid View (ผมดัดแปลงเล็กน้อย ตัดเอาเฉพาะส่วน Query ออกมา)

SELECT [t0].[site_id], [t0].[customer_id], [t0].[site_name]
FROM [dbo].[Site] AS [t0]

SELECT [t0].[customer_id], [t0].[customer_code], [t0].[customer_name]
FROM [dbo].[Customer] AS [t0]
WHERE [t0].[customer_id] = @p0

SELECT [t0].[customer_id], [t0].[customer_code], [t0].[customer_name]
FROM [dbo].[Customer] AS [t0]
WHERE [t0].[customer_id] = @p0

........

Query หลังๆ นั้นเกิดจากการ query เพื่อหา Customer ซ้ำๆ ของ site แต่ละ record
ยิ่งจำนวน Site มาก จำนวน Query ก็มากตามไปด้วย และนั่นก็คือประเด็นของ blog เรื่องนี้

วิธีแก้ เขาเรียกว่าการทำ Eager Loading แปลเป็นไทยได้ว่า การโหลดแบบหิวกระหาย
คือมีข้อมูลเท่าไหร่ มันจะดูดมาให้หมด ให้หมดในคราวเดียว
ผลที่ได้ก็คือจำนวนฟิลด์ในการโหลด record หนึ่งจะมากขึ้น ตามแต่จำนวนฟิลด์ของตาราง
แต่ที่ได้กลับมาคุ้มกว่าก็คือ ข้อมูลทุกอย่างนี้สามารถทำได้ใน Query เดียว

การบอกให้ Linq to SQL โหลดข้อมูลแบบหิวกระหายนี้ให้สั่งได้ทาง Load Option ของ DataContext
โดยเราจะต้องบอกไปว่า ในการโหลดข้อมูลของคลาสต่อไปนี้ (ในที่นี้คือ Site)
ให้ eager load association property ต่อไปนี้ด้วย (ในที่นี้คือ Customer)
ดังนั้น โค้ดสำหรับการ Bind GridView จึงต้องมีการเพิ่มบรรทัดที่ 2-4 เข้าไปดังนี้

            Dim ctx As New MyDataContext
            Dim lo As New Data.Linq.DataLoadOptions
            lo.LoadWith(Of Site)(Function(r As Site) r.Customer)
            ctx.LoadOptions = lo
            With gv_Site
                .DataSource = ctx.Sites
                .DataBind()
            End With

และผลที่ได้เมื่อจับดูด้วย SQL Profiler ก็จะเห็นว่า แทนที่ Linq จะทำการ select Customer ทีละบรรทัดๆ
คราวนี้มันจะทำการ select Site พร้อมกับ join กับตาราง Customer (ตามที่เรากำหนดไว้ด้วย Association)
ทำให้เราได้ข้อมูลของทุก record ออกมาได้ใน query เดียวเท่านั้น เป็นวิธีการใช้งาน Linq to SQL ที่จะได้ performance ที่ดีกว่า

SELECT [t0].[site_id], [t0].[customer_id], [t0].[site_name], [t1].[customer_id] AS [customer_id2], [t1].[customer_code], [t1].[customer_name]
FROM [dbo].[Site] AS [t0]
INNER JOIN [dbo].[Customer] AS [t1] ON [t1].[customer_id] = [t0].[customer_id]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: