عند تصميمنا لتطبيق جديد تعودنا دائماً بالبدء بتصميم قواعد البيانات والتي نحاول فيها مراعاة Database Normalization والتي تنص إحدى نقاطها على أنه يجب أن يكون بالجدول البيانات الخاصة به فقط ويتم إنشاء جداول أخرى وربطهم بعلاقات لنتمكن من إنشاء ترابط بين هذه الجداول والحصول على هذه البيانات بطريقة متكاملة.

ولكن عند التصميم وبرمجة API Requests يكون اﻷمر مختلف، فهنا نحن نهتم بأن يكون Response يحتوي على بيانات متكاملة وذات معنى للمطور الذي يستخدمها، بغض النظر على الكيفية التي تم تصميم قاعدة البيانات بها. وأيضا علينا مراعاة أداء وسرعة API Request والذي يمكننا أولا تحسينه من خلال تحسين جودة الكود وتطبيق بعض القواعد، وثانياً يمكننا تحسينها من خلال دراسة API Responses التي نريدها وكيفية تقسيمها لتكون سهلة اﻹستخدام ولترجع فقط البيانات المطلوبة منها.

قد يكون هذا الكلام غير واضح إلى الآن، فكيف يمكننا تحسين اﻷداء ودارسة API لتكون سهلة اﻹستخدام؟

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

الحل اﻷول:

