ADO.NET #4 : Basic of DataSet

บทนี้มาถึง "ของเล่นใหม่" ของ ADO.NET จริงๆ ซักที มัวแต่รำมวยอยู่ตั้งนาน ได้ฤกษ์แล้วครับที่เราจะพูดถึงเรื่อง DataSet ในบทนี้ผมจะเขียนโปรแกรมเพื่อ update ชื่อหน้าและชื่อหลังของผู้แต่งหนังสือให้กลายเป็นอักษรตัวใหญ่ทั้งหมด แต่ผมก็ไม่อยากที่จะไปเปลี่ยนแปลงข้อมูลใน authors เอาอย่างนี้ดีกว่า ผมลอกข้อมูลจาก authors มาเป็น authors2 ก่อนด้วยตำสั่ง 

SELECT * INTO pubs..authors2
FROM pubs..authors

สั่งใน Query Analyser หรือ osql, isql ของ Microsoft SQL Server ก็ได้ครับ

     ผมได้รับ Email จากผู้อ่าน Web นี้คนหนึ่งบอกว่าอย่างให้แสดงตัวอย่าง XML ให้ดูหน่อยว่า ADO.NET และ XML มันสัมพันธ์กันอย่างไร ใน Web Page นี้ผมก็จะแสดงให้เห็นตรงนั้นด้วยครับ เรามาเริ่มกันเลยครับ

  ทำไมต้องมี DataSet

                อันนี้ขอฉายซ้ำหน่อยครับ กลัวว่าถ้าใครไม่คุ้นเคยกับมันจะต่อเรื่องไม่ติด เรื่องของ DataSet นั้นเกิดมาจากความต้องการในการปรับปรุง RecordSet ของ ADO เรามาดูกันทีละข้อเลยดีกว่า

 Recordset ไม่เหมาะสมกับ Web Application

                Web Application นั้น ถ้าเป็น Technology ของ Microsoft แล้ว จะใช้เครื่องมือที่เรียกว่า Active Server Page (ASP) ในการทำ  โดยปกติแล้ว มันจะเริ่มทำงานเมื่อมี user เรียกผ่าน Browser เข้ามา มันจะทำคำนวณต่างๆ แล้ว Render ออกมาเป็น HTML ส่งกลับมาให้ browser ซึ่งถ้าในระหว่างการ Render นั้น มีการติดต่อกับ Database แล้ว ADO จะเป็นเครื่องมือที่ถูกเรียกใช้

                        ปัญหาที่เกิดขึ้นก็คือ ADO นั้นถูกออกแบบมาสำหรับทำ Application ที่ใช้ในสำนักงาน ไม่ใช่ Web Application ดังนั้นโดยธรรมชาติของ ADO มันจะติดต่อกับ Database ตลอดเวลา ตั้งแต่ต้นจน Render เสร็จ ซึ่งในความเป็นจริงแล้ว ถ้าเป็น Web Application มาจะติดต่อกับ Database นานเกินไปทำให้กิน Resource ของทาง Database Server มากเกินไป อย่าลืมนะครับว่า Web Site ยอดนิยมนั้นในหนึ่งนาทีนั้น มีผู้เรียกเข้ามาจำนวนมาก ซึ่งนี่ก็เป็นสาเหตุหนึ่งทำให้ระบบทั้งระบบช้าลงไปหมด

                        ถึงแม้ว่าใครจะบอกว่า ADO ก็สามารถตัดการ Connection กับ Database Server ได้ทันทีที่ดึงข้อมูลเสร็จ จริงครับความสามารถนี้เราเรียกว่า Disconnected Database ซึ่งยอมให้เรายังสามารถใช้งาน RecordSet หลังจากที่เราตัดที่ตัดการ Connection กับ Database Server แล้ว ซึ่งความสามารถนี้ Microsoft ได้มาจาก Foxpro หลังจากซื้อกิจการบริษัท Fox Holding เป็นที่เรียบร้อย จริงครับที่ Microsoft มี Technology นี้สำหรับใช้กับ ADO อยู่ แต่นั่นหมายความว่า เราต้องทำการควบคุมทุกครั้งในการติดต่อและการเลิกติดต่อ และโปรแกรมเมอร์ส่วนมากจะละเลยที่จะสนใจเรื่องนี้ จนกระทั่งเมื่อการทำงานช้าลงอย่างเห็นได้ชัด จึงมาให้ความสนใจ แต่มันก็สายเกินไปแล้วที่จะแก้ Code ทั้งหมดที่มีในมือ

                พอมาถึง ADO.NET นั้นการติดต่อกับ Database Server และการตัดการ Connection นั้นเป็นไปโดยอัตโนมัติ ADO.NET จะติดต่อ Database Server เมื่อมีความต้องการที่จะใช้จริงๆ และจะตัด Connection ทันทีเมื่อเลิกใช้ครับ

