MySQL وتصميم قواعد البيانات

القيود وسلامة البيانات

13 دقيقة الدرس 29 من 40

القيود وسلامة البيانات

قيود قاعدة البيانات هي قواعد يتم فرضها على مستوى قاعدة البيانات للحفاظ على دقة البيانات واتساقها وموثوقيتها. تعمل كحراس، تمنع البيانات غير الصالحة من الدخول إلى قاعدة البيانات الخاصة بك وتضمن تطبيق قواعد الأعمال بشكل متسق عبر جميع التطبيقات التي تصل إلى البيانات.

لماذا تهم القيود

تخيل نظام تجارة إلكترونية حيث تصل تطبيقات متعددة (ويب، موبايل، API) إلى قاعدة البيانات. بدون قيود، يمكن لتطبيق واحد به خلل أن يدرج طلباً بسعر سالب أو إنشاء مستخدم بدون بريد إلكتروني. تمنع القيود مثل هذه التناقضات من خلال فرض القواعد على مستوى قاعدة البيانات.

التحقق على مستوى قاعدة البيانات مقابل مستوى التطبيق: التحقق من التطبيق مهم لتجربة المستخدم، ولكن قيود قاعدة البيانات هي خط دفاعك الأخير. تضمن سلامة البيانات حتى عندما تحتوي التطبيقات على أخطاء أو عند حدوث وصول مباشر إلى قاعدة البيانات.

أنواع القيود

يدعم MySQL عدة أنواع من القيود:

NOT NULL: يضمن أن العمود لا يمكن أن يحتوي على قيم NULL UNIQUE: يضمن أن جميع القيم في العمود فريدة PRIMARY KEY: يحدد كل صف بشكل فريد (NOT NULL + UNIQUE) FOREIGN KEY: يضمن السلامة المرجعية بين الجداول CHECK: يضمن أن القيم تلبي شروط محددة (MySQL 8.0.16+) DEFAULT: يوفر قيمة افتراضية عند عدم تحديدها

قيد NOT NULL

يمنع قيد NOT NULL القيم الفارغة في العمود:

-- إنشاء جدول مع NOT NULL CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) NOT NULL, username VARCHAR(50) NOT NULL, bio TEXT, -- يمكن أن يكون NULL created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- هذا يعمل INSERT INTO users (email, username) VALUES ('john@example.com', 'john123'); -- هذا يفشل: العمود 'email' لا يمكن أن يكون null INSERT INTO users (username) VALUES ('jane123'); -- إضافة NOT NULL إلى عمود موجود ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NOT NULL;
تحذير: تفشل إضافة NOT NULL إلى عمود موجود إذا كانت أي صفوف تحتوي بالفعل على قيم NULL. قم بتحديث تلك الصفوف أولاً أو قدم قيمة DEFAULT.

قيد UNIQUE

يضمن قيد UNIQUE أن جميع القيم في العمود مميزة:

-- قيد فريد على عمود واحد CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) UNIQUE NOT NULL, username VARCHAR(50) UNIQUE NOT NULL, phone VARCHAR(20) UNIQUE ); -- قيد فريد مسمى CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(50), name VARCHAR(255), CONSTRAINT uq_product_sku UNIQUE (sku) ); -- قيد فريد مركب (المجموعة يجب أن تكون فريدة) CREATE TABLE course_enrollments ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_student_course UNIQUE (student_id, course_id) );

UNIQUE يسمح بقيم NULL متعددة (NULL لا يعتبر مساوياً لـ NULL):

-- كلاهما يعمل (يُسمح بـ NULL عدة مرات في عمود UNIQUE) INSERT INTO users (email, username, phone) VALUES ('a@ex.com', 'user1', NULL); INSERT INTO users (email, username, phone) VALUES ('b@ex.com', 'user2', NULL); -- هذا يفشل (رقم هاتف مكرر) INSERT INTO users (email, username, phone) VALUES ('c@ex.com', 'user3', '555-1234'); INSERT INTO users (email, username, phone) VALUES ('d@ex.com', 'user4', '555-1234');

قيد PRIMARY KEY

يحدد PRIMARY KEY كل صف بشكل فريد ويجمع بين NOT NULL و UNIQUE:

-- مفتاح أساسي على عمود واحد CREATE TABLE customers ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL ); -- مفتاح أساسي مسمى CREATE TABLE orders ( order_id INT AUTO_INCREMENT, order_date DATE NOT NULL, customer_id INT NOT NULL, CONSTRAINT pk_orders PRIMARY KEY (order_id) ); -- مفتاح أساسي مركب (أعمدة متعددة) CREATE TABLE order_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10, 2) NOT NULL, PRIMARY KEY (order_id, product_id) );
أفضل ممارسة: استخدم INT أو BIGINT واحد متزايد تلقائياً كمفتاح أساسي لمعظم الجداول. احتفظ بالمفاتيح الأساسية المركبة لجداول الربط (علاقات متعدد إلى متعدد).

