Multi-Tenancy ทำให้เป็นเรื่องง่าย

11 นาที 16 views บันทึกเป็น PDF
Multi-Tenancy ทำให้เป็นเรื่องง่าย

Multi-Tenancy คืออะไรและทำไมมือใหม่ต้องรู้จัก...

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

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เรียนรู้วิธีเขียนแอป Flutter เชื่อมต่อ Meta Smart Glasses ให้ทำงานเร็ว ไม่กระตุก ด้วยการวางสถาปัตยกรรมโค้ดและการจัดการข้อมูลแบบมือโปรที่มือใหม่ทำตามได้จริง

ที่มา: DEV Community

6 hours ago 10 นาที
5 views
เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

อยากทำระบบ Real-Time แต่ไม่รู้จะเลือกใช้ Polling, SSE หรือ WebSocket ดี? มาดูวิธีเลือกใช้ให้เหมาะกับงาน เพื่อให้แอปของคุณทำงานลื่นไหลและประหยัดทรัพยากรเซิร์ฟเวอร์

ที่มา: DEV Community

10 hours ago 10 นาที
4 views