Recordset รองรับได้แค่ Table เดียว

                        ในเมื่อกระแสของ Web Service กำลังแรง มีการแข่งขันกันค่อนข้างสูง การที่ Recordset นั้นรองรับงานได้เพียง Table เดียวนั้นไม่เพียงพอแล้วสำหรับงานในยุคปัจจุบัน เพราะถ้าบริษัทอื่นส่งข้อมูลเป็น XML เฃ้ามา แล้ว Microsoft รองรับได้เพียง Table เดียว จะทำให้เกิดปัญหาได้ ดังนั้น Microsoft ต้องปรับเป็น ADO.NET เพื่อรองรับความสามารถของ XML ได้อย่างเต็มที่ ดังนั้น Microsoft จึงปรับปรุง Recordset เดิมขึ้นใหม่ให้รองรับได้หลาย Tables ผลลัพธ์ออกมาจึงเป็น DataSet และ DataSet ยังเพิ่มความสามารถในการทำ Reference Integrity อีกด้วย

Recordset ไม่เป็นอิสระจาก Database Server

                Recordset ไม่สามารถพัฒนาได้มากนักเป็นเพราะว่าต้องผูกติดกับ Database Server ที่ตัวเองติดต่อด้วย ดังนั้นเพื่อให้รองรับกับ Database Server ทุกตัว ทาง Recordset จึงจำเป็นต้องทำงานแบบกลางๆ ทำให้ความสามารถด้อยลง และความสามารถบางอย่างไม่สามารถบรรจุเฃ้าไปใน Recordset ได้ DataSet ที่ออกแบบใหม่จึงมีความอิสระต่อ Database Server ทำให้มีความสามารถและประสิทธิภาพเพิ่มขึ้น

ADO เดิมไม่สามารถทำงานผ่าน Firewall

                Hacker สมัยนี้เกลื่อนบ้านเกลื่อนเมือง ส่วนมากเป็นพวกทหารราบ คือไปลอกเอาวิธีการคนอื่นเขาคิดมา ผมว่านะ Hacker ที่ว่าเก่งๆ นั้นก็ไม่สามารถเปรียบเทียบได้กับผู้คิดค้นหรอกครับ เปรียบเสมือนว่า ถ้ามีใครคิดค้นสูตรพิสดารในการพยากรณ์อนาคตได้ เขากลัวว่าใครก็ตามจะเอาของเขาไป เขาจึงปิดบ้านใส่กุญแจลงกลอนเสียอย่างดี แต่สุดท้ายก็โดนโจรสะเดาะกุญแจเอาของดีไปจนได้ แบบนี้จะบอกว่าโจรนี้เก่งกว่าคนคิดค้นอีก ผมว่าหลงประเด็น ตัวผมเองก็หลงประเด็นเหมือนกัน ไม่รู้ว่ามาถึงเรื่องนี้ได้ยังไง กลับไปเรื่อง Hacker ก่อนดีกว่า ด้วยความจริงที่ว่าบ้านของเรามีประตูหน้าต่างเยอะเกินไป Firewall จึงทำหน้าที่ ปิดประตูทั้งหมดเสีย เหลือเพียง ประตู Web เพียงประตูเดียว ดังนั้น ADO จึงไม่สามารถทำงานได้เพราะดันไปใช้งานประตูที่ไม่ใช่ Web พอมาถึง ADO.NET เราสามารถสร้างเป็น XML เพื่อผ่านประตู Web เลย ดังนั้น ADO.NET จึงสามารถใช้งานกับ Firewall ได้

เรามาลองดู Code กันเลยดีกว่า

dataset1.cs

using System;
using System.Data;
using System.Data.SQL;
class DataSet1
{
	public static void Main()
	{
		SQLConnection cn = new SQLConnection("server1", "sa", "", "pubs");
		SQLDataSetCommand dsc = new SQLDataSetCommand("SELECT au_fname, au_lname FROM authors2", cn);
		
		DataSet ds = new DataSet();
		dsc.FillDataSet(ds, "authors2");
		
		foreach (DataRow r in ds.Tables["authors2"].Rows) {
			r["au_fname"] = r["au_fname"].ToString().ToUpper();
			r["au_lname"] = r["au_lname"].ToString().ToUpper();
		}

		dsc.Update(ds, "authors2");
	}
}

