อันที่จริงเรื่องนี้ผมก็อยากจะบรรยายหรือเล่าสู่กันฟังมาตั้งนานแล้วครับ ... พี่น้อง แต่ก็ลืมทุกทีซิน่า ก็คือ การทำรหัสสินค้า (รหัสลูกค้า รหัสนักศึกษา หรือ อื่นๆ) เป็นตัวเดียวกันกับ Primary Key ... ผมเองก็เคยลองแวะเวียนไปดูตำราภาคภาษาไทยบ้าง ดาวน์โหลดโปรแกรมสำเร็จรูปที่ทำออกมาขายบ้าง ก็แทบจะเหมือนกันหมดเลยทีเดียวเชียวแหละครับพี่น้อง ... พี่น้องครับ ... อนึ่งหากตารางที่ออกแบบมาในลักษณะนี้ มันเป็นตารางเดี่ยวๆ ไม่เกี่ยวกับตารางอื่นใด มันก็คงไร้ซึ่งถึงปัญหา แต่หากมันไปเกี่ยวข้องกับการเชื่อมต่อเข้าไปยังตารางข้อมูลอื่นๆล่ะ ... ปัญหาก็จะเกิดตามหลังขึ้นมาอื้อเลย ... เผลอๆอาจสายเกินการณ์ไปก็เป็นได้ ... ลองอ่านและวิเคราะห์ดูเอาเองเถิดครับ ... พี่น้อง
 จากตัวอย่างนี้ก็คือ การเอารหัสสินค้า (ProductCode) มาทำเป็น Primary Key ถามว่าการออกแบบในลักษณะนี้ถูก หรือ ผิด ?????
"คนเขียนไม่ได้ใช้งาน คนใช้งานไม่ได้เขียน แต่คนเขียนต้องเข้าใจคนใช้งาน"
พี่น้องทั้งหลาย ... เรายังไม่ต้องไปมองถึงตอนที่ออกรายงาน (Report) เลยครับ ลองคิดดูซิว่า โอกาสที่ผู้ใช้งานจะพิมพ์รหัสสินค้าผิดพลาด มันมีทางเกิดขึ้นได้เสมอ สิ่งนี้ที่เราเรียกว่า "Human Error" หากป้อนรหัสสินค้าไม่ถูกต้อง (โดยทั่วไปก็จะเป็น ProductCode หรือ ProductID) ก็จะมีผลกระทบไปยังรายการซื้อ/ขาย นั่นก็หมายความว่า เราต้องเข้าไปตามแก้ไขในทุกๆตารางข้อมูลที่เกี่ยวข้องกับรายการสินค้านี้ด้วยเสมอ ... "ถูกต้องมั้ยครับ" ถามว่าแล้วเราจะแก้ปัญหานี้อย่างไรดี
 ปัญหาของการใช้ Primary Key เป็นตัวเดียวกับ ProductCode - ตารางการซื้อสินค้า เมื่อผู้ใช้งานป้อนรหัสสินค้าผิด เราก็ต้องตามไปแก้ไขในทุกๆตารางที่เกี่ยวข้องกับตารางสินค้า นอกจากนี้ไฟล์ฐานข้อมูลของคุณก็จะมีขนาดใหญ่มากขึ้นด้วย ... โปรดสังเกต
"หลงทางน่ะเสียเวลา หลงรักภรรยาคนอื่นน่ะถึงกับเสียชีวิตได้" ... 55555
 ตัวอย่างการแยก Primary Key ออกจาก ProductCode
เจ้า PK น่ะก็คือ Primary Key ที่เราผู้เป็นโปรแกรมมั่ว เอ๊ย โปรแกรมเมอร์ จะต้องใช้เป็น Key Behind ... เอิ๊กๆๆๆๆ ยังกะ Code Behind ของ Dot Net เลย แต่อันนี้ผมบัญญัติศัพท์ของผมเองแหละ แบบว่าเราต้องใช้รหัสนี้ให้ทำงานอยู่เบื้องหลัง โดยที่ผู้ใช้งาน (Users) เขาไม่รู้ และ ไม่ได้นำไปใช้งานหรอกครับ แต่เรานี่แหละต้องใช้มัน และจากตารางการออกแบบของผม ผมไม่เคยให้ PK นี้เป็น Autonumber เลย สาเหตุก็ตามที่ผมได้เคยอธิบายไปแล้วหลายรอบ อันนี้ก็ไม่ขอกล่าวซ้ำๆซากๆอีก
 การเชื่อมความสัมพันธ์ใบรายการซื้อสินค้า ระหว่าง Primary Key (ตัวจริง) กับ Foriegn Key