قيد CHECK (MySQL 8.0.16+)

تضمن قيود CHECK أن القيم تلبي شروط محددة:

-- قيد CHECK أساسي CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL, discount_percent DECIMAL(5, 2), CONSTRAINT chk_price_positive CHECK (price > 0), CONSTRAINT chk_stock_non_negative CHECK (stock >= 0), CONSTRAINT chk_discount_range CHECK (discount_percent BETWEEN 0 AND 100) ); -- هذا يعمل INSERT INTO products (name, price, stock, discount_percent) VALUES ('Laptop', 999.99, 50, 10.00); -- هذا يفشل: انتهاك قيد التحقق 'chk_price_positive' INSERT INTO products (name, price, stock, discount_percent) VALUES ('Free Item', 0, 10, 0); -- هذا يفشل: انتهاك قيد التحقق 'chk_discount_range' INSERT INTO products (name, price, stock, discount_percent) VALUES ('Invalid', 100, 10, 150);

قيود CHECK المتقدمة

يمكن لقيود CHECK الإشارة إلى أعمدة متعددة:

CREATE TABLE promotions ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, min_purchase DECIMAL(10, 2), max_discount DECIMAL(10, 2), CONSTRAINT chk_date_order CHECK (end_date >= start_date), CONSTRAINT chk_discount_valid CHECK (max_discount <= min_purchase) ); -- التحقق من راتب الموظف CREATE TABLE employees ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL, position ENUM('intern', 'junior', 'senior', 'manager') NOT NULL, salary DECIMAL(10, 2) NOT NULL, CONSTRAINT chk_intern_salary CHECK ( position != 'intern' OR salary BETWEEN 20000 AND 40000 ), CONSTRAINT chk_manager_salary CHECK ( position != 'manager' OR salary >= 80000 ) );
قيد: لا يمكن لقيود CHECK الإشارة إلى جداول أخرى أو استخدام استعلامات فرعية. للتحقق عبر الجداول، استخدم المحفزات أو منطق التطبيق.

قيد DEFAULT

يوفر DEFAULT قيماً تلقائية عند عدم تحديدها:

CREATE TABLE posts ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL, content TEXT NOT NULL, status ENUM('draft', 'published', 'archived') DEFAULT 'draft', views INT DEFAULT 0, is_featured BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- فقط العنوان والمحتوى مُوفران، الآخرون يستخدمون الافتراضيات INSERT INTO posts (title, content) VALUES ('My Post', 'Content here'); -- النتيجة: status='draft', views=0, is_featured=FALSE, الطوابع الزمنية تُعيّن تلقائياً

إدارة القيود

يمكنك إضافة وتعديل وإسقاط القيود بعد إنشاء الجدول:

-- إضافة قيد UNIQUE ALTER TABLE users ADD CONSTRAINT uq_email UNIQUE (email); -- إضافة قيد CHECK ALTER TABLE products ADD CONSTRAINT chk_price CHECK (price > 0); -- إسقاط قيد ALTER TABLE users DROP CONSTRAINT uq_email; ALTER TABLE products DROP CHECK chk_price; -- إضافة NOT NULL ALTER TABLE users MODIFY COLUMN email VARCHAR(255) NOT NULL; -- إزالة NOT NULL (جعله قابل للإفراغ) ALTER TABLE users MODIFY COLUMN phone VARCHAR(20) NULL;

عرض القيود

استعلم عن مخطط المعلومات لرؤية تفاصيل القيود:

-- عرض جميع القيود لجدول SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE, TABLE_NAME FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = 'your_database' AND TABLE_NAME = 'products'; -- عرض تعريفات قيود CHECK SELECT CONSTRAINT_NAME, CHECK_CLAUSE FROM information_schema.CHECK_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = 'your_database'; -- عرض قيود العمود SHOW CREATE TABLE products;

مثال من الواقع: نظام طلبات التجارة الإلكترونية

إليك مثال كامل مع أنواع قيود متعددة:

CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, order_number VARCHAR(50) UNIQUE NOT NULL, status ENUM('pending', 'processing', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending' NOT NULL, subtotal DECIMAL(10, 2) NOT NULL, tax DECIMAL(10, 2) NOT NULL, shipping DECIMAL(10, 2) NOT NULL, total DECIMAL(10, 2) NOT NULL, order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, shipped_date TIMESTAMP NULL, -- قيود CHECK CONSTRAINT chk_subtotal_positive CHECK (subtotal > 0), CONSTRAINT chk_tax_non_negative CHECK (tax >= 0), CONSTRAINT chk_shipping_non_negative CHECK (shipping >= 0), CONSTRAINT chk_total_correct CHECK (total = subtotal + tax + shipping), CONSTRAINT chk_shipped_date_after_order CHECK ( shipped_date IS NULL OR shipped_date >= order_date ) ); -- هذا يعمل INSERT INTO orders (customer_id, order_number, subtotal, tax, shipping, total) VALUES (123, 'ORD-2024-0001', 100.00, 8.00, 5.00, 113.00); -- هذا يفشل: انتهاك chk_total_correct INSERT INTO orders (customer_id, order_number, subtotal, tax, shipping, total) VALUES (123, 'ORD-2024-0002', 100.00, 8.00, 5.00, 100.00);
أفضل ممارسة: استخدم أسماء قيود ذات معنى (مثل chk_price_positive) بدلاً من السماح لـ MySQL بتوليد الأسماء. هذا يجعل رسائل الخطأ أوضح وإدارة القيود أسهل.

معالجة انتهاكات القيود

عند انتهاك القيود، يعيد MySQL رموز خطأ محددة:

-- خطأ 1048: لا يمكن أن يكون العمود null (انتهاك NOT NULL) -- خطأ 1062: إدخال مكرر (انتهاك UNIQUE/PRIMARY KEY) -- خطأ 3819: انتهاك قيد التحقق (انتهاك قيد CHECK) -- خطأ 1452: فشل قيد المفتاح الخارجي (انتهاك FOREIGN KEY) -- مثال PHP: معالجة انتهاكات القيود try { $pdo->exec( "INSERT INTO users (email, username) VALUES ('test@ex.com', 'test')" ); } catch (PDOException $e) { if ($e->getCode() == 23000) { // انتهاك قيد السلامة if (strpos($e->getMessage(), 'Duplicate entry') !== false) { echo "هذا البريد الإلكتروني أو اسم المستخدم مستخدم بالفعل"; } } else { throw $e; } }

تمرين عملي:

المهمة: أنشئ جدول نظام حجز مع القيود المناسبة.

المتطلبات:

  1. الحجز يجب أن يحتوي على customer_id و room_id و check_in و check_out
  2. رقم الحجز يجب أن يكون فريداً
  3. المغادرة يجب أن تكون بعد الوصول
  4. الحالة يجب أن تكون pending/confirmed/cancelled (افتراضي: pending)
  5. التكلفة الإجمالية يجب أن تكون موجبة
  6. لا يمكن للعميل أن يكون لديه حجوزات متداخلة لنفس الغرفة

الحل:

CREATE TABLE bookings ( id INT PRIMARY KEY AUTO_INCREMENT, booking_number VARCHAR(50) UNIQUE NOT NULL, customer_id INT NOT NULL, room_id INT NOT NULL, check_in DATE NOT NULL, check_out DATE NOT NULL, status ENUM('pending', 'confirmed', 'cancelled') DEFAULT 'pending' NOT NULL, total_cost DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- القيود CONSTRAINT chk_checkout_after_checkin CHECK (check_out > check_in), CONSTRAINT chk_cost_positive CHECK (total_cost > 0), CONSTRAINT uq_customer_room_dates UNIQUE (customer_id, room_id, check_in) ); -- اختبار حجز صالح INSERT INTO bookings (booking_number, customer_id, room_id, check_in, check_out, total_cost) VALUES ('BK-2024-001', 123, 201, '2024-03-15', '2024-03-18', 450.00); -- هذا يفشل: check_out يجب أن يكون بعد check_in INSERT INTO bookings (booking_number, customer_id, room_id, check_in, check_out, total_cost) VALUES ('BK-2024-002', 124, 202, '2024-03-15', '2024-03-15', 150.00);

القيود مقابل المحفزات

اختر الأداة المناسبة للتحقق من البيانات:

استخدم القيود عندما: ✓ التحقق البسيط على مستوى العمود ✓ متطلبات التفرد ✓ السلامة المرجعية (المفاتيح الخارجية) ✓ فحوصات النطاق الأساسية ✓ الأداء أمر بالغ الأهمية استخدم المحفزات عندما: ✓ منطق الأعمال المعقد ✓ التحقق عبر الجداول ✓ سجل التدقيق ✓ القيم المحسوبة/المشتقة ✓ رسائل خطأ مخصصة مطلوبة

الملخص

في هذا الدرس، تعلمت:

  • تفرض القيود سلامة البيانات على مستوى قاعدة البيانات
  • NOT NULL يمنع القيم الفارغة في الأعمدة
  • UNIQUE يضمن أن جميع القيم مميزة
  • PRIMARY KEY يحدد الصفوف بشكل فريد (NOT NULL + UNIQUE)
  • قيود CHECK تتحقق من البيانات مقابل الشروط (MySQL 8.0.16+)
  • DEFAULT يوفر قيماً تلقائية
  • استخدم أسماء قيود ذات معنى لرسائل خطأ أفضل
  • القيود هي خط دفاعك الأخير ضد البيانات غير الصالحة
التالي: في الدرس التالي، سنستكشف السلامة المرجعية وقيود المفتاح الخارجي بالتفصيل، بما في ذلك عمليات التسلسل!