Skip to content

قاعدة البيانات

يستخدم FORGE PostgreSQL 15+ كمحرك قاعدة البيانات الوحيد، مستفيداً من ميزاته المتقدمة مثل UUIDs وأعمدة JSONB والبحث النصي الكامل. المشروع المُولّد يتضمن نظام ترحيل وبذر كامل يُبقي مخططك مُصنّفاً وبيانات التطوير قابلة لإعادة الإنتاج.

إعدادات الاتصال

اتصال قاعدة البيانات يُعدّ عبر متغيرات البيئة:

bash
# .env
DATABASE_URL=postgres://forge_user:forge_pass@localhost:5432/forge_db
DATABASE_MAX_CONNECTIONS=10
DATABASE_MIN_CONNECTIONS=2

يُنشأ connection pool عند البدء باستخدام SQLx:

rust
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;

pub async fn create_pool(database_url: &str) -> PgPool {
    PgPoolOptions::new()
        .max_connections(10)
        .min_connections(2)
        .acquire_timeout(std::time::Duration::from_secs(5))
        .idle_timeout(std::time::Duration::from_secs(600))
        .connect(database_url)
        .await
        .expect("Failed to create database connection pool")
}

نصيحة

SQLx يتحقق من استعلامات SQL في وقت الترجمة مقابل مخطط قاعدة البيانات الفعلي. شغّل cargo sqlx prepare لتوليد بيانات استعلام offline لبناءات CI حيث قاعدة البيانات غير متاحة.

اصطلاحات المخطط

كل جدول في مشروع FORGE المُولّد يتبع هذه الاصطلاحات:

المفاتيح الرئيسية

جميع الجداول تستخدم مفاتيح رئيسية UUID تُولّدها PostgreSQL:

sql
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    -- ...
);

الطوابع الزمنية

كل جدول يتضمن أعمدة created_at و updated_at:

sql
CREATE TABLE users (
    -- ...
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

الحذف الناعم

الجداول التي تدعم الحذف الناعم تتضمن عمود deleted_at:

sql
CREATE TABLE users (
    -- ...
    deleted_at TIMESTAMPTZ
);

تحذير

عند استعلام الجداول القابلة للحذف الناعم، ضمّن دائماً WHERE deleted_at IS NULL لاستبعاد السجلات المحذوفة. الـ services المُولّدة تتعامل مع هذا تلقائياً، لكن كن على علم به عند كتابة استعلامات مخصصة.

أعمدة JSONB

المحتوى القابل للترجمة والبيانات الوصفية المرنة تستخدم JSONB:

sql
CREATE TABLE contents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    slug VARCHAR(255) NOT NULL UNIQUE,
    translations JSONB NOT NULL DEFAULT '{}',
    metadata JSONB DEFAULT '{}',
    -- ...
);

ترجمات JSONB تتبع هذه البنية:

json
{
  "en": {
    "title": "Welcome",
    "body": "Welcome to our site"
  },
  "ar": {
    "title": "مرحبا",
    "body": "مرحبا بكم في موقعنا"
  }
}

الترحيلات

الترحيلات توجد في database/migrations/ ومُرقّمة بالتسلسل:

database/migrations/
├── 00001_create_users_table.sql
├── 00002_create_roles_table.sql
├── 00003_create_permissions_table.sql
├── 00004_create_role_user_table.sql
├── 00005_create_permission_role_table.sql
├── 00006_create_settings_table.sql
├── 00007_create_password_resets_table.sql
├── 00008_create_otp_codes_table.sql
├── 00009_create_media_table.sql
├── 00010_create_audit_logs_table.sql
├── 00011_create_translations_table.sql
├── 00012_create_pages_table.sql
├── 00013_create_faqs_table.sql
├── 00014_create_contents_table.sql
├── 00015_create_menus_table.sql
├── 00016_migrate_media_polymorphic.sql
└── 00017_create_lookups_table.sql

تشغيل الترحيلات

الترحيلات تُنفّذ تلقائياً عند بدء التطبيق:

rust
sqlx::migrate!("./database/migrations")
    .run(&pool)
    .await
    .expect("Failed to run migrations");

كتابة ترحيل جديد

أنشئ ملف SQL جديد بالرقم التسلسلي التالي:

sql
-- database/migrations/00018_create_products_table.sql
CREATE TABLE products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(255) NOT NULL UNIQUE,
    translations JSONB NOT NULL DEFAULT '{}',
    price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    deleted_at TIMESTAMPTZ
);