เวลา Compile อย่าลืม /r:system.dll /r:system.data.dll

จาก Code นี้คุณจะเห็น  

ถ้าคุณรันโปรแกรมดังกล่างมันจะเกิด Exception Error ที่ชื่อว่า Invalid attempt to update Table 'authors2' No key columns were defined.                    

เรามาลองดูกันดีกว่าว่า DataSet ภายในนั้นทำงานอย่างไร

    เพื่อการนำเอา DataSet ไปใช้งานได้อย่างถูกต้องนั้น เราต้องมีความรู้ว่า DataSet นั้นทำงานอย่างไร สิ่งแรกที่เราต้องรู้ก็คือ DataSet นั้นเป็นอิสระกับการติดต่อกับ Database ดังนั้น DataSet จึงไม่สามารถใช้สิ่งที่เรียกว่า Server Cursor ได้ ซึ่งเรื่องนี้อาจจะเป็นข้อด้อยข้อเดียวที่ ADO.NET สู้ ADO เก่าไม่ได้ ก็แนวคิดมันขัดกันครับ การใช้ Server Cursor นั้น เราต้องติดต่อกับ SQL Server ตลอดเวลา 

    เมื่อ DataSet จำเป็นที่ต้องใช้ Client cursor เพียงอย่างเดียว นั่นก็หมายความว่า DataSet จะจำลองตัวเองเป็น Database ย่อยๆ ในหน่วยความจำของ Client ซึ่งเราสามารถ ปรับปรุงข้อมูลใน Database ดังกล่าวได้ แต่อย่าลืมว่าการปรับปรุงนั้นกระทำในหน่วยความจำของเรา ไม่ใช่บน SQL Server ถ้าเราดูที่ Code เราจะเห็นได้ว่า คำสั่ง foreach() นั่นแหละครับที่กระทำอย่างที่กล่าวมาแล้วข้างต้น

    คราวนี้เมื่อถึงเวลาที่ต้องกลับไปปรับปรุง Database จริงแล้วด้วยคำสั่ง dsc.Update(ds, "authors2") ในเมื่อเป็น Client Cursor วิธีการที่จะปรับปรุงข้อมูลกลับไปที่ SQL Server นั้นคือการสร้างคำสั่ง SQL แล้วยิงไปที่ Database Server ในที่นี้คือคำสั่ง Update นั่นเอง ยกตัวอย่างเช่น

UPDATE authors2 
SET au_fname='LIVIA', au_lname='KARSEN' 
WHERE au_fname='LIVIA', au_lname='KARSEN' 

    ถ้าคุณมี 30 columns ในส่วนของ set ก็จะมีการ set ทั้ง 30 columns ไม่ว่า columns นั้นมีการแก้ไขหรือไม่ และ where ก็ต้องมีทั้ง 30 columns อยู่ในส่วน where แต่นั่นก็ไม่ได้เป็นหลักประกันอะไร ถ้าทั้ง 30 columnsที่ดึงมา หา unique index ไม่ได้ การ update เข้าไปอาจส่งผลให้ update ได้เกิน 1 row (record) และวิธีนี้เป็นวิธีที่ ADO ใช้ ซึ่งอาจมักจะเกิดปัญหา และโปรแกรมเมอร์ที่ไม่เข้าใจเรื่องนี้ ก็จะบอกว่าเป็นบั๊กของ ADO

    แต่พอมาถึง ADO.NET ทาง Microsoft ได้ปรับปรุงจาก ADO ในเรื่องนี้ด้วย โดยเมื่อเราสั่ง Method Update ของ SQLDataSetCommand ADO.NET จะติดต่อไปยัง Database Server เพื่อขอรายละเอียดของ Table ว่า column  ต่างๆ ของ Database นั้นมียามอะไรบ้าง และมี Index ตัวใดบ้าง ข้อมูลเหล่านี้เราจะเรียกว่า Meta Data ซึ่งสำหรับ Microsoft SQL Server แล้ววีธีการขอ Meta Data นั้น ทำได้โดยคำสั่ง

