MySQL وتصميم قواعد البيانات
تصميم قاعدة البيانات متعددة المستأجرين
تصميم قاعدة البيانات متعددة المستأجرين
تعتبر بنية المستأجرين المتعددة ضرورية لتطبيقات SaaS حيث يخدم مثيل تطبيق واحد عدة عملاء (مستأجرين). في هذا الدرس، سنستكشف ثلاثة مناهج للمستأجرين المتعددين ومزاياها وعيوبها: قاعدة بيانات مشتركة مع معرف المستأجر، وقواعد بيانات منفصلة، ومخططات منفصلة.
مناهج المستأجرين المتعددة
كل نهج له مزايا وتحديات مميزة:
النهج 1: قاعدة بيانات مشتركة + معرف المستأجر
✓ الأكثر فعالية من حيث التكلفة
✓ سهل الصيانة
✗ خطر تسرب البيانات
✗ صعوبة التخصيص لكل مستأجر
النهج 2: قاعدة بيانات منفصلة لكل مستأجر
✓ عزل كامل للبيانات
✓ سهل النسخ الاحتياطي/الاستعادة لكل مستأجر
✓ يمكن تخصيص المخطط لكل مستأجر
✗ تكاليف أعلى
✗ نشر معقد
النهج 3: مخطط منفصل لكل مستأجر
✓ عزل جيد داخل قاعدة بيانات واحدة
✓ تكاليف معتدلة
✗ ميزة خاصة بقاعدة البيانات
✗ قابلية التوسع محدودة
النهج 1: قاعدة بيانات مشتركة مع معرف المستأجر
النهج الأكثر شيوعاً لتطبيقات SaaS:
-- جدول المستأجرين
CREATE TABLE tenants (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
custom_domain VARCHAR(255) UNIQUE NULL,
plan ENUM('free', 'basic', 'pro', 'enterprise') DEFAULT 'free',
status ENUM('active', 'suspended', 'trial', 'cancelled') DEFAULT 'trial',
max_users INT UNSIGNED DEFAULT 5,
max_storage_mb INT UNSIGNED DEFAULT 1000,
trial_ends_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_subdomain (subdomain),
INDEX idx_status (status)
) ENGINE=InnoDB;
-- المستخدمون (كل منهم ينتمي إلى مستأجر)
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
role ENUM('owner', 'admin', 'user', 'guest') DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE KEY unique_email_per_tenant (tenant_id, email),
INDEX idx_tenant (tenant_id),
INDEX idx_email (email)
) ENGINE=InnoDB;
-- المشاريع (محدودة للمستأجر)
CREATE TABLE projects (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
status ENUM('active', 'archived', 'completed') DEFAULT 'active',
created_by BIGINT UNSIGNED NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT,
INDEX idx_tenant (tenant_id),
INDEX idx_status (tenant_id, status)
) ENGINE=InnoDB;
-- المهام (محدودة للمستأجر)
CREATE TABLE tasks (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
project_id BIGINT UNSIGNED NOT NULL,
assigned_to BIGINT UNSIGNED NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
status ENUM('todo', 'in_progress', 'review', 'done') DEFAULT 'todo',
priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
due_date DATE NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_tenant (tenant_id),
INDEX idx_project (project_id),
INDEX idx_assigned (assigned_to),
INDEX idx_status (tenant_id, status)
) ENGINE=InnoDB;
-- إعدادات المستأجر (تخزين مفتاح-قيمة)
CREATE TABLE tenant_settings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
setting_key VARCHAR(100) NOT NULL,
setting_value TEXT,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE KEY unique_setting (tenant_id, setting_key),
INDEX idx_tenant (tenant_id)
) ENGINE=InnoDB;
-- تتبع الاستخدام (للفوترة)
CREATE TABLE tenant_usage (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
metric_type ENUM('storage', 'users', 'api_calls', 'projects') NOT NULL,
metric_value BIGINT UNSIGNED NOT NULL,
recorded_at DATE NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE KEY unique_daily_metric (tenant_id, metric_type, recorded_at),
INDEX idx_tenant_date (tenant_id, recorded_at)
) ENGINE=InnoDB;
-- سجل التدقيق (تتبع جميع إجراءات المستأجر)
CREATE TABLE audit_logs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id INT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NULL,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id BIGINT UNSIGNED NOT NULL,
old_values JSON NULL,
new_values JSON NULL,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_tenant_created (tenant_id, created_at),
INDEX idx_entity (entity_type, entity_id),
INDEX idx_user (user_id)
) ENGINE=InnoDB;
قاعدة أمان حرجة: قم دائماً بتضمين tenant_id في كل جملة WHERE وشرط JOIN. قد يؤدي فقدان فلتر المستأجر إلى كشف البيانات للمستأجر الخطأ!
الاستعلامات الأساسية مع عزل المستأجر
يجب أن يقوم كل استعلام بالتصفية حسب tenant_id:
-- الحصول على مشاريع المستأجر (صحيح)
SELECT * FROM projects
WHERE tenant_id = 1
ORDER BY created_at DESC;
-- الحصول على مهام المشروع (صحيح - تحقق مزدوج)
SELECT t.*
FROM tasks t
JOIN projects p ON t.project_id = p.id
WHERE t.tenant_id = 1
AND p.tenant_id = 1
AND t.status = 'in_progress';
-- الحصول على المهام المعينة للمستخدم (صحيح)
SELECT t.*
FROM tasks t
WHERE t.tenant_id = 1
AND t.assigned_to = 5
ORDER BY t.due_date;
-- خطأ - فلتر tenant_id مفقود (خطأ أمني!)
SELECT * FROM tasks WHERE status = 'todo';
-- سياق المستأجر على مستوى التطبيق
SET @current_tenant_id = 1;
-- استخدام متغير الجلسة في الاستعلامات
SELECT * FROM projects
WHERE tenant_id = @current_tenant_id;
أفضل الممارسات: استخدم طرق عرض قاعدة البيانات أو البرمجيات الوسيطة على مستوى التطبيق لحقن فلاتر tenant_id تلقائياً، مما يقلل من خطر نسيان الفلتر.
النهج 2: قاعدة بيانات منفصلة لكل مستأجر
عزل كامل مع قواعد بيانات منفصلة:
-- قاعدة البيانات الرئيسية: سجل المستأجرين
CREATE DATABASE saas_master;
USE saas_master;
CREATE TABLE tenants (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
subdomain VARCHAR(100) UNIQUE NOT NULL,
database_name VARCHAR(100) UNIQUE NOT NULL,
database_host VARCHAR(255) DEFAULT 'localhost',
status ENUM('active', 'suspended', 'trial') DEFAULT 'trial',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_subdomain (subdomain),
INDEX idx_database (database_name)
) ENGINE=InnoDB;
-- إنشاء قاعدة بيانات المستأجر ديناميكياً
SET @tenant_db = 'tenant_1_db';
SET @sql = CONCAT('CREATE DATABASE ', @tenant_db);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- التبديل إلى قاعدة بيانات المستأجر
USE tenant_1_db;
-- مخطط قياسي (نفس الشيء لجميع المستأجرين)
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
full_name VARCHAR(100) NOT NULL,
role ENUM('owner', 'admin', 'user') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE projects (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
description TEXT,
status ENUM('active', 'archived') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- لا حاجة لـ tenant_id - عزل كامل!
استراتيجية النشر: استخدم نصوص ترحيل قاعدة البيانات التي تعمل ضد كل قاعدة بيانات مستأجر. تتبع إصدارات الترحيل لكل مستأجر في قاعدة البيانات الرئيسية.
سير عمل توفير المستأجر
عملية الإعداد الكاملة للمستأجر:
-- الخطوة 1: إنشاء مستأجر
INSERT INTO tenants (name, subdomain, plan, status, trial_ends_at)
VALUES (
'Acme Corp',
'acme',
'trial',
'active',
DATE_ADD(NOW(), INTERVAL 14 DAY)
);
SET @tenant_id = LAST_INSERT_ID();
-- الخطوة 2: إنشاء مستخدم المالك
INSERT INTO users (tenant_id, email, password_hash, full_name, role)
VALUES (
@tenant_id,
'owner@acme.com',
'$2y$10$...',
'John Doe',
'owner'
);
-- الخطوة 3: تهيئة الإعدادات الافتراضية
INSERT INTO tenant_settings (tenant_id, setting_key, setting_value)
VALUES
(@tenant_id, 'timezone', 'UTC'),
(@tenant_id, 'date_format', 'Y-m-d'),
(@tenant_id, 'language', 'en');
-- الخطوة 4: إنشاء مشروع نموذجي
INSERT INTO projects (tenant_id, name, description, created_by)
SELECT
@tenant_id,
'مشروع الترحيب',
'مشروعك الأول',
id
FROM users
WHERE tenant_id = @tenant_id AND role = 'owner';
تمرين تطبيقي:
المهمة: إضافة نظام علامات الميزات للمستأجر.
المتطلبات:
- إنشاء جدول علامات الميزات
- تمكين/تعطيل الميزات لكل مستأجر
- الميزات الافتراضية للمستأجرين الجدد
- استعلام للتحقق مما إذا كان المستأجر لديه وصول إلى الميزة
الحل:
CREATE TABLE features (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
CREATE TABLE tenant_features (
tenant_id INT UNSIGNED NOT NULL,
feature_id INT UNSIGNED NOT NULL,
is_enabled BOOLEAN DEFAULT TRUE,
enabled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (tenant_id, feature_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE
) ENGINE=InnoDB;
-- التحقق مما إذا كان المستأجر لديه ميزة
SELECT EXISTS(
SELECT 1 FROM tenant_features
WHERE tenant_id = 1
AND feature_id = (SELECT id FROM features WHERE name = 'advanced_reporting')
AND is_enabled = TRUE
) AS has_feature;
-- الحصول على جميع الميزات الممكّنة للمستأجر
SELECT f.name, f.description
FROM features f
JOIN tenant_features tf ON f.id = tf.feature_id
WHERE tf.tenant_id = 1 AND tf.is_enabled = TRUE;
الملخص
في هذا الدرس، تعلمت:
- ثلاثة مناهج لتصميم قاعدة البيانات متعددة المستأجرين
- تنفيذ قاعدة بيانات مشتركة مع عزل معرف المستأجر
- أفضل ممارسات الأمان لمنع تسرب البيانات
- بنية قاعدة بيانات منفصلة لكل مستأجر
- سير عمل توفير وإعداد المستأجر
- تتبع الاستخدام وحسابات الفوترة
- إعداد التقارير عبر المستأجرين للمسؤولين
التالي: في الدرس التالي، سنصمم قواعد بيانات السلاسل الزمنية والتحليلات للتعامل مع البيانات واسعة النطاق!