-- Index on slug for fast lookups
CREATE INDEX idx_products_slug ON products(slug);

-- Index for active products
CREATE INDEX idx_products_active ON products(is_active) WHERE deleted_at IS NULL;

نصيحة

استخدم الفهارس الجزئية (WHERE deleted_at IS NULL) لإبقاء الفهارس صغيرة وفعالة. PostgreSQL ستستخدم هذه الفهارس للاستعلامات التي تتضمن نفس جملة WHERE.

الـ Seeders

الـ seeders تملأ قاعدة البيانات بالبيانات الأولية المطلوبة لعمل التطبيق:

database/seeders/
├── 01_roles.sql           # Default roles (admin, user)
├── 02_permissions.sql     # All permissions
├── 03_admin_user.sql      # Default admin account
├── 04_settings.sql        # Application settings
├── 05_pages.sql           # Default pages
└── 06_translations.sql    # UI translations

مثال seeder لمستخدم admin:

sql
-- database/seeders/03_admin_user.sql
INSERT INTO users (name, email, password, email_verified_at, is_active)
VALUES (
    'Admin',
    'admin@forge.dev',
    -- Argon2 hash for 'password'
    '$argon2id$v=19$m=19456,t=2,p=1$...',
    now(),
    true
)
ON CONFLICT (email) DO NOTHING;

-- Assign admin role
INSERT INTO role_user (user_id, role_id)
SELECT u.id, r.id
FROM users u
CROSS JOIN roles r
WHERE u.email = 'admin@forge.dev'
AND r.name = 'admin'
ON CONFLICT DO NOTHING;

استراتيجية الفهرسة

يُولّد FORGE فهارس لأنماط الاستعلام الشائعة:

sql
-- Foreign key indexes (always indexed)
CREATE INDEX idx_role_user_user_id ON role_user(user_id);
CREATE INDEX idx_role_user_role_id ON role_user(role_id);

-- Uniqueness constraints that act as indexes
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_mobile ON users(mobile) WHERE mobile IS NOT NULL;

-- Slug lookups
CREATE UNIQUE INDEX idx_contents_slug ON contents(slug);

-- Soft delete filtering
CREATE INDEX idx_users_active ON users(is_active) WHERE deleted_at IS NULL;

-- JSONB indexes for translations
CREATE INDEX idx_contents_translations ON contents USING GIN(translations);

-- Timestamp ordering
CREATE INDEX idx_contents_created_at ON contents(created_at DESC);

الاستعلام مع الترجمات

الوصول للحقول المُترجمة باستخدام مُشغّلات JSONB في PostgreSQL:

sql
-- Get Arabic title
SELECT translations->'ar'->>'title' AS title_ar
FROM contents
WHERE slug = 'about-us';

-- Search across translations
SELECT *
FROM contents
WHERE translations->'en'->>'title' ILIKE '%welcome%';

-- Get all translations for a specific language
SELECT translations->'en' AS en_translation
FROM contents
WHERE deleted_at IS NULL;

في Rust مع SQLx:

rust
// Query with specific language extraction
let contents = sqlx::query_as!(
    ContentWithTranslation,
    r#"
    SELECT
        id,
        slug,
        translations->$1->>'title' AS "title?",
        translations->$1->>'body' AS "body?",
        created_at
    FROM contents
    WHERE deleted_at IS NULL
    ORDER BY created_at DESC
    "#,
    locale
)
.fetch_all(&pool)
.await?;

Connection Pooling

التطبيق المُولّد يستخدم connection pool المدمج في SQLx. الإعدادات الرئيسية:

الإعدادالافتراضيالوصف
max_connections10أقصى عدد من الاتصالات في الـ pool
min_connections2أدنى عدد من الاتصالات الخاملة المحفوظة
acquire_timeout5sأقصى وقت انتظار للحصول على اتصال
idle_timeout600sإغلاق الاتصالات الخاملة بعد هذه المدة

تحذير

عيّن max_connections بناءً على إعداد max_connections في PostgreSQL مقسوماً على عدد نسخ التطبيق. مثلاً، إذا كان PostgreSQL يسمح بـ 100 اتصال وتشغّل 5 نسخ، عيّن max_connections إلى 15-18، مع ترك هامش لاتصالات الإدارة.

انظر أيضاً

Released under the MIT License.