จากตารางตัวอย่างของการทำใบรายการซื้อสินค้า สิ่งที่เราเก็บข้อมูลที่สำคัญๆไว้ก็คือ รหัสประจำสินค้า ProductFK ของตาราง tblPurchaseDetail (ตัวนี้ คือ คีย์รอง - Foriegn Key) เชื่อมความสัมพันธ์ไปยัง PK ของตารางสินค้า (tblProduct) ในสถานะการณ์ที่ผู้ใช้งานใส่รหัสสินค้า (ProductCode) ผิด (หรือ รายละเอียดของสินค้าไม่ถูกต้อง) เราก็จะทำการแก้ไขข้อมูลในตารางสินค้าเท่านั้น ส่วนตารางอื่นๆไม่ว่าจะเป็นตารางการซื้อ หรือ ขาย เราไม่จำเป็นต้องมาเปลี่ยนแปลงตาม เพราะว่าเราใช้ฟิลด์ที่ผมได้ระบุเอาไว้ เป็นตัวต่อเชื่อม ไม่ได้เอารหัสสินค้า (ProductCode) มาเชื่อมความสัมพันธ์
กล่าวได้ง่ายๆ็คือ Primary Key (ที่แท้จริง) ของแต่ละตารางจะไม่มีการเปลี่ยนแปลงค่าได้เลย และ Primary Key ที่ว่ามานี้ มันจะต้องเป็นตัวเลขเท่านั้น Primary Key กับ Foriegn Key มันก็ต้องเป็นข้อมูลชนิดเดียวกัน
Primary Key มันต้องเป็นตัวเลขเสมอเหรอ ... แล้วทำไมล่ะ ... พี่น้องทั้งหลายครับ ก่อนเขียนบทความนี้ผมได้กล่าวถึงเรื่อง "ชนิดของข้อมูล" เอาไว้ให้บางส่วนแล้ว นั่นแหละคือเหตุผล ... และ PK = 1000 กับ PK = '1000' นี่มันไม่เหมือนกันน่ะครับ ตัวแรกมันคือ "หนึ่งพัน" ส่วนตัวหลังน่ะคือ "หนึ่งศูนย์ศูนย์ศูนย์"
 เราทำการแก้ไขข้อมูลรายการสินค้าที่ตาราง tblProduct เท่านั้น
แน่นอนครับ ... มาถึงบรรทัดนี้ หลายคนอาจจะสงสัย หรือ โต้แย้งว่า หากทำแบบนี้แล้วเวลาค้นหา หรือ เรียกรายการซื้อขายมาดู มันจะไม่ช้าเหรอ เพราะสิ่งที่จะค้นหาหลักๆก็คือ รหัสสินค้า หรือ ชื่อสินค้า ทำไมไม่เก็บข้อมูลสำคัญๆเหล่านี้ไว้ในตารางการซื้อ/ขายไปเลยล่ะ ... สิ่งที่ผมนำเสนอมานี้ ... พี่น้องต้องลองทดสอบให้เห็นจะๆกะตาตัวเองครับ "อย่าพึ่งเชื่อ" หรือคำที่มักใช้เสมอ "เขาว่ากันว่า" เพราะสาระหลักๆมันคือเรื่องของ "ความซ้ำซ้อน และ ความถูกต้องแม่นยำของข้อมูล"
 ค่า Primary Key หรือ PK นี่แหละครับที่เราต้องเอามาใช้อ้างอิงถึงเสมอในการเขียนโปรแกรมควบคุมการทำงาน เมื่อนำค่า PK (ของ tblProduct - ตารางสินค้า) ตัวนี้ไปเก็บในตารางการซื้อ/ขาย ก็ต้องเอาไปใส่ไว้ใน ProductFK ของตารางที่ว่ามาในการใช้งานจริง เวลาที่มันแสดงผลในตารางกริด เราจะต้องซ่อนมันเอาไว้ ... เดี๋ยวผู้ใช้งงตายเลยซิครับ
