Skip to content

النماذج

النماذج (Models) هي بنى Rust تُعيّن مباشرة لجداول قاعدة البيانات. تُعرّف شكل بياناتك، وتتعامل مع التسلسل، وتوفر الأساس لاستعلامات قاعدة البيانات الآمنة من حيث النوع باستخدام SQLx. يُولّد FORGE نماذج لكل وحدة في مشروعك، متبعاً أنماطاً متسقة للحقول والـ traits والترجمات.

اصطلاحات النماذج

كل struct نموذج يشتق هذه الـ traits الأساسية:

rust
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    // ...
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
}
Traitالغرض
Debugتمكين الطباعة للتصحيح
Cloneالسماح بنسخ القيمة
Serializeالتحويل لـ JSON للاستجابات API
Deserializeالتحليل من JSON (يُستخدم في الاختبارات والداخلي)
FromRowتعيين صفوف قاعدة البيانات لحقول الـ struct عبر SQLx

نصيحة

FromRow يسمح لـ SQLx بتعيين نتائج الاستعلام لـ struct الخاص بك تلقائياً. أسماء الأعمدة يجب أن تطابق أسماء الحقول تماماً، أو يمكنك استخدام #[sqlx(rename = "column_name")] لعدم التطابق.

النماذج الأساسية

User

rust
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct User {
    pub id: Uuid,
    pub name: String,
    pub email: String,
    #[serde(skip_serializing)]
    pub password: String,
    pub mobile: Option<String>,
    pub avatar: Option<String>,
    pub email_verified_at: Option<DateTime<Utc>>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
}

تحذير

حقل password يستخدم #[serde(skip_serializing)] لضمان عدم تضمينه أبداً في استجابات API. طبّق دائماً هذا الـ attribute على الحقول الحساسة.

Role

