Multi-Tenancy คืออะไรและทำไมมือใหม่ต้องรู้จัก
เวลาเราสร้างแอปพลิเคชันประเภท SaaS (Software as a Service - ซอฟต์แวร์ที่ให้บริการผ่านอินเทอร์เน็ตแบบเก็บค่าสมาชิก) สิ่งที่ต้องเจอแน่ๆ คือการแบ่งข้อมูลให้แต่ละบริษัทหรือแต่ละกลุ่มผู้ใช้เห็นเฉพาะข้อมูลของตัวเองเท่านั้น แนวคิดนี้เรียกว่า Multi-Tenancy (การรองรับผู้ใช้หลายกลุ่มในระบบเดียวโดยข้อมูลแยกจากกันเด็ดขาด) ซึ่งเปรียบเหมือนการสร้างตึกอพาร์ตเมนต์ที่ทุกห้องมีกุญแจของตัวเอง แม้จะอยู่ในตึกเดียวกันแต่คนห้อง 101 ก็ไม่สามารถเข้าไปเปิดประตูห้อง 202 ได้
สำหรับโปรแกรมเมอร์มือใหม่ หลายคนมักเข้าใจผิดว่าการทำระบบนี้ต้องแยก Database (ฐานข้อมูลที่เก็บข้อมูลทั้งหมดของแอป) ออกจากกันเป็นรายบริษัท ซึ่งเป็นวิธีที่ซับซ้อนและดูแลรักษายากมากโดยไม่จำเป็น ในความเป็นจริงสำหรับแอปส่วนใหญ่ เราสามารถใช้ฐานข้อมูลเดียว แต่ใช้วิธีการติดป้ายกำกับข้อมูลว่านี่เป็นของ "ทีมงาน" ไหน เพื่อให้ระบบคัดกรองข้อมูลให้เราโดยอัตโนมัติ
การเข้าใจเรื่องนี้จะช่วยให้คุณออกแบบโครงสร้างแอปได้สะอาดและขยายขีดความสามารถได้ง่ายในอนาคต โดยไม่ต้องไปพึ่งพา Package (ชุดคำสั่งที่คนอื่นเขียนไว้ให้เรานำมาใช้ต่อ) ขนาดใหญ่ที่อาจจะทำให้โปรเจกต์ของคุณหนักเกินไป เราจะมาเรียนรู้วิธีจัดการเรื่องนี้ใน Laravel (เฟรมเวิร์กภาษา PHP สำหรับพัฒนาเว็บแอปพลิเคชัน) แบบง่ายๆ ที่คุณสามารถทำตามได้ทันที
เตรียมระบบให้พร้อมด้วย Global Scopes
หัวใจสำคัญของ Global Scopes (การกำหนดเงื่อนไขการดึงข้อมูลที่จะติดตัวไปกับทุกคำสั่งดึงข้อมูล) คือการทำให้แอปของเรา "ฉลาด" พอที่จะกรองข้อมูลตามเจ้าของโดยที่เราไม่ต้องไปเขียนคำสั่งดึงข้อมูลซ้ำๆ ในทุกจุดของโค้ด เราจะใช้สิ่งที่เรียกว่า Trait (เครื่องมือใน PHP ที่ช่วยให้เรานำชุดคำสั่งมาใช้ซ้ำในคลาสต่างๆ ได้) เพื่อให้โมเดลของเราจำได้เสมอว่าต้องดึงข้อมูลของทีมไหนเท่านั้น
ลองนึกภาพว่าคุณมีพนักงานคอยคัดแยกจดหมาย ถ้าพนักงานคนนี้เห็นชื่อบริษัทบนซองจดหมาย เขาก็จะหยิบเฉพาะจดหมายของบริษัทนั้นมาให้คุณโดยที่คุณไม่ต้องสั่งเขาทุกครั้ง การใช้ Trait ร่วมกับ Global Scope ก็ทำหน้าที่แบบนั้น มันจะคอยดูว่าตอนนี้ผู้ใช้งานอยู่ใน "ทีม" ไหน แล้วแอบใส่คำสั่งกรองข้อมูลเพิ่มเข้าไปในทุกคำสั่งดึงข้อมูลจากฐานข้อมูลให้เราเอง
วิธีนี้จะช่วยป้องกันความผิดพลาดที่เรียกว่า Data Leakage (การที่ข้อมูลหลุดไปถึงผู้ใช้ที่ไม่ได้รับอนุญาต) ได้อย่างดีเยี่ยม เพราะถ้าเราเขียนโค้ดลืมใส่เงื่อนไขกรองข้อมูล ระบบก็จะยังคงทำหน้าที่กรองให้เราอยู่ดีผ่านเงื่อนไขกลางที่เรากำหนดไว้ใน Trait นี้เอง
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToTeam {
protected static function bootBelongsToTeam(): void {
// กรองข้อมูลเฉพาะทีมปัจจุบัน
static::addGlobalScope('team', function (Builder $builder) {
if (auth()->check() && auth()->user()->current_team_id) {
$builder->where('team_id', auth()->user()->current_team_id);
}
});
// กำหนดค่าทีมให้ข้อมูลใหม่โดยอัตโนมัติ
static::creating(function ($model) {
if (auth()->check() && !$model->team_id) {
$model->team_id = auth()->user()->current_team_id;
}
});
}
}
ในโค้ดส่วนแรก เราสร้าง Trait ชื่อ BelongsToTeam ขึ้นมาเพื่อใส่ฟังก์ชัน bootBelongsToTeam ซึ่งจะทำงานตอนที่โมเดลถูกเรียกใช้ คำสั่ง addGlobalScope จะคอยดึงเฉพาะข้อมูลที่มี team_id ตรงกับผู้ใช้ปัจจุบัน และฟังก์ชัน creating จะช่วยเติม team_id ให้ข้อมูลใหม่ที่ถูกสร้างขึ้นโดยอัตโนมัติ
ผลลัพธ์ที่ได้คือ เมื่อคุณพิมพ์คำสั่งดึงข้อมูลปกติ เช่น Post::all() ระบบจะแปลงเป็นคำสั่ง SQL ที่มีเงื่อนไข WHERE team_id = ... ให้เองโดยที่คุณไม่ต้องพิมพ์เพิ่มแม้แต่บรรทัดเดียว ทำให้โค้ดของคุณสะอาดและปลอดภัยกว่าเดิมมาก
ระวังปัญหาชื่อคอลัมน์ซ้ำซ้อนในฐานข้อมูล
ความผิดพลาดที่โปรแกรมเมอร์มือใหม่มักเจอคือการเขียนคำสั่งดึงข้อมูลที่มีความสัมพันธ์กัน หรือที่เรียกว่า Pivot Table (ตารางกลางที่ใช้เชื่อมความสัมพันธ์ระหว่างสองตาราง) แล้วไม่ได้ระบุชื่อตารางให้ชัดเจน ทำให้ระบบงงว่าตารางไหนมีคอลัมน์ชื่ออะไรกันแน่ ซึ่งจะทำให้เกิดข้อผิดพลาดที่เรียกว่า SQL Ambiguity (ความกำกวมของคำสั่งฐานข้อมูล) และแอปจะหยุดทำงานทันที
เปรียบเทียบง่ายๆ เหมือนคุณอยู่ในห้องประชุมที่มีคนชื่อ "สมชาย" สองคน ถ้าคุณตะโกนเรียก "สมชาย!" ทั้งสองคนจะหันมามองคุณพร้อมกันและไม่รู้ว่าคุณเรียกใคร แต่ถ้าคุณระบุว่า "สมชาย แผนกบัญชี!" คราวนี้ทุกคนก็จะเข้าใจตรงกันว่าคุณต้องการสื่อสารกับใคร การเขียนคำสั่งดึงข้อมูลก็ต้องระบุชื่อตารางนำหน้าชื่อคอลัมน์เสมอ
เราต้องฝึกนิสัยการเขียนโค้ดให้ชัดเจนตั้งแต่วันแรก เพราะเมื่อโปรเจกต์ของคุณใหญ่ขึ้น ความซับซ้อนของข้อมูลจะเพิ่มขึ้นตามไปด้วย การป้องกันปัญหาตั้งแต่ต้นจะช่วยลดเวลาในการ Debug (กระบวนการค้นหาและแก้ไขข้อผิดพลาดในโปรแกรม) ไปได้มหาศาล และทำให้เพื่อนร่วมทีมอ่านโค้ดของคุณได้ง่ายขึ้นด้วย
// แบบที่ผิด: อาจทำให้เกิด SQL Ambiguity ได้
if (!$user->teams()->where('team_id', $team->id)->exists()) {
abort(403);
}
// แบบที่ถูก: ระบุชื่อตารางชัดเจน ปลอดภัยกว่า
if (!$user->teams()->where('teams.id', $team->id)->exists()) {
abort(403);
}
ในตัวอย่างแรก เราเขียนแค่ where('team_id', ...) ซึ่งถ้าในตาราง users ก็มี team_id ด้วย ระบบจะแยกไม่ออกว่าหมายถึงตารางไหนกันแน่ แต่ในตัวอย่างที่สอง เราใช้ where('teams.id', ...) เพื่อเจาะจงว่าเรากำลังเช็คไอดีของตาราง teams เท่านั้น
ผลลัพธ์คือคำสั่งนี้จะทำงานได้อย่างแม่นยำไม่ว่าสถานการณ์ของข้อมูลจะซับซ้อนแค่ไหนก็ตาม และถ้าผู้ใช้พยายามเข้าถึงข้อมูลที่ไม่มีสิทธิ์ ระบบจะสั่ง abort(403) เพื่อหยุดการทำงานและแจ้งเตือนว่าผู้ใช้ไม่มีสิทธิ์เข้าถึงข้อมูลส่วนนี้ทันที
เชื่อมต่อข้อมูลกับหน้าบ้านด้วย Inertia.js
เมื่อเราจัดการเรื่องข้อมูลใน Backend (ส่วนของการประมวลผลหลังบ้าน) เรียบร้อยแล้ว ขั้นตอนต่อไปคือการส่งข้อมูลทีมที่ผู้ใช้กำลังใช้งานอยู่ไปให้ Frontend (ส่วนของหน้าเว็บไซต์ที่ผู้ใช้มองเห็น) ได้รับรู้ เพื่อให้หน้าเว็บแสดงเมนูสลับทีมหรือแสดงชื่อทีมปัจจุบันได้อย่างถูกต้องโดยไม่ต้องโหลดหน้าเว็บใหม่บ่อยๆ
เราจะใช้ Middleware (ชั้นตัวกลางที่คอยดักจับและจัดการคำขอข้อมูลก่อนถึงตัวแอป) ของ Inertia.js (เครื่องมือเชื่อมต่อ Laravel กับ React/Vue เพื่อสร้างแอปหน้าเดียว) เข้ามาช่วย เพื่อส่งข้อมูลผู้ใช้และข้อมูลทีมที่เขาสังกัดอยู่ไปให้ทุกหน้าเว็บโดยอัตโนมัติเหมือนการส่งจดหมายที่แนบรายละเอียดผู้รับไปให้ทุกครั้งที่เปิดหน้าเว็บขึ้นมา
การทำแบบนี้จะทำให้ State Management (การจัดการข้อมูลที่เปลี่ยนแปลงตลอดเวลาในหน้าเว็บ) ของเราง่ายขึ้นมาก เพราะไม่ว่าผู้ใช้จะเปลี่ยนหน้าไปที่ไหน ข้อมูลทีมปัจจุบันก็จะตามไปด้วยเสมอ ทำให้ประสบการณ์การใช้งานของผู้ใช้ราบรื่นและดูเป็นมืออาชีพมากขึ้นครับ
// ในไฟล์ app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array {
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user() ? array_merge($request->user()->toArray(), [
'current_team' => $request->user()->currentTeam,
'teams' => $request->user()->teams,
]) : null,
],
]);
}
โค้ดส่วนนี้คือการแทรกข้อมูลทีมเข้าไปในโครงสร้างของ Inertia ทำให้เราสามารถเรียกใช้งานผ่าน usePage().props.auth.user.current_team ในฝั่ง React (ไลบรารีสำหรับสร้างหน้าจอผู้ใช้) ได้ทันที โดยไม่ต้องไปเขียนคำสั่งดึงข้อมูลทีมใหม่ในทุกๆ หน้า
ผลลัพธ์ที่คุณจะเห็นคือ เมื่อคุณเปิดหน้าเว็บขึ้นมา ข้อมูลทีมทั้งหมดจะถูกส่งมาพร้อมกับหน้าเพจ คุณสามารถใช้ตัวแปรเหล่านี้ไปสร้างเมนู Dropdown (เมนูแบบดึงลง) เพื่อให้ผู้ใช้กดสลับทีมได้ทันทีโดยไม่ต้องเขียนคำสั่งดึงข้อมูลซ้ำซ้อนอีกต่อไป
ก้าวต่อไปสู่การเป็นโปรแกรมเมอร์มือโปร
การเรียนรู้เรื่อง Multi-Tenancy เป็นเพียงก้าวแรกของการสร้างซอฟต์แวร์ที่รองรับผู้ใช้งานจำนวนมาก สิ่งที่สำคัญที่สุดไม่ใช่เครื่องมือที่เราใช้ แต่คือการฝึกคิดแบบเป็นระบบและเข้าใจว่าทำไมเราถึงต้องเลือกวิธีนี้ วิธีที่ผมแนะนำไปเป็นพื้นฐานที่มั่นคงสำหรับโปรเจกต์ส่วนใหญ่ และช่วยให้คุณเติบโตไปสู่การสร้างระบบที่ซับซ้อนกว่านี้ได้ง่ายขึ้น
สำหรับคนที่กำลังฝึกเขียนโค้ด ผมอยากแนะนำให้ลองสร้างโปรเจกต์เล็กๆ ที่มีการจัดการข้อมูลหลายทีมดูครับ เริ่มจากการทำระบบจดบันทึกส่วนตัวที่แยกตาม "โปรเจกต์" ของเราเอง การลองลงมือทำด้วยตัวเองจะทำให้คุณเข้าใจปัญหาและวิธีแก้ที่แท้จริง มากกว่าการแค่อ่านบทความหรือดูวิดีโอสอนเพียงอย่างเดียว
อย่ากลัวที่จะทำผิดพลาดหรือเขียนโค้ดได้ไม่สวยในครั้งแรก การเป็นโปรแกรมเมอร์ที่เก่งคือการเขียนโค้ดให้ทำงานได้ก่อน แล้วค่อยปรับปรุงให้มันสะอาดและมีประสิทธิภาพขึ้นในภายหลัง การหมั่นฝึกฝนและตั้งคำถามกับโค้ดที่ตัวเองเขียน จะช่วยให้คุณเก่งขึ้นวันละนิดและก้าวสู่สายงานนี้ได้อย่างมั่นคงครับ
สรุป: เริ่มต้นง่ายๆ ก็ได้ผลลัพธ์ที่ดี
ทั้งหมดนี้คือวิธีสร้างระบบรองรับหลายทีมใน Laravel โดยไม่ต้องพึ่งพาตัวช่วยภายนอกที่ซับซ้อน เพียงแค่ใช้ Global Scope เพื่อกรองข้อมูล, ระบุชื่อตารางให้ชัดเจนเพื่อป้องกันข้อผิดพลาด, และใช้ Inertia.js เพื่อแชร์ข้อมูลทีมไปยังหน้าเว็บ ก็เพียงพอแล้วสำหรับแอปพลิเคชันส่วนใหญ่ที่คุณกำลังสร้างในฐานะโปรแกรมเมอร์รุ่นใหม่
ตัวอย่างการนำไปใช้จริง: ถ้าคุณกำลังสร้างเว็บจัดการงาน (Task Management) ให้คนในทีมใช้ คุณสามารถใช้ Trait BelongsToTeam ใส่ในโมเดล Task ได้เลย ทันทีที่พนักงานสร้างงานใหม่ ระบบจะจัดเก็บงานนั้นเข้าสู่ทีมของเขาโดยอัตโนมัติ และเมื่อเขาเปิดหน้าเว็บขึ้นมา เขาก็จะเห็นเฉพาะงานของทีมตัวเองเท่านั้นโดยไม่ต้องเขียนโค้ดกรองข้อมูลซ้ำๆ
จำไว้ว่าความเรียบง่ายคือหัวใจของซอฟต์แวร์ที่ดี การเลือกใช้เครื่องมือที่เหมาะสมและเข้าใจโครงสร้างพื้นฐานจะช่วยให้คุณพัฒนาแอปได้เร็วและดูแลรักษาง่ายในระยะยาว ขอให้สนุกกับการเขียนโค้ดและสร้างโปรเจกต์ที่น่าภูมิใจออกมาให้โลกได้เห็นครับ หากมีข้อสงสัยหรือติดขัดตรงไหน อย่าลังเลที่จะลองผิดลองถูกและค้นหาคำตอบด้วยตัวเอง เพราะนั่นคือเส้นทางที่โปรแกรมเมอร์ทุกคนต้องเดินผ่าน
ที่มา: Multi-Tenancy Made Simple — DEV Community: laravel