ช่วงว่าง ๆ เห็นบทความเรื่อง Practical Persistence in Go: Organising Database Access
ไม่แน่ใจว่าใคร share มา แต่เมื่อได้อ่านและลองทำตามแล้วพบว่า
มีสิ่งที่น่าสนใจและน่าเรียนรู้มาก ๆ
จึงทำการแปลและสรุปไว้อ่านนิดหน่อย
มาเริ่มกันเลย
เริ่มจากที่มาของบทความคือ
มีการถามใน Reddit ว่า
ในการจัดการข้อมูลใน database ของระบบ web application
ที่พัฒนาด้วยภาษา Go นั้น
มีแนวปฏิบัติที่ดีหรือ best practice อะไรและอย่างไรบ้าง ?
ซึ่งในการพุดคุยนี้ ก็มีคำตอบที่น่าสนใจจำนวนมาก
ยกตัวอย่างเช่น
- ใช้งาน dependency injection
- ใช้งานง่าย ๆ ผ่าน Global variable
- ใช้ connection pool โดยใช้งานผ่าน pointer ของ Context
ในบทความผู้เขียนบอกว่า
ไม่ว่าจะวิธีใดก็ตาม มันขึ้นอยู่กับ project นั้น ๆ มากกว่าว่าเป็นอย่างไร
ทั้งโครงสร้างและขนาดของ project
ทั้งรูปแบบของการทดสอบ
ทั้งแนวทางในการขยายในอนาคต
เป็นสิ่งสำคัญมาก ๆ ต่อการเลือกแนวทาง
การจัดการข้อมูลใน database
ดังนั้นมาดูว่าแต่ละวิธีเป็นอย่างไร
มีข้อดีและข้อเสียอย่างไร ตลอดจน code ตัวอย่างอีกด้วย
ในบทความประกอบไปด้วย 4 วิธีดังนี้
- Global variable
- Dependency Injection
- Interface
- Request-scopes context
วิธีที่ 1 Global variable
เป็นวิธีที่ง่ายและตรงไปตรงมาสุด ๆ
นั่นคือการจัดเก็บการเชื่อมต่อไปยัง database ไว้ใน Global variable
ซึ่ง Global variable นี้สามารถใช้งานได้จากตรงส่วนไหนของ code ได้เลย
นั่นคือจะมีส่วนการสร้างการเชื่อมต่อ database ไว้ที่เดียว
ส่วนมากจะทำการสร้างไว้ตอน program เริ่มทำงาน
จากนั้นสามารถใช้งานได้ทั้ง production code และ test code เลย
มาดูตัวอย่าง code ที่ใช้งาน Global variable
ทำการสร้าง Global variable ชื่อ db ไว้ใน package models ดังนี้
ตัวแปร db นี้สามารถถูกใช้จาก package ต่าง ๆ ได้เลย ดังตัวอย่าง
[gist id="80481b450efcb1282b79fd3af7e12870" file="2.go"]โดยที่วิธีการนี้มีมีข้อดีดังนี้
- ส่วนของการจัดการกับ database อยู่ที่เดียว นั่นคือจัดการได้ง่าย
- เหมาะกับระบบงานที่มีขนาดเล็ก เนื่องจากง่ายต่อการดูว่า Global variable ถูกใช้งานอย่างไร แต่ถ้าใหญ่ขึ้นการดูแลจะยากขึ้น
- ในการทดสอบนั้น เราจะใช้งาน database จริง ๆ เหมือนกับ production code ไม่ทำการ mock database
วิธีที่ 2 Dependency Injection
วิธีการนี้ คือ ทำการส่ง pointer ของการเชื่อมต่อ database ไปยัง HTTP handler
จากนั้นก็ถูกส่งไปยัง business logic อีกต่อหนึ่ง
ซึ่งค่าที่ส่งเข้ามาอาจจะเป็นสิ่งอื่น ๆ ที่ต้องใช้งานอีกก็ได้
ยกตัวอย่างเช่น logger และ caching เป็นต้น
ยกตัวอย่าง code ของสิ่งที่จะส่งเข้ามา
สามารถเขียน code ในรูปแบบของ struct ได้ดังนี้
จากนั้นทำการสร้างสิ่งต่าง ๆ ที่ struct ตัวนี้ต้องการทั้งหมด
ในตัวอย่างคือ การเชื่อมต่อไปยัง database
จากนั้นทำการสร้าง struct และส่ง struct เข้าไปยัง HTTP handler
ซึ่งการส่งค่าในภาษา Go นั้น ทำได้ 2 แบบคือ
receiver และ closure (เป็นการส่งผ่าน parameter) ดังนี้
จากวิธีการนี้จะเห็นได้ว่า
เราสามารถกำหนดได้ว่า ในแต่ละ HTTP handler ต้องการใช้อะไรบ้าง
ก็ส่งไปเพียงเท่านั้น ไม่สิ้นเปลือง
ส่วนการทดสอบนั้น เราจะใช้งาน database จริง ๆ เหมือนกับ production code
ไม่ทำการ mock database ได้
เนื่องจากต้องทำการสร้าง database ขึ้นมาจริง ๆ เท่านั้น
แต่ fake ได้นะ คือการจำลอง database ขึ้นมา
วิธีที่ 3 Interface
เป็นวิธีการที่ปรับปรุงจากการใช้งาน Depenency Injection มานิดหน่อย
แทนที่ใน struct จะประกอบไปด้วย การเชื่อมต่อไปยัง database จริง ๆ
ก็เปลี่ยนไปใช้ interface แทน
ส่งผลให้เราสามารถจำลอง database ใด ๆ ก็ตามเข้ามาได้เลย
ช่วยทำให้ทดสอบได้ง่ายขึ้น ไม่ต้องไปผูกติดกับ database จริง ๆ
มาดูตัวอย่าง code กัน
เริ่มด้วยการสร้าง interface ชื่อว่า Datastore ขึ้นมา
ซึ่งมี function 1 ตัวคือ allBooks()
สำหรับดึงข้อมูลของหนังสือทั้งหมดจาก database ออกมา
ในส่วนของ books.go นั้น
จะเห็นได้ว่ามี function ที่มี signature เดียวกับ interface Datastore
(มันคือการ implementation นั่นเอง)
ตรงนี้อาจจะทำให้มือใหม่งง ๆ กันสักหน่อย
ช่วยทำให้เราสามารถเขียนชุดการทดสอบแบบ Unit test ได้ง่าย ๆ ดังนี้
[gist id="80481b450efcb1282b79fd3af7e12870" file="7.go"]วิธีที่ 4 Request-scoped context
วิธีการนี้จะทำการส่งและเก็บการเชื่อมต่อกับ database ผ่าน context package นั่นเอง
โดยที่เจ้าของ blog ต้นทางบอกว่า ไม่ชอบวิธีการนี้
เพราะว่ามันดูยุ่งยากและวุ่นวายเกินไป
ในเอกสารของ context package นั้น อธิบายไว้ว่า
Use context Values only for request-scoped data that transits processes and APIs,
not for passing optional parameters to functions.
นั่นคือส่งค่าเฉพาะสิ่งที่จะใช้ในการทำงานของ API เท่านั้น
ส่วนค่าอื่น ๆ ที่ส่งไปยัง function อื่น ๆ อย่างส่งมาเลย
มาดูตัวอย่างกัน ซึ่งจะส่งข้อมูลของ struct ContextInjector ผ่าน context package
[gist id="80481b450efcb1282b79fd3af7e12870" file="8.go"]ตัวอย่างของ code อยู่ใน GitHub::Up1