rust
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Role {
    pub id: Uuid,
    pub name: String,
    pub display_name: Option<String>,
    pub description: Option<String>,
    pub is_system: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

Permission

rust
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Permission {
    pub id: Uuid,
    pub name: String,
    pub display_name: Option<String>,
    pub description: Option<String>,
    pub group_name: Option<String>,
    pub is_system: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

Content (قابل للترجمة)

rust
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Content {
    pub id: Uuid,
    pub slug: String,
    pub content_type: String,
    pub translations: serde_json::Value,
    pub metadata: Option<serde_json::Value>,
    pub is_active: bool,
    pub sort_order: i32,
    pub created_by: Option<Uuid>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
}

النماذج القابلة للترجمة

النماذج مع دعم متعدد اللغات تُخزّن الترجمات في عمود JSONB. حقل translations يحمل كائن JSON متداخل مفهرس باللغة:

rust
// The translations field stores this structure:
// {
//   "en": { "title": "About Us", "body": "We are..." },
//   "ar": { "title": "عنا", "body": "نحن..." }
// }

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Content {
    pub id: Uuid,
    pub slug: String,
    pub translations: serde_json::Value,
    // ...
}

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

استخراج ترجمة لغة محددة في SQL:

rust
// Fetch content with specific language translation extraction
let content = sqlx::query_as!(
    ContentLocalized,
    r#"
    SELECT
        id,
        slug,
        translations->$1->>'title' AS "title?",
        translations->$1->>'body' AS "body?",
        translations->$1->>'description' AS "description?",
        is_active,
        created_at
    FROM contents
    WHERE slug = $2 AND deleted_at IS NULL
    "#,
    locale,  // e.g., "en" or "ar"
    slug
)
.fetch_optional(&pool)
.await?;

تحديث الترجمات

تحديث لغة واحدة بدون الكتابة فوق الأخرى:

rust
// Update only the Arabic translation
sqlx::query!(
    r#"
    UPDATE contents
    SET translations = jsonb_set(
        translations,
        $1,
        $2::jsonb
    ),
    updated_at = now()
    WHERE id = $3
    "#,
    &format!("{{{}}}", locale) as &str,  // e.g., "{ar}"
    serde_json::to_value(&translation)?,
    content_id
)
.execute(&pool)
.await?;

نصيحة

استخدم دالة jsonb_set في PostgreSQL لتحديث ترجمات اللغات الفردية بدون قراءة وإعادة كتابة عمود JSONB بالكامل. هذا يتجنب حالات السباق عندما يُعدّل مستخدمون متعددون لغات مختلفة في نفس الوقت.

مساعدات الاستعلام

النماذج غالباً تتضمن دوال مرتبطة للاستعلامات الشائعة:

rust
impl User {
    /// Find user by ID (excluding soft-deleted)
    pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result<Option<User>, sqlx::Error> {
        sqlx::query_as!(
            User,
            r#"
            SELECT id, name, email, password, mobile, avatar,
                   email_verified_at, is_active,
                   created_at, updated_at, deleted_at
            FROM users
            WHERE id = $1 AND deleted_at IS NULL
            "#,
            id
        )
        .fetch_optional(pool)
        .await
    }

    /// Find user by email
    pub async fn find_by_email(pool: &PgPool, email: &str) -> Result<Option<User>, sqlx::Error> {
        sqlx::query_as!(
            User,
            r#"
            SELECT id, name, email, password, mobile, avatar,
                   email_verified_at, is_active,
                   created_at, updated_at, deleted_at
            FROM users
            WHERE email = $1 AND deleted_at IS NULL
            "#,
            email
        )
        .fetch_optional(pool)
        .await
    }

    /// List users with pagination
    pub async fn paginate(
        pool: &PgPool,
        page: i64,
        per_page: i64,
        search: Option<&str>,
    ) -> Result<(Vec<User>, i64), sqlx::Error> {
        let offset = (page - 1) * per_page;

        let total = sqlx::query_scalar!(
            r#"
            SELECT COUNT(*) as "count!"
            FROM users
            WHERE deleted_at IS NULL
            AND ($1::text IS NULL OR name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%')
            "#,
            search
        )
        .fetch_one(pool)
        .await?;

        let users = sqlx::query_as!(
            User,
            r#"
            SELECT id, name, email, password, mobile, avatar,
                   email_verified_at, is_active,
                   created_at, updated_at, deleted_at
            FROM users
            WHERE deleted_at IS NULL
            AND ($1::text IS NULL OR name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%')
            ORDER BY created_at DESC
            LIMIT $2 OFFSET $3
            "#,
            search,
            per_page,
            offset
        )
        .fetch_all(pool)
        .await?;

        Ok((users, total))
    }
}

Model vs DTO

النماذج تمثل صف قاعدة البيانات تماماً. الـ DTOs (كائنات نقل البيانات) تمثل ما يُرسله ويستقبله الـ API. أبقهم منفصلين:

صف قاعدة البيانات  ──▶  Model (User)       ──▶  طبقة Service

طلب API   ──▶  DTO (CreateUserRequest)        │

استجابة API  ◀──  DTO (UserResponse) ◀──  طبقة Service

قد يحتوي النموذج على حقول حساسة (مثل password) التي يجب ألا تظهر أبداً في استجابات API. الـ DTOs تتعامل مع هذا التحويل. انظر الـ DTOs للتفاصيل.

العلاقات

العلاقات تُحمّل عبر joins صريحة أو استعلامات منفصلة، وليس من خلال سحر ORM:

rust
/// User with loaded roles
pub struct UserWithRoles {
    pub user: User,
    pub roles: Vec<Role>,
}

impl UserWithRoles {
    pub async fn find(pool: &PgPool, user_id: Uuid) -> Result<Self, sqlx::Error> {
        let user = User::find_by_id(pool, user_id)
            .await?
            .ok_or(sqlx::Error::RowNotFound)?;

        let roles = sqlx::query_as!(
            Role,
            r#"
            SELECT r.id, r.name, r.display_name, r.description,
                   r.is_system, r.created_at, r.updated_at
            FROM roles r
            INNER JOIN role_user ru ON ru.role_id = r.id
            WHERE ru.user_id = $1
            "#,
            user_id
        )
        .fetch_all(pool)
        .await?;

        Ok(Self { user, roles })
    }
}

نصيحة

SQLx لا يتضمن ORM تقليدي مع lazy-loading للعلاقات. بدلاً من ذلك، حمّل البيانات المرتبطة بشكل صريح. هذا يجعل سلوك الاستعلام قابلاً للتنبؤ ويتجنب مشكلة N+1 التي تُعاني منها ORMs.

انظر أيضاً

Released under the MIT License.