เจาะลึกระบบ Database-per-Tenant ใน Laravel สำหรับงาน SaaS ระดับองค์กร

9 นาที 12 views บันทึกเป็น PDF
เจาะลึกระบบ Database-per-Tenant ใน Laravel สำหรับงาน SaaS ระดับองค์กร

ทำความรู้จัก Database-per-Tenant วิธีแยกฐานข้อมูลให้ลูกค้าแต่ละรายใน Laravel เพื่อความปลอดภัยสูงสุดของข้อมูล พร้อมวิธีจัดการ Migration สำหรับมือใหม่

ทำไมต้องแยกฐานข้อมูลให้แต่ละลูกค้า (Database-per-Tenant)

เวลาเราทำซอฟต์แวร์แบบ SaaS (ซอฟต์แวร์ที่ให้บริการผ่านอินเทอร์เน็ตโดยเก็บค่าสมาชิก) ให้กับองค์กรใหญ่ๆ อย่างธนาคารหรือหน่วยงานรัฐ ความปลอดภัยของข้อมูลคือเรื่องสำคัญที่สุด ปกติเรามักจะใช้ฐานข้อมูลชุดเดียวแล้วเพิ่มคอลัมน์ (ช่องเก็บข้อมูล) ชื่อว่า tenant_id เพื่อแยกข้อมูลลูกค้าออกจากกัน แต่วิธีนี้มีความเสี่ยงสูงหากโปรแกรมเมอร์ลืมเขียนคำสั่งกรองข้อมูลให้ครบ

การแยกฐานข้อมูลแบบ Database-per-Tenant (การให้ลูกค้าแต่ละรายมีฐานข้อมูลส่วนตัวแยกจากกัน) จึงเป็นทางออกที่ตอบโจทย์ความปลอดภัยสูงสุด เปรียบเทียบง่ายๆ เหมือนการเช่าหอพัก ถ้าทุกคนใช้ห้องน้ำรวม (ฐานข้อมูลรวม) โอกาสที่คนจะเดินเข้าผิดห้องหรือเห็นของคนอื่นก็มีสูง แต่ถ้าเราให้ทุกคนมีห้องน้ำส่วนตัวในห้องพักตัวเอง ต่อให้เกิดความผิดพลาดขึ้น ข้อมูลของลูกค้า A ก็ไม่มีทางรั่วไหลไปหาลูกค้า B ได้เลย

นอกจากเรื่องความปลอดภัย ลูกค้ากลุ่มองค์กรใหญ่ยังมีกฎหมายเรื่องที่ตั้งข้อมูล (Data Residency) เช่น ลูกค้าในยุโรปต้องเก็บข้อมูลไว้ในเซิร์ฟเวอร์ที่ยุโรปเท่านั้น การแยกฐานข้อมูลช่วยให้เราย้ายฐานข้อมูลของลูกค้าบางรายไปไว้ในเครื่องเซิร์ฟเวอร์ที่กำหนดได้ง่ายขึ้น โดยไม่ต้องยุ่งกับโค้ดหลักของโปรแกรมเลย

โครงสร้างระบบ: Landlord และ Tenant

หัวใจสำคัญของระบบนี้คือการแบ่งฐานข้อมูลออกเป็นสองประเภท คือ Landlord (ฐานข้อมูลหลักที่เก็บรายชื่อลูกค้าทั้งหมด) และ Tenant (ฐานข้อมูลย่อยของลูกค้าแต่ละราย) เราต้องมีระบบคอยตรวจจับว่าคนที่เข้ามาใช้งานคือใคร เพื่อให้โปรแกรมเชื่อมต่อฐานข้อมูลได้ถูกต้อง

เมื่อมีคำขอ (Request) เข้ามาผ่านโดเมน (ที่อยู่เว็บไซต์) เช่น acme.smarttechdevs.in ระบบจะเข้าไปดูในฐานข้อมูล Landlord เพื่อหาว่าโดเมนนี้เป็นของลูกค้าคนไหน ข้อมูลการเชื่อมต่อฐานข้อมูลส่วนตัวของลูกค้ารายนั้นอยู่ที่ไหน จากนั้นโปรแกรมจะสั่งเปลี่ยนการเชื่อมต่อ (Dynamic Connection) ให้ไปที่ฐานข้อมูลของลูกค้ารายนั้นทันที

การจัดการแบบนี้ต้องใช้ Middleware (ตัวกลางที่ดักจับคำขอก่อนถึงหน้าเว็บ) เพื่อตรวจสอบชื่อโดเมน หากไม่พบฐานข้อมูลที่ระบุไว้ ระบบควรแจ้งเตือนข้อผิดพลาดทันที เพื่อป้องกันไม่ให้ข้อมูลแสดงผลผิดพลาดหรือเข้าถึงข้อมูลที่ไม่ได้รับอนุญาต