ดังนั้นเราต้องแยกมุมมองให้ออก ในขณะที่เรามองมาที่ตารางสินค้า สิ่งที่ต้องยึดหลักเอาไว้เสมอ ก็คือ Primary Key กับ ProductCode ที่ทั้งสองตัวต้องไม่ซ้ำกันกับรายการอื่น (ดังโค้ดตัวอย่างที่แสดงทางด้านล่างนี้) แต่หากเรามองไปที่ตารางการซื้อ หรือ ขาย เราก็โฟกัสมันมาที่ ProductFK (Foriegn Key) แทน เพราะค่านี้จะต้องชี้ หรือ มีความสัมพันธ์ไปยังตารางสินค้า ... พี่น้องต้องแยกแยะให้ถูกด้วยน่ะครับ
ตัวอย่างมาจากบทความ VB + DataBase ตอนที่ 11/1
 |
' วิธีการคำนวณหาค่า Primary Key หรือ PK
' การใช้งานจริง ... แนะนำให้แก้ไขเป็น Public Function ด้วยน่ะครับ
' ส่งแค่ชื่อตารางข้อมูลเข้ามา เพราะผมตั้งชื่อฟิลด์ PK (Primary Key) เหมือนกันอยู่ทุกๆตารางไงล่ะ ... เอิ๊กๆๆๆๆ
' อ้อ ... แล้วส่งค่ากลับแบบ Long Integer ด้วยน่ะ ... มือใหม่ก็คงจะงงเป็นแถวเลย 55555 (มันแปลว่าไรฟ่ะ)
Sub SetupNewData()
Dim Rec As Long
Set DS = New Recordset
' นำข้อมูลจากตารางมาหาจำนวนสูงสุด
' ต้องระมัดระวังในการจัดเรียงข้อมูลด้วยน่ะครับผม
' มีวิธีอื่นๆนอกจากนี้เยอะแยะไป ... ลองไปฝึกหัดคิดด้วยตัวเองบ้างเน้อ
Statement = "SELECT * FROM tblProduct ORDER BY PK "
DS.Open Statement, ConnMyDB, adOpenForwardOnly, adLockReadOnly, adCmdText
Rec = 0
' จุดเริ่มต้นของ Begin of File (BOF) กับ End Of File (EOF) มันเท่ากัน ก็แสดงว่าไม่มีข้อมูล
If DS.BOF Or DS.EOF Then
Rec = 0
Else
' หากใช้วิธีการนับ ค่าสุดท้ายที่ได้จะไม่ตรง ... เพราะบางครั้งเราอาจทำการลบรายการบางส่วนออกไป
DS.MoveLast
Rec = DS("PK")
End If
Rec = Rec + 1 ' เพิ่มจำนวน PK
PK = Rec
DS.Close: Set DS = Nothing
End Sub
|
เทคนิคของการตรวจสอบรหัสสินค้า ... หรือเหล่าบรรดารหัสลูกค้า/รหัสผู้จำหน่ายสินค้า
Sub RecordToScreen()
Set DS = New Recordset
' นำข้อมูลจากตารางมาแสดงผล
' พี่น้องครับ ... ไม่ต้องมาจดจำการ Join กันของตารางหรอกครับ
' ผมแนะนำให้ใช้งาน Query ที่อยู่ใน MS Access เป็นตัวทดสอบก่อน
' เมื่อได้ผลลัพธ์ดั่งใจที่ต้องการแล้ว ก็ค่อยทำการตัดแปะมาใส่ ... ง่ายกว่ากันเยอะเลย
' บางอย่างก็ควรทำด้วยความเข้าใจ ... แบบไม่ต้องมานั่งท่องจำให้ปวดกะโหลกหรอกครับ
' อีกอย่าง ... เขามีเครื่องมือมาให้ใช้แล้ว ก็ควรจะใช้มันให้คุ้มค่าด้วยล่ะกัน ... เหอๆๆๆๆๆ
Statement = "SELECT tblProduct.*, " & _
" tblProductGroup.ProductGroupName, " & _
" tblBrandName.BrandName, " & _
" tblUnit.UnitName " & _
" FROM tblUnit INNER JOIN (tblBrandName INNER JOIN " & _
" (tblProductGroup INNER JOIN tblProduct ON " & _
" tblProductGroup.PK = tblProduct.ProductGroupFK) ON " & _
" tblBrandName.PK = tblProduct.BrandNameFK) ON " & _
" tblUnit.PK = tblProduct.UnitFK " & _
" WHERE [tblProduct.PK] = " & PK & _
" ORDER BY [tblProduct.PK] "
DS.Open Statement, ConnMyDB, adOpenForwardOnly, , adCmdText
' นำข้อมูลมาแสดงผลบนหน้าจอ
txtProductCode.Text = "" & DS("ProductCode")
' ============================================================
' เก็บค่า ProductCode เดิมไว้ก่อน เพื่อทดสอบว่ามีค่าซ้ำกันหรือไม่ ก่อนที่จะทำการบันทึกผล
' ดูจากการเกิดเหตุการณ์ cmdSave_Click น่ะครับ ... พี่น้อง
' นั่นคือการนำคุณสมบัติ Tag ของ TextBox มาใช้งานให้เกิดประโยชน์ ... โดยเราเอาค่าเดิมมาซ่อนเอาไว้ก่อน
' เทคนิคการใช้งานแบบนี้ ... ขอบอกครับขอบอก นำไปใช้งานได้สารพัดอย่างแหละ (แต่อย่าใช้มันไปซื้อโอเลี้ยงให้น่ะ ... 55555)
txtProductCode.Tag = txtProductCode.Text
' ...
' ...
' ...
End Sub
|
เทคนิคของการตรวจสอบรหัสสินค้า ... ก่อนทำการบันทึกผลเก็บเข้าสู่ตารางข้อมูล
Private Sub cmdSave_Click()
' ProductCode
If Trim(txtProductCode.Text) = "" Or Len(Trim(txtProductCode.Text)) = 0 Then
MsgBox "กรุณาป้อนรหัสสินค้าให้เรียบร้อยก่อนด้วย.", vbOKOnly + vbExclamation, "รายงานสถานะ"
txtProductCode.SetFocus
Exit Sub
ElseIf Trim(txtDescription.Text) = "" Or Len(Trim(txtDescription.Text)) = 0 Then
MsgBox "กรุณาป้อนชื่อสินค้าให้เรียบร้อยก่อนด้วย.", vbOKOnly + vbExclamation, "รายงานสถานะ"
txtDescription.SetFocus
Exit Sub
End If
'
' หากมีการเปลี่ยนแปลงค่าข้อมูลของ ProductCode
' txtProductCode.Tag เราพิจารณาได้จาก 2 เหตุการณ์ คือ
' 1. เพิ่มข้อมูลใหม่ กรณีนี้ค่าใน Tag เดิมก็จะเป็นค่าว่างอยู่แล้ว
' 2. แก้ไขข้อมูล กรณีนี้ให้ไปดูจากโปรแกรมย่อย RecordToScreen จะเห็นได้ว่ามีการเก็บค่า
' ลงใน Tag ไว้ก่อนล่วงหน้าแล้ว ... อย่าลืมครับ Visual Basic มันมีสาระสำคัญอยู่ที่ Events/Driven
If txtProductCode.Text <> txtProductCode.Tag Then
If CheckExistCode > 0 Then
MsgBox "มีรหัสสินค้า: " & Trim(txtProductCode.Text) & " เรียบร้อยแล้ว กรุณาแก้ไขใหม่ด้วย.", _
vbOKOnly + vbExclamation, "รายงานสถานะ"
Exit Sub
End If
End If
' ในเหตุการณ์นี้เราจะทำการดัก Error ที่เกิดขึ้นก่อนทำการบันทึกข้อมูลครับ ... พี่น้อง
' เมื่อไม่มี Error หรือ ความผิดพลาดแล้ว ก็ค่อยไปโปรแกรมย่อย SaveData
Call SaveData
End Sub
' ฟังค์ชั่นที่ใช้ในการตรวจสอบรหัสสินค้าว่าซ้ำกันหรือไม่
Function CheckExistCode() As Byte
Set DS = New Recordset
SQLStmt = "SELECT * FROM tblProduct WHERE [ProductCode] = " & "'" & Trim(txtProductCode.Text) & "'" & _
" ORDER BY [PK]"
DS.CursorLocation = adUseClient
DS.Open SQLStmt, ConnMyDB, adOpenForwardOnly, adLockReadOnly, adCmdText
' คืนค่ากลับเป็นจริงหากมีค่าซ้ำกัน แสดงว่า DS.RecordCount ต้องมีค่ามากกว่า 0
CheckExistCode = DS.RecordCount
DS.Close: Set DS = Nothing
End Function
|
::: ลองดูตัวอย่างชัดๆอีกตัวหนึ่งครับ ::: |
 การออกแบบตารางข้อมูลลูกค้า โดยให้ CustomerID เป็น Primary Key
 การออกแบบตารางข้อมูลรายการซื้อสินค้าของลูกค้า
