เทคนิคการสร้างข้อมูล Lookup บน AngularJS ด้วยข้อมูลจาก Excel

บ่อยมากที่เราจะต้องทำ Form ที่เก็บข้อมูลจังหวัด อำเภอ ตำบล และทุกๆ คนก็ไม่ต้องการที่จะกรอกข้อมูลเหล่านี้เอง ถ้าเป็นสมัยก่อน เราคงจะคิดว่า ต้องสร้างเป็น Web Service และใช้ Ajax ไปโหลดข้อมูลมา วันนี้ ผมขอลองเสนออีกหนึ่งทางเลือกหนึ่ง คือ ใช้ฝั่ง Client เป็นคนทำตัว Filter จังหวัด โดยใช้ Service สำหรับ Angular JS ที่สร้างจากไฟล์ Excel รายชื่อตำบล ซึงผม GoogleBing มาได้ ไฟล์นี้ครับ [เว็บ dwr.go.th]

UPDATE: ใช้ไฟล์นี้แล้วครับ เพราะว่า ไฟล์เดิม ไม่มีจังหวัด กทม! (เกือบไป) [เว็บ bot.or.th]

image

จากไฟล์นี้ เป็นรูปแบบที่ผมชอบมาก คือ ใส่ข้อมูล Duplicate กันมาเลย ไม่ต้อง Normalize เพราะว่าเราสามารถใช้งานพวก Filter ได้โดยสะดวก แน่นอนว่ามันผิดหลักการฐานข้อมูลสุดๆ แต่ว่าข้อมูลแบบนี้ เราทำงานด้วยง่ายมากครับ อย่างที่เราทราบดีกว่า ฐานข้อมูลถ้า Normalize มาก ก็ต้อง Join มาก ถ้า Join มาก ก็ทำงานช้า แต่ถ้าไม่ Normalize เลย ข้อมูลก็จะบวม และผิดพลาดได้ง่าย เพราะมันมีข้อมูลซ้ำกันที่เราอาจจะตามไปแก้ไขไม่ครบ แต่ว่าเขียนโปรแกรมง่ายกว่า

แผนของผม ผมจะให้ข้อมูล Lookup ของผม เป็น Object แบบนี้ครับ

image

เริ่มจากจังหวัดก่อน

ดังนั้นขั้นแรกผมจึงต้องการชื่อจังหวัดทั้งหมดออกมาก่อน เราก็ทำการ Copy คอลัมน์แรก แล้วทำการ Paste ใน Sheet ใหม่ จากนั้นใช้คำสั่ง Remove Duplicates เพื่อเอาข้อมูลที่ซ้ำกันออก เหลือแค่รายชื่อจังหวัดตามที่เราต้องการ

image

image

เรียบร้อยแล้ว ใน Column ถัดมา เราจะใช้ Excel ช่วย Generate Code ให้เราครับ!

วิธีการก็คือ ใส่สูตรลงไป ดังนี้ครับ

="province['" & B1 & "'] = { 'name': '" & B1 & "', 'children' : [] };"

และเมื่อเราลากสูตรให้มัน Apply กับทุกแถว เราก็จะได้ Code ดังนี้

image

แต่ถ้าอยากได้ Index เป็นเลข (ตามตัวอย่างจะได้ Associative Array) ก็สามารถประยุกต์ได้ครับ ด้วยสูตรนี้

="province[province.length] = { 'name': '" & B1 & "', 'children' : [] };"

image

แต่ว่า เนื่องจากเราต้องการจะ Lookup กลับไปยังจังหวัดด้วย ดังนั้น เราจะต้องใช้แบบ Index เป็นชื่อจังหวัดก่อนนะครับ

อำเภอ

พอจะเห็นแนวทางแล้วใช่ไหมละครับ เราก็ทำแบบเดียวกันกับจังหวัด แต่ว่า Remove Duplicate ที่อำเภอแทน ในหน้าจอก็เลือกเฉพาะคอลัมน์อำเภอ เพื่อตัดให้เหลือแต่อำเภอที่ Unique ครับ

image

image

เสร็จแล้ว เราก็มาใช้ Excel สร้าง Code ให้เราอีกรอบ

="province['" & B1 & "'].children['" & C1 & "'] = { 'name' : '" & C1 & "', 'children' : [] };"

image

พอเราเอาโค๊ดทั้งหมดไปรวมกัน แล้วลองรันดู ก็จะพบว่า เราได้ Structure ตามที่ต้องการอยู่ใน Memory เรียบร้อยครับ

image

ตำบล

สำหรับตำบลนั้น เราไม่ต้อง Remove Duplicates แล้ว เพราะว่าข้อมูลที่เราต้องการนั้นก็คือข้อมูลที่มันแสดงอยู่เลย เปลี่ยนสูตรนิดหน่อย ดังนี้ครับ

="province['" & B1 & "'].children['" & C1 & "'].children.push('" & D1 & "');"

image

ก็จะเป็นขั้นตอนสุดท้ายของการ Initialize ข้อมูลตำบลเข้าไปในอำเภอครับ

ซ่อมและ Sort Array จากนั้นทำเป็น AngularJS Module

จากนั้น เราจะต้องเพิ่ม Code อีกเล็กน้อย เพื่อให้ Array ของจังหวัดเรา สามารถใช้งานได้เป็น Array จริงๆ ไม่อย่างนั้นแล้ว จะไม่สามารถ Binding กับ AngularJS ได้ครับ แล้วก็เราควรจะ Sort ด้วย เพื่อให้การแสดงผลสมบูรณ์ขึ้น ผมได้ทำเป็น Module สำหรับ AngularJS เอาไว้แล้ว อยู่บน GitHub ครับ ผมใช้งานอยู่กับโปรเจคหนึ่ง ใช้งานได้ดี และเร็วมากครับ (ก็มันไม่ต้องโหลดนี่นา)