قد يخطر علينا أولاً أنه يمكننا تصميم API Request لعرض المقال ولنقل أنه كان {posts/{postId

ليقوم بعرض التعليقات التي كتبت عليه دائما، ليكون Response كالتالي:

"data": {
    "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
    "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
    "published_at": "2007-06-05 08:07:11",
    "comments": [{
            "body": "Porro molestias maxime assumenda dolores.",
            "user": {
                "name": "Delores Schmeler IV",
                "email": "reilly.genevieve@example.net"
            }
        },
        {
            "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
            "user": {
                "name": "Murphy Legros",
                "email": "emilie.weber@example.org"
            }
        }
    ]
}

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

الحل الثاني:

كحل أفضل يمكننا أن نفصل المقالة عن تعليقاتها وذلك عن طريق إنشاء إثنين من APIs اﻷول لعرض المقالة بتفاصيلها اﻷساسية فقط وليكون الرابط {posts/{postId

"data": {
    "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
    "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
    "published_at": "2007-06-05 08:07:11"
}

والآخر لعرض تعليقات هذه المقالة ليكون posts/{postId}/comments

"data": [
    {
        "body": "Porro molestias maxime assumenda dolores.",
        "user": {
            "name": "Delores Schmeler IV",
            "email": "reilly.genevieve@example.net"
        }
    },
    {
        "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
        "user": {
            "name": "Murphy Legros",
            "email": "emilie.weber@example.org"
        }
    },
    {
        "body": "Aut odio qui odit est labore quod.",
        "user": {
            "name": "Daija Legros",
            "email": "hildegard61@example.net"
        }
    }
]

بهذه الطريقة يمكننا اﻷن تحميل فقط البيانات التي نريدها فعند عرضنا لصفحة المقال المختصر يمكننا استدعاء API اﻷول، وعند عرضنا لصفحة المقال الملحق معه التعليقات التي كتبت عليه، يمكننا إستخدام API اﻷول والثاني معاً، هذا جيد جداً. ولكن هل قمنا بتسهيل استخدام هذين APIs على المطورين الذين يستخدمونها في FrontEnd؟ لا أعتقد ذلك فهم الآن مضطرين لاستدعاء طلبين لعرض الصفحة المقال مع تعليقاته، وماذا لو كان للمقال علاقات أخرى مثلا Tags عندها سنقوم بإنشاء API أخر منفصل ليكونوا بذلك 3.

هذه الطريقة هي صحيحة من ناحية الآداء، ولكن من ناحية سهولة اﻹستخدام على المطورين الذين يستخدمونها فهي غير مؤكدة، قد تكون صحيحة عند معرفتنا أنه عدد العلاقات للمقال لن تزيد، وأننا نريد فقط التعليقات، فهذا يعتمد على تحليلنا للنظام.

إذا ما الحل اﻷخر؟

الحل الثالث:

لنتمكن من استدعاء العلاقات فقط عندما نريدها وأيضاً تسهيل استخدام API على المطوين يمكننا اﻹستفادة من Eloquent Relationships Eager Loading لدى Laravel وأيضا أعتقد أنها موجودة لدى إطارات العمل اﻷخرى تحت نفس المسمى. ما تقوم به هذه الميزة هي تضمين العلاقات للمصدر الذي نطلبه (في حالتنا هي المقالة) لنتمكن من الحصول على هذه العلاقات مع بيانات المصدر المطلوب.

ويتم اﻷمر في إطار العمل Laravel عن طريق تحديد مصفوفة with داخل Post Model

protected $with = ['comments'];

public function comments()
{
    return $this->hasMany(Comment::class);
}

فبهذه الطريقة عند استدائنا للمقالة سنحصل على التعليقات معها. ولكن ألم نقل أننا نريد الحصول على التعليقات فقط عندما نريدها؟

نعم، ولهذه باﻹستفادة من هذه الميزة، قمت بإنشاء طريقة لإضافة العلاقات المطلوبة قفط.

أولا: كيف يمكننا تحديد ما هي العلاقات التي نريدها أن تتضمن من المقال؟

يمكننا تحديد عن طريق اﻹستفادة من API Query Parameters، عن طريق إرسال بارامتر يحدد ماهي العلاقات.

ثانياً: كيف نحصل على العلاقات بناءاً على هذا البارامتر؟

نقوم بإضافة هذه العلاقات التي طلبناها إلى مصفوفة with، التي ستكون مسؤولة على تضمينها هي فقط.

وبهذه الطريقة إذا قمنا بطلب API بدون إرسال البارامتر سنحصل على المقال بياناته الأساسية فقط، أما إذا أرسلنا أسماء العلاقات التي نريدها بالبارامتر سيتم تضمينه مع المقال.

ونظراً لتكرر استخدام هذه الطريقة في عدة مشاريع قمت بتحويلها إلى باكج Dynamic Relations Includes.

لنقم بتحويل مثالنا هذا إلى هذه الطريقة باستخدامها.

التنصيب:

يمكننا تنصيبها عن طريق composer باستخدام لأمر

composer require kalshah/dynamic-relations-includes

الاستخدام:

سنقوم بإستخدام IncludeRelations trait داخل Post Model

use IncludeRelations; ثم نقوم بتحديد أننا نريد أن تكون علاقة comments من العلاقات المسموح تضمينها باستخدام API Query Parameter.

protected $loadableRelations = ['comments'];

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

أخيرا سيكون شكل Post Model كالتالي:

والآن يمكننا طلب المقال بياناته الأساسية عن طريق {posts/{postId ليكون Response:

"data": {
    "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
    "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
    "published_at": "2007-06-05 08:07:11"
}

وطلبه مع تضمين التعليقات عن طريقposts/{postId}?include[]=comments ليكون Response

"data": {
    "title": "Fugit assumenda placeat corporis facere et ullam consequatur.",
    "body": "Necessitatibus iusto rerum harum et sed earum. Necessitatibus impedit est sunt aut reiciendis fuga. Quia aut necessitatibus autem occaecati magnam.",
    "published_at": "2007-06-05 08:07:11",
    "comments": [
        {
            "body": "Porro molestias maxime assumenda dolores.",
            "user": {
                "name": "Delores Schmeler IV",
                "email": "reilly.genevieve@example.net"
            }
        },
        {
            "body": "Animi asperiores rerum quas culpa cumque ab magnam.",
            "user": {
                "name": "Murphy Legros",
                "email": "emilie.weber@example.org"
            }
        }
    ]
}

نهايةً نحن تمكنا من تضمين العلاقات بناءاً على طلب المطور الذي سيقوم باستخدام هذه APIs ليكون تصميم تطبيقنا متجاوب مع مختلف العلاقات المطلوبة.

يمكنكم اﻹطلاع على توثيق لمعرفة كيفية تطبيق نفس الطريقة عندما نريد تضمين مجموع السجلات للعلاقة، مثل إجمالي عدد التعليقات لكل مقالة.

رابط المقالة على مدونتي