// ตัวอย่างโค้ดใน Middleware เพื่อสลับฐานข้อมูล
$subdomain = explode('.', $request->getHost())[0];
$tenant = Tenant::where('subdomain', $subdomain)->first();

Config::set('database.connections.tenant', [
    'driver' => 'mysql',
    'host' => $tenant->db_host,
    'database' => $tenant->db_name,
    'username' => $tenant->db_username,
    'password' => decrypt($tenant->db_password),
]);
DB::setDefaultConnection('tenant');

ในโค้ดนี้ บรรทัดแรกคือการดึงชื่อโดเมนย่อยออกมา บรรทัดต่อมาคือการหาข้อมูลฐานข้อมูลใน Landlord จากนั้นใช้ฟังก์ชัน Config::set เพื่อตั้งค่าการเชื่อมต่อใหม่ในหน่วยความจำ และ DB::setDefaultConnection เพื่อบอกให้ Laravel ใช้ฐานข้อมูลนี้แทนอันเดิม

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

รับมือกับปัญหาระบบจัดการฐานข้อมูล (Migration Crisis)

ความท้าทายใหญ่ของการแยกฐานข้อมูลคือเรื่อง Migration (ขั้นตอนการอัปเดตโครงสร้างตารางฐานข้อมูล) ถ้าคุณมีลูกค้า 500 ราย คุณไม่สามารถรันคำสั่งปกติได้ เพราะมันจะอัปเดตแค่ฐานข้อมูลหลักเท่านั้น เราจึงต้องสร้างคำสั่งพิเศษขึ้นมาจัดการเรื่องนี้โดยเฉพาะ

เราต้องเขียนคำสั่งแบบ Artisan Command (คำสั่งที่รันผ่านหน้าจอ Terminal ของ Laravel) ที่ทำหน้าที่ไล่ลูป (การทำซ้ำ) ไปยังฐานข้อมูลลูกค้าทุกคนเพื่อรันการอัปเดต ถ้าเราลืมรันให้ฐานข้อมูลใดฐานข้อมูลหนึ่ง ระบบของลูกค้ารายนั้นอาจจะพังทันทีเมื่อโปรแกรมต้องการโครงสร้างตารางแบบใหม่

คำสั่งนี้ต้องมีความแม่นยำสูงและต้องมีระบบบันทึกผล (Logging) ว่าฐานข้อมูลไหนรันสำเร็จหรือล้มเหลว การทำแบบนี้ช่วยลดความผิดพลาดจากคนได้มาก และทำให้เรามั่นใจว่าฐานข้อมูลของลูกค้าทุกคนมีโครงสร้างที่เหมือนกันและอัปเดตเป็นเวอร์ชันล่าสุดพร้อมๆ กัน

// คำสั่งรัน Migration ให้ทุกฐานข้อมูลลูกค้า
foreach ($tenants as $tenant) {
    Config::set('database.connections.tenant', [...]);
    DB::purge('tenant'); // ล้างค่าการเชื่อมต่อเก่าทิ้ง
    Artisan::call('migrate', ['--database' => 'tenant', '--force' => true]);
}

บรรทัดแรกคือการวนลูปรายชื่อลูกค้า ต่อมาคือการตั้งค่าเชื่อมต่อใหม่เหมือนใน Middleware ส่วน DB::purge สำคัญมากเพราะเป็นการสั่งให้ Laravel ลืมการเชื่อมต่อเดิมทิ้งไปก่อนจะเริ่มเชื่อมต่อใหม่ และ Artisan::call คือการสั่งรันคำสั่งอัปเดตฐานข้อมูลให้เสร็จสิ้น

ผลลัพธ์คือโปรแกรมจะรันการอัปเดตให้ฐานข้อมูลลูกค้าทุกคนทีละรายจนครบโดยอัตโนมัติ ช่วยลดงาน manual ที่แสนน่าเบื่อและผิดพลาดง่ายลงได้มหาศาล

การจัดการงานเบื้องหลัง (Asynchronous Queues)

งานเบื้องหลังหรือ Queue (งานที่ให้ระบบทำค้างไว้โดยไม่รอให้เสร็จทันที) เป็นอีกจุดที่มือใหม่มักพลาด เพราะ Worker (กระบวนการที่คอยทำงานเบื้องหลัง) มักจะเริ่มทำงานด้วยค่าเริ่มต้นของระบบ ซึ่งไม่ใช่ฐานข้อมูลของลูกค้าแต่ละราย

วิธีแก้คือเราต้องส่งค่า tenant_id ติดไปกับข้อมูลของงานนั้นๆ ด้วย เมื่อ Worker หยิบงานขึ้นมาทำ มันต้องอ่านค่า tenant_id แล้วสั่งสลับการเชื่อมต่อฐานข้อมูลให้ถูกต้องก่อนที่จะเริ่มประมวลผลธุรกิจ เปรียบเหมือนพนักงานส่งของที่ต้องดูชื่อลูกค้าบนกล่องก่อนจะเดินไปส่งให้ถูกบ้าน