image

Code บน GitHub:

https://github.com/nantcom/thaiprovinces

แล้วมันไม่กินแรมเหรอ!?

เป็นคำถามที่ผมต้องตอบบ่อยมากในเรื่องการกินแรมครับ ผมขออธิบายด้วย 2 แนวคิดหลักๆ ก่อนครับ

  • RAM ไม่ใช่สิ่งมีค่าเหมือนสมัยก่อน
  • ปัญหาในคอมพิวเตอร์ แก้ได้แค่สองทาง คือ 1) ใช้จำ 2) ใช้การคำนวณ

ใช้แรมอย่างคุ้มค่า…?

เริ่มจากแนวคิดแรกก่อนนะครับ คือ สมัยที่แรมเรามีหลัก KB (1024MB = 1MB, 1024MB = 1GB) นั้น การเขียนโปรแกรมให้กินแรมน้อย เป็นสิ่งสำคัญมาก แต่ปัจจุบัน มือถือ Android ราคาถูก ก็มีแรมให้เราใช้อย่างน้อย 300MB แล้ว และการสร้าง Object ถึงแม้จะมีประมาณ 8000 Object ก็ไม่น่าจะกินแรมเกิน 1MB ครับ แต่เพื่อไขข้อข้องใจ เราก็ต้องตรวจสอบก่อนว่า สมมุติฐานเราเป็นจริงหรือไม่ด้วยเครื่องมือ Memory Profiler ที่มีอยู่ใน Internet Explorer ครับ

โดยการเปิดเครื่องมือนักพัฒนา (F12) แล้วกดที่ Tab “Memory” จากนั้นกดปุ่ม Play แล้วกดปุ่ม Take Heap Snapshot

image

แล้วจากนั้น เราก็เปิดดูว่า มี Object อะไรที่กินแรมอยู่บ้าง ก็จะพบว่า Object จังหวัดของเรานั้น ใช้แรมไปเพียง 200KB หรือประมาณ 0.2MB ครับ

image

ส่วนของ Chrome นั้น จะอยู่ใน Tab ชื่อว่า “Profiles” ซึ่งเราสามารถจับ Heap Snapshot ได้เช่นกัน โดยบน Chrome นั้น เราใช้แรมไป 260KB = 0.2MB เช่นกัน

image

image

เปรียบเทียบกับ Web Application อย่าง GMail แล้ว ผมคิดว่าวิธีนี้ก็ยังถือว่าอยู่ในเกณฑ์ที่รับได้ครับ

image

ดังนั้น เรื่องการกินแรม จึงไม่น่าจะเป็นประเด็นครับ เพราะ “แรมเราเยอะ” อยู่แล้ว Smile 

แล้ว Bandwidth ละ!?

ลอง GZip Fatest แล้ว เหลือ 100KB ครับ

image

แต่ประเด็นสำคัญก็คือ…

เราต้องเลือกระหว่าง กินพื้นที่ หรือ กินเวลา

ปัญหาในคอมพิวเตอร์นั้น มีทางแก้ไขได้เพียง 2 แนวทางเท่านั้น และส่วนใหญ่เราจะต้องมา Balance กันว่า เราเลือกจะใช้พื้นที่เยอะขึ้น เพื่อประหยัดเวลา หรือว่า จะประหยัดพื้นที่ แต่ต้องเสียเวลามากขึ้น ก็คือ Space Complexity (พื้นที่) จะแปรผกผันกับ Time Complexity (เวลา) เสมอ จากปัญหาเรื่องการเก็บข้อมูลอำเภอจังหวัดนี้ก็เช่นเดียวกันครับ เราก็พบว่า ทุกทางเลือก จะวนเวียนอยู่ภายในสองเรื่องนี้

  • ถ้าเราเก็บจังหวัดทั้งหมด อำเภอทั้งหมด ก็จะเปลืองแรม
  • ถ้าเราไม่เก็บจังหวัดเลย เราก็ต้องไปเรียกจังหวัดออกมาจาก Server ทุกครั้ง ซึ่งเสียเวลา

หรือถ้าเราใช้วิธีที่อยู่กึ่งกลางระหว่างสองทางนี้ ก็จะเป็นทางที่เสียเวลาพอดีๆ กับเสียแรมพอดีๆ แต่เราก็ไม่สามารถที่จะลดเวลาจนเหลือใกล้ 0 ในขณะที่แรมไม่เพิ่มขึ้นได้ และในทางกลับกัน เราก็ไม่สามารถลดแรมจนเหลือน้อยที่สุด โดยที่เวลาไม่เพิ่มขึ้นได้เช่นกัน

แน่นอนว่า ขึ้นอยู่กับการออกแบบระบบของเราครับ ว่าจะเลือกทางไหน สำหรับบทความนี้ และโปรเจคนี้ ผมเลือกให้กินแรมมากขึ้น เพื่อให้ประหยัดเวลาของผู้ใช้ เวลาที่ผู้ใช้เลือกอำเภอ/ตำบลมาแสดงผล และที่สำคัญคือ ประหยัดเวลาผม เพราะผมไม่ต้องเขียน API ฝั่ง Server ครับ! Open-mouthed smileOpen-mouthed smileOpen-mouthed smile

ขอบคุณทุกท่านที่ติดตามอ่านมาจนถึงตรงนี้นะครับ แล้วพบกันใหม่ครับ