SETL FMTONLY ON
SELECT * FROM authors2

    ซึ่งคำสั่งแรกนั้นเป็นคำสั่งที่ขอดึงเอาเฉพาะ Meta Data มา ส่วนบรรทัดที่สองคือการระบุ Table ที่เราไปขอ จากนั้น DataSetCommand จะทำการวิเคราะห์ดูว่า column ต่างๆ ที่เราต้องการดึงมานั้น มี Unique Index รองรับหรือไม่ ถ้าไม่มี ADO.NET จะสร้าง exception ที่ชื่อว่า Invalid attempt to update Table 'authors2' No key columns were defined ขึ้นมานั่นเองวิีธีแก้ เราสามารถที่จะสร้าง Unique Index สำหรับชื่อหน้าและชื่อหลังก็ได้ แต่วิธีนี้ไม่ปลอดภัยนั้น เพราะชื่อจำลอง ศรีเมืองยังมีตั้ง 5 คน ดังนั้นสิ่งที่นักคอมพิวเตอร์นิยมทำก็คือ หารหัสที่ unique มาแทนข้อมูลแต่ละบรรทัด เช่นรหัสบัตรประชาชนเป็นต้น ถึงแม้จะมีจำลอง ศรีเมือง 5 คน แต่ทั้ง 5 คนจะมีรหัสบัตรประชาชนไม่ซ้ำกัน ทำให้เราสามารถระบุตัวได้ ใน Table Authors2 ก็เช่นกัน มี au_id เป็น unique key ดังนั้นคุณควรจะสร้าง unique Index ดังนี้ครับ

CREATE UNIQUE  INDEX idx_00 ON authors2 (au_id)

    ดังนั้นแทนที่เราจะดึงข้อมูลมาเฉพาะ au_fname และ au_lname เราต้องดึง au_id แถมมาต้อง Select ด้วย ซึ่งจะทำให้ ADO.NET สามารถสร้างคำสั่ง SQL ดังนี้ได้                 

UPDATE authors2 
SET au_fname='LIVIA', au_lname='KARSEN', au_id = '756-30-7391'
WHERE au_id = '756-30-7391'

    จะเห็นได้ว่าในส่วนของ W็HERE นั้นดูดีขึ้น แต่ในส่วนของ SET นั้น ถ้ามี 30 columns ก็ต้องทำทั้ง 30 columns เช่นเดิม ดังนั้นจะเห็นได้ว่า ระบบใหญ่ๆ ส่วนมากจะเขียนคำสั่ง SQL เพื่อไป Update Database เองไม่พึ่ง ADO หรือ ADO.NET 

XML

       อย่างที่ผมติดผู้อ่าน Web ท่านหนึ่งที่สงสัยเกี่ยวกับ ADO.NET กับ XML นั้น ก่อนที่จะทำความเข้าใจนั้นผมขออธิบาย XML ในอีกมุมหนึ่งดีกว่า

    ในอดีตนั้น เมื่อคุณต้องการที่จะ copy Table หนึ่งไปยังอีกที่หนึ่งที่ต่าง Server กัน วิธีการที่พื้นๆ ที่สุดถ้าเป็น Microsoft SQL Server นั่นก็คือการใช้ BCP ทำการ dump table นั้นออกมาเป็น Text File จากนั้นก็ copy ใส่แผ่นดิสก์เพื่อ BCP เข้าอีก Server หนึ่ง การทำอย่างนี้ดูเหมือนว่าง่าย แต่ก็มีข้อจำกัดสองประการ คือ

  1.     BCP ทำได้เฉพาะข้อมูลเท่านั้น ไม่รองรับ Schema คุณต้องมี File ที่เก็บ Schema ต่างหาก (schema ก็เป็นที่บอกว่า table นี้ประกอบด้วย columns อะไรบ้างและมีชนิดเป็นอะไร)
  2. ใช้ได้กับ SQL Server เท่านั้น DBMS แต่ละตัว มีวิธีการคล้าย BCP ด้วยกันทั้งสิ้นแต่ใช้ร่วมกันไม่ได้

    จากนั้นก็มีการพัฒนามาใช้ ODBC และ OLEDB เป็นตัวกลางในการโอนระหว่าง Server ที่อาจต่างชนิดกัน แต่ก็มีปัญหาอีกตรงที่ว่า คุณภาพของ ODBC ของการติดต่อ DBMS แต่ละยี่ห้อนั้นไม่เท่ากัน มีบั๊กเยอะนั่นเอง

    พอมาถึง XML นั้น จริงๆ แล้วมันก็คือ Text File ตัวหนึ่งที่คล้ายกับ BCP นั่นเอง แต่ทุกๆ DBMS ยอมรับรูปแบบนี้ มันจะมีนิยามของ Schema และ data อยู่ใน file เดียวกัน เมื่อ SQL Server ดึงข้อมูลออกมาเป็น XML เราก็สามารถเอา DBMS ยี่ห้ออื่นเช่น Oracle ไปดึง Table นั้นเข้าได้

    และ XML ก็ถูกใช้มากกับ WebService ซึ่งผมจะไม่พูดอะไรตรงนี้เพราะมันจะไปกันใหญ่ วกกลับมาที่ ADO.NET ครับ ADO.NET ก็สามารถที่จะรับข้อมูลเข้าเป็น XML และ ข้อมูลออกเป็น XML เช่นกัน ใช้กันง่ายๆ เลยครับ ยกตัวอย่างเช่น