หากเราไม่จัดการเรื่องนี้ งานเบื้องหลังจะพยายามบันทึกข้อมูลลงในฐานข้อมูลหลักของระบบ (Landlord) แทนที่จะเป็นฐานข้อมูลของลูกค้า ทำให้ข้อมูลปนกันมั่วไปหมดหรือทำงานล้มเหลวตั้งแต่ขั้นตอนแรก

ข้อควรระวังสำหรับโปรแกรมเมอร์มือใหม่

การทำ Database-per-Tenant เพิ่มความซับซ้อนให้ระบบอย่างมาก อย่าเพิ่งรีบทำหากโปรเจกต์ของคุณยังอยู่ในช่วงเริ่มต้นหรือมีลูกค้าไม่กี่ราย เพราะค่าใช้จ่ายในการดูแลเซิร์ฟเวอร์จะพุ่งสูงขึ้นตามจำนวนฐานข้อมูลที่เพิ่มขึ้นทันที

จุดที่มือใหม่มักพลาดคือการลืม DB::purge ทำให้ระบบจำการเชื่อมต่อเก่าของลูกค้าคนก่อนหน้าไว้ และเผลอเขียนข้อมูลลงผิดฐานข้อมูล นี่คือข้อผิดพลาดร้ายแรงที่อาจทำให้ข้อมูลลูกค้าหายหรือรั่วไหลได้ ดังนั้นต้องทดสอบระบบหลายรอบก่อนนำไปใช้จริง

นอกจากนี้ การจัดการเรื่อง Backup (การสำรองข้อมูล) ก็ต้องทำเป็นระบบ เพราะแต่ละฐานข้อมูลมีขนาดต่างกันและอาจต้องการเวลาสำรองข้อมูลที่ต่างกัน หากคุณยังไม่คุ้นเคยกับการจัดการเซิร์ฟเวอร์ แนะนำให้เริ่มจากระบบฐานข้อมูลเดียวให้คล่องก่อน แล้วค่อยขยับขยายเมื่อจำเป็นต้องรับงานระดับองค์กรจริงๆ

สรุป: เมื่อไหร่ที่ต้องเลือกทางนี้

เลือกใช้ Database-per-Tenant ก็ต่อเมื่อลูกค้าของคุณคือองค์กรใหญ่ที่ต้องการความปลอดภัยระดับสูงสุด หรือมีกฎหมายบังคับเรื่องการแยกข้อมูลอย่างชัดเจน หากคุณกำลังทำโปรเจกต์ส่วนตัวหรือสตาร์ทอัพเริ่มต้น ให้ใช้ระบบฐานข้อมูลรวมไปก่อนเพื่อประหยัดเวลาและทรัพยากร

ตัวอย่างการนำไปใช้จริง: หากคุณกำลังพัฒนาโปรแกรมบัญชีให้บริษัทขนาดเล็ก การใช้ tenant_id ก็เพียงพอแล้ว แต่ถ้าวันหนึ่งคุณมีลูกค้าเป็น "ธนาคาร" ที่มีกฎระเบียบเข้มงวด คุณค่อยวางแผนย้ายระบบไปเป็นแบบแยกฐานข้อมูลเพื่อให้ผ่านการตรวจสอบความปลอดภัย

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


ที่มา: Absolute Isolation: Database-per-Tenant in Laravel 🗄️ — DEV Community: laravel

แชร์บทความ

Facebook X LINE

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

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

เบื่อไหมที่ต้องสลับหน้าจอไปมาเพื่อจัดการเซิร์ฟเวอร์? มาลองใช้ Easy SSH ปลั๊กอิน VS Code ที่ช่วยให้คุณรีโมทผ่าน Terminal ได้สะดวก แถมโหลดไฟล์ได้ง่ายแค่กด Ctrl+click

ที่มา: DEV Community

6 hours ago 11 นาที
3 views
วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

อยากทำแอปท่องเที่ยวแต่ดึงข้อมูลราคาจาก Google Hotels ไม่ได้? มาดูวิธีใช้ Apify Actor ช่วยดึงข้อมูลแบบอัตโนมัติด้วย Python ง่ายๆ ไม่ต้องกลัวเว็บพัง

ที่มา: DEV Community

10 hours ago 8 นาที
5 views
วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

เคยไหม? โค้ดรันได้ในเครื่องแต่พอปล่อยจริงกลับพัง! มาดูวิธีตรวจสอบความพร้อมของโปรเจกต์ก่อนอัปขึ้น GitHub ด้วยเครื่องมือ ReleaseReady กัน

ที่มา: DEV Community

14 hours ago 9 นาที
6 views