การเชื่อมต่อความสัมพันธ์ของตาราง tblCustomer กับ tblInvoice
 ผลลัพธ์ของการทำ Query
อุ๊ยตาย ว๊ายกริ๊ด ... ผู้ใช้งาน (User) จำเป็นต้องแก้ไขรหัสลูกค้าน่ะ
เป็นไงล่ะ ถูกตัดขาดความความสัมพันธ์ กันสะบั่นหั่นแหลกไปเลยซิครับพี่น้องดังนั้นรายชื่อลูกค้า " CUS-50000X นายทองก้อน ทับทิมกรอบ" ก็ไม่แสดงผลออกมาเลยไง
::: แล้วจะแก้ปัญหายังไงกันดีล่ะเนี่ย ::: |
ห้าม !!! ผู้ใช้งานคีย์ข้อมูลผิดพลาดเป็นอันขาด ... หากเขา (หรือ เธอ) คีย์ผิดพลาด ถือเป็นความผิดอย่างมหันต์ อภัยให้ไม่ได้ ต้องด่าไฟแลบไปเลย พร้อมกับตัดเงินเดือน 50% ... ก้ากๆๆๆๆๆๆๆๆๆๆๆ
ห้าม !!! เปลี่ยนแปลงข้อมูลไม่ว่ากรณีใดๆทั้งสิ้น เพราะมันทำให้โปรแกรมเมอร์มีภาระมากขึ้นโดยใช่เหตุ ... 55555 ... โดยเฉพาะโปรแกรมเมอร์แบบเราๆท่านๆที่มีความสุจริต และ เที่ยงทำ ... ยังไม่ถึงเที่ยง ไม่ทำน่ะจ๊ะ เอิ๊กๆๆๆๆๆ
เพิ่มเวลา และ ขั้นตอน (อีกนิดเดียวเอง ... เหอๆๆๆๆๆ) ก็ตามไปแก้ไขสิ่งที่เกี่ยวข้องกับฟิลด์ CustomerID ให้หมดในทุกๆตารางเลยน่ะซิ ... 55555
มาถึงตอนนี้หากพี่น้องคิดไม่ออก ... ผมก็ไม่รู้จะบอกยังไงแล้วล่ะครับ
พี่น้องเคยเขียนโปรแกรม (ที่ใช้งานได้จริงน่ะ) แล้วมันไม่เคยเกิด Error กันบ้างมั้ยล่ะครับ ... เหอๆๆๆๆ ... บางครั้ง Error มันก็ไม่เกี่ยวหรอกครับ เช่น พวกรหัสสินค้า ... ตอนเริ่มต้นตั้งกิจการก็ตั้งรหัสเอาตามใจชอบ แต่พอกิจการใหญ่โตขึ้นก็เลยกะจะโกอินเตอร์ซ่ะหน่อย จึงขอรหัส EAN-13 มาใส่เป็นรหัสสินค้าแทน อ้าวววว !!! แล้วมันจะเปลี่ยนยังไงล่ะเนี่ย ... และแล้ววิชามารก็ถูกงัดออกมาใช้ ตอนขายโปรแกรมขายไปสามหมื่น ... (เสียงโปรแกรมเมอร์) โอ้โหยากมากเลยน่ะครับเฮีย งานนี้กระผมก็ขอคิดค่าโมดิฟายโปรแกรมอีกห้าหมื่นครับ รับรองสามารถ Support EAN-13 ของเฮียได้แน่ๆ ... อิอิอิอิอิ
|