xml.cs

using System;
using System.Data;
using System.Data.SQL;
class DataSet1
{
	public static void Main()
	{
		SQLConnection cn = new SQLConnection("com1supoj", "sa", "", "pubs");
		SQLDataSetCommand dsc = new SQLDataSetCommand("SELECT * FROM authors2", cn);
		DataSet ds = new DataSet();
		string strXml;
		
		dsc.FillDataSet(ds, "authors2");
		strXml = ds.Xml;
		Console.WriteLine(strXml);
		DataSet ds2 = new DataSet();
		ds2.Xml = strXml;
		foreach (DataRow r in ds2.Tables["authors2"].Rows) {
			Console.WriteLine(r["au_fname"] + " " + r["au_lname"]);
		}
	}
}

จะได้ผลลัพธ์แบบย่อๆ ดังนี้

DOS Prompt

C:\CS>xml
<NewDataSet>
<xsd:schema id="NewDataSet" targetNamespace="" xmlns="" xmlns:xsd="http://www.
w3.org/1999/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="authors2">
<xsd:complexType content="elementOnly">
<xsd:all>
<xsd:element name="au_fname" type="xsd:string"/>
<xsd:element name="au_lname" type="xsd:string"/>
<xsd:element name="au_id" type="xsd:string"/>
</xsd:all>
</xsd:complexType>
</xsd:element>

<xsd:element name="NewDataSet" msdata:IsDataSet="True">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element ref="authors2"/>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<authors2>
<au_fname>JOHNSON</au_fname>
<au_lname>WHITE</au_lname>
<au_id>172-32-1176</au_id>
</authors2>
<authors2>
<au_fname>MARJORIE</au_fname>
<au_lname>GREEN</au_lname>
<au_id>213-46-8915</au_id>
</authors2>
<authors2>

...

</NewDataSet>

JOHNSON WHITE
MARJORIE GREEN
....
C:\CS>_

สังเกตดูนั้นครับ เราดึงข้อมูลมาใส่ไว้ใน DataSet ที่ชื่อว่า ds ซึ่งเราขอ output ให้เป็น text ภาษา XML โดยใช้ Property ที่ชื่อว่า Xml ผมเลยสั่งพิมพ์ภาษา XML ออกมา ว่ามันมีนิยามอย่างไร จะสั่งเกตนะครับตรงที่ผม highlight เป็นสีเหลือง ส่วนแรกเป็น schema ส่วนที่สองเป็น data นั่นเอง

    เสร็จแล้วผมสร้าง Datase ชื่อว่า ds2 ไม่เคยดึงข้อมูลจากไหนมาก่อน ผม import ภาษา XML เข้าไปใน ds2 ทำให้ ds2 มี Table authors2 ขึ้นมาใหม่ เสร็จแล้วผมจึงสั่งพิมพ์ข้อมูลจาก authors ใน ds2 ออกมาปรากฏว่าออกมาได้ไม่มีปัญหาอะไร

    คงมองเห็นภาพแล้วนะครับว่า มันสำพันธ์กับ XML อย่างไร โดยปกติแล้ว ถ้าเป็น Web Service ตัวของ Web Service จะมาแอบเรียก Method ของ DataSet เช่น ReadXMLData(), WriteXMLData(), ReadXMLSchema(), WriteXMLSchema() เป็นต้น โดยอัตโนมัติครับ ทำให้คุณแทบไม่ต้องไปยุ่งกับ XML เองเลย