راهنمای جامع روابط مدل‌ها (relationship) در لاراول

Laravel Eloquent Model Relationship

راهنمای جامع روابط مدل ها (relationship) در لاراول

لاراول یکی از محبوب ترین فریم ورک های موجود در ایران است و افراد بسیار زیادی روزانه از آن استفاده می کنند اما از آنجایی که این فریم ورک یک فریم ورک full stack می باشد یادگیری آن به زمان نیاز دارد و باید قبل از استفاده از آن با برخی از مباحث پایه آشنا باشید. یکی از مباحثی که در لاراول تازه کاران را گمراه می کند مسئله relationship ها یا روابط بین مدل ها در لاراول است.

همانطور که می دانید لاراول از یک ORM به نام Eloquent استفاده می کند بنابراین روابط بین مدل ها در لاراول با استفاده از این ORM تعریف می شود. ما در این مقاله می خواهیم در این باره توضیحاتی را ارائه کنیم اما قبل از آن باید با برخی از مفاهیم آشنا شویم. ما در این مقاله فرض می کنیم فقط با پایگاه های داده رابطه ای مانند MySQL کار می کنیم تا درک آن ساده تر شود.

سطح مقاله: برای مطالعه این مقاله باید با یک پایگاه داده رابطه ای مانند MySQL یا PostgreSQL یا امثال آن ها و همچنین لاراول آشنا باشید. هدف این مقاله آشنایی با ماهیت روابط بین مدل ها در لاراول است و قصد آموزش لاراول را نداریم.

Data Model یا مدل داده

Data Model یا مدل داده به زبان ساده به ساختاری گفته می شود که شما بر اساس آن داده های خود را در پایگاه داده ذخیره می کنید. به طور مثال ممکن است شما تمام داده های سایت خودتان را درون یک جدول بزرگ MySQL ذخیره کنید (این کار اصلا پیشنهاد نمی شود). این جدول همان مدل داده شما است. از طرفی ممکن است داده هایتان را در ۱۰ جدول مختلف تقسیم کنید. این هم یک مدل داده است.

ما در هنگام توسعه یک برنامه full stack سعی می کنیم ساختار مدل ها و داده ها در ORM را تا حد ممکن نزدیک به ساختار آن ها در پایگاه داده نگه داریم. با این کار پیچیدگی های کار کمتر می شود و اصلا یکی از اهداف ORM ها نیز همین است. زمانی که با مدل داده کار می کنید ممکن است واژه Cardinality را بشنوید که در واقع همان count یا تعداد است بنابراین تصور نکنید که واژه ای بسیار تخصصی و پیچیده است.

Relationships یا رابطه

هر زمانی که داده هایی را در یک سیستم ذخیره می کنیم، این داده ها از راه های مختلفی به هم مرتبط می شوند. به نحوه ارتباط این داده ها با یکدیگر relationship یا رابطه می گوییم. من ابتدا یک مثال از روابط بین داده ای را برای شما می آورم که هیچ ارتباطی با پایگاه داده ها ندارد. فرض کنید چند آیتم را درون یک آرایه ذخیره کرده ایم. یک رابطه ساده این است که ایندکس هر آیتم از ایندکس آیتم قبلی بیشتر است!

با این حساب متوجه می شوید که روابط بین داده ای تعریف ثابتی ندارند بنابراین اگر دو نفر به یک مجموعه داده نگاه کنند ممکن است دو رابطه مختلف را از آن استخراج کنند و حرف هر دو نفر نیز صحیح است. اگر در زمینه های تحلیل داده و آمار کار کرده باشید این موضوع را به خوبی می دانید.

انواع رابطه در RDBMS

عبارت RDBMS مخفف relational database management system (سیستم مدیریت پایگاه داده رابطه ای) است. پایگاه های داده ای مانند MySQL و PostgreSQL و SQL Server و SQLite و MariaDB و ... مثال هایی از پایگاه های داده رابطه ای می باشند که یعنی هدف اصلی از طراحی آن ها کارکرد بهینه با استفاده از روابط بین داده ای است.

تمام RDBMS به ما اجازه می دهند بین داده های خودمان روابطی را تعریف کنیم. این پایگاه های داده ویژگی های خاصی را در اختیار ما قرار می دهند تا بتوانیم با استفاده از آن ها چنین روابطی را تعریف کنیم (Primary Key و Foreign Key و Constraint ها). من نمی خواهم این مقاله را به یک  ما می خواهیم در این بخش انواع این روابط را بررسی کنیم.

روابط یک به یک (one-to-one)

تقریبا در تمام برنامه های وب مفهومی به نام «حساب کاربری» یا همان user account وجود دارد و این حساب های کاربری معمولا چنین رابطه ای دارند:

  • هر کاربر می تواند یک حساب داشته باشد.
  • هر حساب متعلق به یک کاربر است.

این یک رابطه یک به یک است. چرا؟ به دلیل اینکه هر دو طرف رابطه فقط با یک خط به هم وصل می شوند. شاید بگویید یک کاربر می تواند با یک ایمیل دیگر یک حساب دیگر بسازد و در این صورت دو حساب خواهد داشت. حرفتان درست است اما از نظر وب سایت ما این دو حساب دو کاربر جدید هستند. توجه داشته باشید که ما در حال طراحی یک سیستم برای کامپیوتر ها هستیم بنابراین باید از زاویه دید آن ها به این مسئله نگاه کنیم.

توجه داشته باشید که این رابطه به شکل مصنوعی ایجاد نشده است بلکه به شکل طبیعی به صورت یک به یک وجود دارد. برای داشتن یک رابطه یک به یک «طبیعی» بودن آن بسیار مهم است. حالا که متوجه وجود چنین رابطه ای (کاربران و حسابشان) در برنامه خودمان شده ایم باید یک رابطه یک به یک را در پایگاه داده خود پیاده سازی کنیم. یک مثال ساده از انجام این کار به شکل زیر است:

CREATE TABLE users(

    id INT NOT NULL AUTO_INCREMENT,

    email VARCHAR(100) NOT NULL,

    password VARCHAR(100) NOT NULL,

    PRIMARY KEY(id)

);




CREATE TABLE accounts(

    id INT NOT NULL AUTO_INCREMENT,

    role VARCHAR(50) NOT NULL,

    PRIMARY KEY(id),

    FOREIGN KEY(id) REFERENCES users(id)

);

ما فعلا با کد SQL خالص این مثال را نوشته ایم. در این مثال دو جدول به نام های users و accounts را داریم. در جدول accounts ستونی به نام id وجود دارد که هم Primary Key و هم Foreign Key می باشد! این نمایی از یک رابطه یک به یک یا one-to-one است. البته در دنیای واقعی کمتر افرادی هستند که آیدی جدول را foreign key کنند بلکه آن ها جداول یک به یک را نیز مانند جداول یک به چند طراحی می کنند (یک ستون اضافه به عنوان foreign key اضافه می کنند).

طبیعتا در حال حاضر ما می توانیم ۲۰۰ ردیف به users اضافه کنیم بدون اینکه به accounts چیزی بدهیم. در این حالت رابطه ما یک به صفر می شود! اما من اصلا قصد بررسی این موضوعات را ندارم. ما می توانیم با استفاده از مفاهیمی مثل Constraint ها یا یک منطق برنامه نویسی خاص در سمت سرور از بروز چنین مشکلی جلوگیری کنیم اما هدف اصلی ما درک روابط یک به یک است نه بررسی مسائل فنی در طراحی ساختار داده!

روابط یک به چند (one-to-many)

ما به صورت روزانه و حتی در زندگی واقعی خودمان با روابط سر و کار داریم. فرض کنید در حال طراحی سیستمی هستید و یک جدول به نام orders (سفارشات) را دارید. چه با مفهوم روابط بین داده ای آشنا باشید و چه نباشید، به صرف برنامه نویس بودنتان ذهن شما از شما می خواهد که یک Foreign Key از جدول سفارشات به جدول users (کاربران) داشته باشید. در این حالت:

  • هر کاربر می تواند چندین سفارش داشته باشد.
  • هر سفارش فقط متعلق به یک کاربر است.

همانطور که می بینید این رابطه از نوع یک (کاربر) به چند (محصول یا سفارش) است. ما در هنگام رسم چنین رابطه ای، از شکل زیر استفاده می کنیم:

نمودار بصری روابط یک به چند در پایگاه داده
نمودار بصری روابط یک به چند در پایگاه داده

در بخشی که این خط به orders متصل می شود به سه بخش منشعب شده است. این حالت «چند» در رابطه یک به چند را نمایش می دهد و نماد تعدد است. اگر یادتان باشد در مورد واژه Cardinality صحبت کردم و گفتم که به معنی تعداد است. به این مبحث کادینالیتی رابطه می گویند چرا که «یک» و «چند» بودن آن را مشخص کرده ایم. اگر بخواهیم این نوع رابطه را با SQL بنویسیم می گوییم:

CREATE TABLE users(

    id INT NOT NULL AUTO_INCREMENT,

    email VARCHAR(100) NOT NULL,

    password VARCHAR(100) NOT NULL,

    PRIMARY KEY(id)

);




CREATE TABLE orders(

    id INT NOT NULL AUTO_INCREMENT,

    user_id INT NOT NULL,

    description VARCHAR(50) NOT NULL,

    PRIMARY KEY(id),

    FOREIGN KEY(user_id) REFERENCES users(id)

);

ستون user_id یک Foreign Key به جدول users است اما unique (یکتا و غیر تکراری) نیست! چرا؟ به دلیل اینکه اگر یک کاربر چندین سفارش داشته باشد، id او در چندین ردیف مختلف از این جدول حضور خواهد داشت و تکراری خواهد بود. از آنجایی که رابطه یک به چند است طبیعتا ما نمی خواهیم این ستون را یکتا کنیم.

روابط چند به چند (many-to-many)

برای درک این نوع روابط نیز به سراغ مثال واقعی می رویم. فرض کنید دو جدول داشته باشیم: authors (نویسندگان) و books (کتاب ها). اگر به این دو مفهوم خوب فکر کنید متوجه چنین رابطه ای می شوید:

  • هر نویسنده می تواند چند کتاب بنویسد.
  • هر کتاب می تواند با همکاری چندین نویسنده نوشته شود.

در این حالت یک رابطه چند به چند یا many-to-many را داریم.

حتی اگر به مثال های دنیای واقعی نیز فکر کنید چنین چیزی را خواهیم داشت. مثلا یک شرکت را در نظر بگیرید که به لوله های پنج اینچی نیاز دارد. در این حالت این شرکت به یک شرکت دیگر که متخصص تولید لوله است سفارش می دهد و مثلا درخواست ۲۰۰ لوله ۵ اینچی می کند. بین این لوله ها و فاکتور های تولید شده رابطه چند به چند برقرار است. چرا؟ به دلیل اینکه هر فاکتور می تواند چندین نوع لوله را در خود داشته باشد و هر نوع لوله می تواند در چندین فاکتور مختلف برای مشتریان مختلف ذکر شود.

به نظر شما چطور می شود یک رابطه چند به چند را در SQL پیاده سازی کرد؟ شاید اولین فکرتان این باشد که یک ستون از هر جدول را به صورت یک foreign key در جدول دیگر ذخیره کنیم اما با انجام این کار به مشکل بزرگی برمی خورید. دوباره به مثال نویسندگان و کتاب ها توجه کنید:

طراحی روابط چند به چند با استفاده از دو foreign key باعث بروز مشکل می شود
طراحی روابط چند به چند با استفاده از دو foreign key باعث بروز مشکل می شود

در نگاه اول همه چیز درست به نظر می آید اما اگر به داده های جدول authors نگاه کنید متوجه یک مشکل می شوید. کتاب هایی که آیدی ۱۲ و ۱۳ را دارند توسط یک نویسنده (آقای Peter با آیدی ۲) نوشته شده اند بنابراین مجبور به تکرار آن ها شده ایم. در این حالت ردیف آقای Peter دائما در حال تکرار شدن است که از نظر طراحی پایگاه داده مشکل دارد (بی دلیل در حال تکرار کردن داده ها در یک جدول هستیم) و گذشته از آن ستون id که باید ستونی یکتا (unique) باشد در حال تکرار شدن است! یعنی در این طراحی هیچ primary key نداریم بنابراین کل طراحی مشکل دار است.

راه حل این است که جدول سومی موسوم به joining table (جدول الحاقی) داشته باشیم که بین جدول نویسندگان و جدول کتاب ها قرار گرفته و آن ها را به هم متصل کند. برای نام گذاری جداول الحاقی معمولا از ترکیب نام جدول اول و دوم استفاده می شود:

طراحی روابط چند به چند با یک جدول الحاقی
طراحی روابط چند به چند با یک جدول الحاقی

در تصویر بالا می بینید که انجام این کار سه نتیجه مثبت داشته است:

  • تعداد ستون های جدول authors کمتر شده است.
  • تعداد ستون های جدول books کمتر شده است.
  • تعداد ردیف های جدول authors کمتر شده است چرا که نیازی به تکرار نویسنده نداریم.

همانطور که می بینید جدول الحاقی ما هیچ primary key ندارد بلکه تمام جدول های الحاقی معمولا فقط دو ستون دارند که هر دو foreign key هایی به دو جدول دیگر هستند. در نهایت برای دسترسی به این مقادیر از دستور JOIN در SQL استفاده می کنیم و داده های مورد نظر از جداول را در هم ادغام می کنیم. چطور؟ این مقاله راجع به کوئری نویسی نیست بنابراین وارد مباحث فنی نمی شوم اما کلیت آن به شکل زیر است:

ابتدا جداول authors و authors_books را بر اساس ستون های id و author_id ادغام می کنیم و سپس جدول های authors_books و books را نیز بر اساس ستون های book_id و id ادغام می کنیم. خسته کننده است، مگر نه؟ به همین دلیل است که بسیاری از افراد از ORM ها استفاده می کنند.

ترسیم مدل روابط در لاراول

حالا که با مفاهیم پایه روابط بین داده ای آشنا شده ایم می توانیم به سراغ لاراول و Eloquent برویم. ساخت data model یا مدل رابطه یک کل منسجم است و نمی توان آن را به بخش های کوچک تر تقسیم کرد بنابراین تصمیم گرفته ام که کل این مفاهیم را در قالب یک پروژه کوچک و عملی به شما نشان بدهم.

در این پروژه می خواهیم روی یک فروشگاه اسباب بازی فرضی کار کنیم. در این فروشگاه ۷ وجود (entity) را خواهیم داشت:

  • users (کاربران)
  • orders (سفارشات - درخواست ثبت شده توسط کاربر برای خرید)
  • invoices (فاکتور ها)
  • items (آیتم های خریداری شده یا همان اسباب بازی ها)
  • categories (دسته بندی های اسباب بازی ها)
  • subcategories (زیردسته های هر دسته بندی)
  • transactions (تراکنش های بانکی)

حالا که entity (وجود) های سسیتم را شناسایی کرده ایم باید به فکر چگونگی ارتباط آن ها با یکدیگر باشیم:

  • کاربران (users) و سفارشات (orders): رابطه یک به چند.
  • سفارشات (orders) و فاکتور ها (invoices): رابطه یک به یک. طبیعتا بسته به نوع کسب و کار شما این رابطه می تواند متفاوت باشد. مثلا اگر شرکت بزرگی دارید که برای هر خرید چند فاکتور برای شرکت های دیگر ارسال می کند دیگر رابطه از نوع یک به یک نخواهد بود اما برای سادگی بحث ما فعلا یک فروشگاه کوچک را در نظر می گیریم که برای هر سفارش یک فاکتور می دهد.
  • سفارشات (orders) و آیتم ها (items): رابطه چند به چند.
  • آیتم ها (items) و دسته بندی ها (categories): رابطه چند به یک. این رابطه نیز به نوع کسب و کارتان بستگی دارد اما برای ساده شدن بحث ما رابطه را چند به یک در نظر می گیریم.
  • دسته بندی ها (categories) و زیردسته ها (Subcategories): رابطه یک به چند. این رابطه نیز به نوع کسب و کارتان بستگی دارد اما برای ساده شدن بحث ما رابطه را یک به چند در نظر می گیریم.
  • سفارشات (orders) و تراکنش ها (Transactions): رابطه یک به چند. چرا؟ برخی تراکنش ها با شکست مواجه می شوند و سپس کاربر دوباره تلاش می کند و مبلغ را در تراکنش بعدی پرداخت می کند. ما برنامه خود را طوری در نظر گرفته ایم که این تراکنش های ناموفق را نیز ذخیره می کند اما اگر شما نخواهید چنین کاری را انجام بدهید طبیعتا نوع رابطه متفاوت خواهد بود.

در ضمن شما می توانستید رابطه ای بین تراکنش ها و فاکتور ها را نیز ایجاد کنید اما من برای ساده تر شدن بحث این کار را نکرده ام. به غیر از این مورد روابط دیگری نیز قابل تصور هستند اما برای برنامه ما کاربرد عملی ندارند. به طور مثال می توان گفت که هر کاربر چندین تراکنش دارد اما این رابطه چه فایده ای دارد؟ با نگاه به روابط بالا می فهمید که در حال حاضر یک رابطه نانوشته بین کاربر و تراکنش ها وجود دارد (users -> orders -> transactions) بنابراین نیازی به تکرار آن نیست. علاوه بر این برای ایجاد چنین تراکنشی باید یک ستون دیگر به نام user_id را در جدول transactions ایجاد می کردیم که فقط حجم داده های ما را بالا می برد.

یک پروژه عملی با لاراول - تعریف روابط

حالا که مفاهیم انتزاعی را مرتب کرده ایم زمان کار عملی رسیده است. همانطور که در ابتدای مقاله توضیح داده شد من فرض می کنم که شما کار با لاراول را بلد هستید و توضیحات اضافی برای دستورات آن نمی دهم بلکه تمرکز خود را روی کدهای روابط می گذاریم. ابتدا یک پروژه لاراول جدید را ایجاد می کنیم:

composer global require laravel/installer -W

laravel new model-relationships-study

فلگ W- باعث به روز رسانی می شود (من قبلا installer لاراول را نصب کرده بودم).

همانطور که گفتم در ORM ها فایل های شما تا حد زیادی خودشان را شبیه به ساختار پایگاه داده می کنند بنابراین باید ابتدا migration ها و model های مورد نظرمان را ایجاد کنیم و سپس به سراغ تعریف روابط برویم. ساده ترین مدل در طراحی ما User است بنابراین از همان شروع می کنیم. همانطور که می دانید می توانیم با دستور زیر یک مدل بسازیم:

php artisan make:model User

اما در پروژه های لاراول به صورت پیش فرض یک مدل User داریم بنابراین نیازی به اجرای دستور بالا نیست بلکه می توانیم مستقیما آن را ویرایش کنیم. باز هم می گویم که من این منطق چنین مباحث ساده ای را توضیح نخواهم داد. من فایل مدل User را بدین شکل ویرایش می کنم:

class CreateUsersTable extends Migration

{

    public function up()

    {

        Schema::create('users', function (Blueprint $table) {

            $table->id();

            $table->string('name');

        });

    }

}

چرا؟ به دلیل اینکه هدف ما بررسی روابط است بنابراین نیازی به ستون password و is_active و امثال آن نداریم. کاربران ما در این برنامه فقط یک آیدی و یک نام خواهند داشت. در مرحله بعدی می خواهیم یک migration را برای Category (دسته بندی ها) بسازیم. برای ساخت migration به همراه model برای category دستور زیر را اجرا می کنیم:

php artisan make:model Category -m

و چنین نتیجه ای خواهید گرفت:

Model created successfully.

Created Migration: 2021_01_26_093326_create_categories_table

حالا کلاس migration ما بدین شکل خواهد بود:

class CreateCategoriesTable extends Migration

{

    public function up()

    {

        Schema::create('categories', function (Blueprint $table) {

            $table->id();

            $table->string('name');

        });

    }

}

در مرحله بعدی به سراغ ساخت migration و model برای SubCategory می رویم:

php artisan make:model SubCategory -m

و نتیجه آن:

Model created successfully.

Created Migration: 2021_01_26_140845_create_sub_categories_table

حالا می توانیم این کلاس را به شکل زیر ویرایش کنیم:

class CreateSubCategoriesTable extends Migration

{

    public function up()

    {

        Schema::create('sub_categories', function (Blueprint $table) {

            $table->id();

            $table->string('name');




            $table->unsignedBigInteger('category_id');

            $table->foreign('category_id')

                ->references('id')

                ->on('categories')

                ->onDelete('cascade');

        });

    }

}

ما یک ستون دیگر به نام category_id را ایجاد کرده ایم که آیدی های جدول categories در این جدول ذخیره خواهد کرد بنابراین یک foreign key است. با انجام این کار یک رابطه یک به چند را بین دسته بندی ها و زیردسته ها در سطح پایگاه داده ایجاد کرده ایم، یعنی همان کاری که با SQL کرده بودیم. طبیعتا ایجاد روابط فقط در سطح پایگاه داده کافی نیست و به کار بیشتری نیاز دارد.

در مرحله بعدی به سراغ items می رویم:

php artisan make:model Item -m

و کلاس migration تولید شده را بدین شکل ویرایش می کنیم:

class CreateItemsTable extends Migration

{

    public function up()

    {

        Schema::create('items', function (Blueprint $table) {

            $table->id();

            $table->string('name');

            $table->text('description');

            $table->string('type');

            $table->unsignedInteger('price');

            $table->unsignedInteger('quantity_in_stock');




            $table->unsignedBigInteger('sub_category_id');

            $table->foreign('sub_category_id')

                ->references('id')

                ->on('sub_categories')

                ->onDelete('cascade');

        });

    }

}

از کد بالا مشخص است که هر آیتم به یک زیردسته (subcategory) به شکل رابطه یک به چند متصل شده است. چرا؟ به دلیل اینکه هر آیتم حتما به یک زیردسته متعلق است. از طرفی به خاطر این رابطه می توان گفت که هر آیتم به صورت غیر مستقیم به یک دسته بندی (category) نیز متصل است.

اگر نظر شما این است که می توانستیم به روش دیگری این کار را انجام بدهیم چطور؟ به مرور زمان متوجه خواهید شد که طراحی پایگاه داده فقط به یک شکل انجام نمی شود بلکه در هر وب سایت روش های مختلفی برای پیاده سازی پایگاه داده وجود دارد. مسئله که شما باید به آن توجه کنید بد نبودن راه حل است.

در مرحله بعدی به سراغ سفارشات (Order) می رویم:

php artisan make:model Order -m

و کلاس تولید شده را به شکل زیر ویرایش می کنیم:

class CreateOrdersTable extends Migration

{

    public function up()

    {

        Schema::create('orders', function (Blueprint $table) {

            $table->id();

            $table->string('status');

            $table->unsignedInteger('total_value');

            $table->unsignedInteger('taxes');

            $table->unsignedInteger('shipping_charges');




            $table->unsignedBigInteger('user_id');

            $table->foreign('user_id')

                ->references('id')

                ->on('users')

                ->onDelete('cascade');

        });

    }

}

احتمالا با خودتان می گویید پس آیتم های این سفارش کجا هستند؟ همانطور که قبلا توضیح دادم رابطه ای چند به چند بین سفارشات (orders) و آیتم (items) وجود دارد و از طرفی هم توضیح دادم که نمی توانیم روی هر دو جدول یک foreign key به جدول دیگر بگذاریم. با این حساب به یک جدول الحاقی نیاز داریم که بین این دو جدول ارتباط برقرار کند.

خوشبختانه در لاراول ترفندی برای ساخت جداول الحاقی وجود دارد. برای این کار باید:

  • جدول جدیدی بسازید (این همان جدول الحاقی خواهد بود).
  • نام دو جدول اصلی را به شکل مفرد (بدون s جمع) و با آندراسکور (علامت _) به هم متصل کرده و به عنوان نام جدول الحاقی استفاده کنید.
  • نام دو جدول اصلی باید بر اساس حروف الفبای انگلیسی مرتب شوند. مثلا اگر جدول اول A و جدول دوم B بود، نام جدول الحاقی باید به شکل A_B باشد (B_A غلط است).

با انجام این کار لاراول به صورت خودکار تشخیص می دهد که جدول سوم یک جدول الحاقی برای دو جدول دیگر است. ما اگر این مراحل را انجام بدهیم، نام جدول الحاقی item_order خواهد شد. از آنجایی که این جدول هیچ نیازی به مدل ندارد (هیچ وقت به طور مستقیم با آن کار نخواهیم کرد) نیازی به ساخت مدل نیست و فقط یک migration را ایجاد می کنیم:

php artisan make:migration create_item_order_table --create="item_order"

حالا کلاس ساخته شده را بدین شکل ویرایش می کنیم:

class CreateItemOrderTable extends Migration

{

    public function up()

    {

        Schema::create('item_order', function (Blueprint $table) {

            $table->unsignedBigInteger('order_id');

            $table->foreign('order_id')

                ->references('id')

                ->on('orders')

                ->onDelete('cascade');

           

            $table->unsignedBigInteger('item_id');

            $table->foreign('item_id')

                ->references('id')

                ->on('items')

                ->onDelete('cascade');   

        });

    }

}

همانطور که گفتم جداول الحاقی معمولا فقط دو ستون دارند که هر foreign key هایی برای ستون های اصلی هستند. ما نیز دقیقا همین کار را کرده ایم.

مدل بعدی ما مدل invoices (فاکتور ها) است:

php artisan make:model Invoice -m

در توضیحات روابط یک به یک به شما نشان دادم که ما می توانیم آیدی یک جدول (primary key) را به عنوان foreign key نیز در نظر بگیریم و بدین شکل روابط یک به یک را تشکیل بدهیم. بسیاری از افراد چنین کاری را انجام نمی دهند و به جای آن یک ستون اضافه را تعریف کرده و به عنوان foreign key در نظر می گیرند. روشی ترکیبی نیز وجود دارد که هر دو روش قبلی را با هم ترکیب می کند. در این روش foreign key را unique (یکتا و غیر تکراری) می کنیم تا مطمئن شویم آیدی مدل پدر تکرار نمی شود:

class CreateInvoicesTable extends Migration

{

    public function up()

    {

        Schema::create('invoices', function (Blueprint $table) {

            $table->id();

            $table->timestamp('raised_at')->nullable();

            $table->string('status');

            $table->unsignedInteger('totalAmount');




            $table->unsignedBigInteger('order_id')->unique();

            $table->foreign('order_id')

                ->references('id')

                ->on('orders')

                ->onDelete('cascade')

                ->unique();

        });

    }

}

باز هم می گویم که فاکتور های ما می توانند بسیار پیشرفته تر و با جزئیات بیشتری باشند اما هدف من ساده کردن این مدل ها برای درک بهتر روابط بین آن ها است و قصد نداریم یک پایگاه داده واقعی را طراحی کنیم.

حالا به آخرین مدل وب سایت خود یعنی Transaction (تراکنش ها) می رسیم. قبلا تصمیم گرفتیم که تراکنش ها را به order ها (سفارشات) متصل کنیم:

php artisan make:model Transaction -m

و در نهایت کلاس تولید شده را ویرایش می کنیم:

class CreateTransactionsTable extends Migration

{

    public function up()

    {

        Schema::create('transactions', function (Blueprint $table) {

            $table->id();

            $table->timestamp('executed_at');

            $table->string('status');

            $table->string('payment_mode');

            $table->string('transaction_reference')->nullable();




            $table->unsignedBigInteger('order_id');

            $table->foreign('order_id')

                ->references('id')

                ->on('orders')

                ->onDelete('cascade');

        });

    }

}

با این حساب یک foreign key را به جدول order ها داریم تا به هم متصل باشند. در نهایت باید migration های تولید شده را اجرا کنیم:

php artisan migrate:fresh

با انجام این دستور چنین نتیجه ای را دریافت می کنید:

Dropped all tables successfully.

Migration table created successfully.

Migrating: 2014_10_12_000000_create_users_table

Migrated:  2014_10_12_000000_create_users_table (3.45ms)

Migrating: 2021_01_26_093326_create_categories_table

Migrated:  2021_01_26_093326_create_categories_table (2.67ms)

Migrating: 2021_01_26_140845_create_sub_categories_table

Migrated:  2021_01_26_140845_create_sub_categories_table (3.83ms)

Migrating: 2021_01_26_141421_create_items_table

Migrated:  2021_01_26_141421_create_items_table (6.09ms)

Migrating: 2021_01_26_144157_create_orders_table

Migrated:  2021_01_26_144157_create_orders_table (4.60ms)

Migrating: 2021_01_27_093127_create_item_order_table

Migrated:  2021_01_27_093127_create_item_order_table (3.05ms)

Migrating: 2021_01_27_101116_create_invoices_table

Migrated:  2021_01_27_101116_create_invoices_table (3.95ms)

Migrating: 2021_01_31_145806_create_transactions_table

Migrated:  2021_01_31_145806_create_transactions_table (3.54ms)

حالا که تمام مدل هایمان ساخته شده است و رابطه آن ها را در سطح پایگاه داده نیز تعریف کرده ایم باید به سراغ تعریف رابطه آن ها در لاراول (در سطح اسکریپت های سرور) برویم.

اولین رابطه یک رابطه یک به چند بین کاربران و سفارشات بود. برای تایید این مسئله می توانیم به فایل های migration سفارشات رفته و ستون user_id را مشاهده کنیم. برای تعریف این رابطه در Eloquent ابتدا به فایل مدل Order و چنین تابعی را در آن تعریف می کنیم:

<?php




namespace App\Models;




use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;




class Order extends Model

{

    use HasFactory;




    public function user() {

        return $this->belongsTo(User::class);

    }

}

من نام تابع را user گذاشته ام چرا که می خواهیم رابطه بین سفارش و کاربر (user) را نمایش بدهیم و این مسئله تبدیل به یک استاندارد در لاراول شده است اما در نظر داشته باشید که نام این تابع می تواند هر چیزی باشد و تنها مسئله مهم چیزی است که این تابع برمی گرداند.

درون این تابع گفته ایم this$ (مدل order) به مدل User (کاربر) تعلق دارد. belongsTo به معنی «متعلق بودن به ...» می باشد. چرا از این دستور استفاده کرده ایم؟ به دلیل اینکه هر سفارش متعلق به یک کاربر است. توجه داشته باشید که بدون تعریف foreign key در پایگاه داده (فیلد user_id) لاراول نمی توانست این موضوع را تشخیص بدهد و رابطه ای را تعریف کند بنابراین تعریف رابطه در هر دو سطح پایگاه داده و اسکریپت الزامی است. کاری که لاراول در اینجا می کند فقط ساده تر کردن دستورات SQL برای ما است.

با انجام این کار می توانیم از طریق سفارش به کاربر آن دسترسی داشته باشیم اما برعکس آن چطور؟ ما می خواهیم از طریق کاربر به سفارشات او نیز دسترسی داشته باشیم (user->orders$). برای انجام این کار باید به مدل User رفته و حالت معکوس رابطه قبلی را در آن بنویسیم اما توجه داشته باشید که هر کاربر چندین سفارش خواهد داشت:

<?php




namespace App\Models;




use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;




class User extends Model

{

    use HasFactory;

    public $timestamps = false;




    public function orders() {

        return $this->hasMany(Order::class);

    }

}

متد hasMany به معنی «چندین ... دارد» است بنابراین در این مثال گفته ایم که مدل کاربر (User) چندین سفارش (Order) دارد. نام گذاری این تابع به orders اجباری نیست اما یک استاندارد در لاراول است و علاوه بر آن کدنویسی شما را راحت تر و واضح تر می کند. در ضمن توجه داشته باشید که باید کلاس مدل Order را مستقیما پاس بدهید (ما از class:: استفاده کرده ایم).

احتمالا می گویید چرا همه چیز را از مدل User پاک کرده ام. هدف ما توضیح روابط بین مدل ها است بنابراین نیازی به کدهای دیگر نداریم. شاید با خودتان بگویید در سطح پایگاه داده هیچ رابطه ای تعریف نشده است که هر users چندین orders داشته باشد بنابراین این رابطه چطور کار می کند؟ اگر به جدول orders نگاه کنید متوجه حضور یک foreign key به جدول users می شوید. بنابراین زمانی که دستوری مانند user->orders$ را اجرا می کنیم، لاراول به سراغ متد ()orders می رود و خودش foreign key را تشخیص می دهد. از آنجا دستور SQL شما تقریبا به شکل زیر می شود:

SELECT * FROM orders WHERE user_id = 23

حالا که نحوه تعریف این روابط را درک کرده اید نیازی به توضیح اضافه نیست بلکه می توانیم سریعا روابط دیگر را نیز تعریف کنیم. بین سفارشات و فاکتور ها رابطه ای یک به یک برقرار است بنابراین به مدل Order رفته و می گوییم:

<?php




namespace App\Models;




use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;




class Order extends Model

{

    use HasFactory;

    public $timestamps = false;




    public function user() {

        return $this->belongsTo(User::class);

    }




    public function invoice() {

        return $this->hasOne(Invoice::class);

    }

}

hasOne به معنی «یک ... دارد» می باشد و در اینجا گفته ایم هر سفارش یک فاکتور دارد. حالت معکوس این رابطه را نیز در مدل invoice تعریف می کنیم:

<?php




namespace App\Models;




use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;




class Invoice extends Model

{

    use HasFactory;

    public $timestamps = false;




    public function order() {

        return $this->belongsTo(Order::class);

    }

}

belongsTo نیز یعنی «متعلق است به ...» بنابراین هر فاکتور متعلق به یک سفارش است.

بین سفارشات و آیتم های خریداری شده یک رابطه چند به چند برقرار است. ما قبلا جدولی الحاقی به نام item_order را برای این دو مورد ایجاد کردیم بنابراین کار زیادی باقی نمانده است. در این حالت از متد belongsToMany استفاده می کنیم که معنی «به چندین ... تعلق دارد» را می دهد. ابتدا از مدل Item شروع می کنیم:

<?php




namespace App\Models;




use Illuminate\Database\Eloquent\Factories\HasFactory;

use Illuminate\Database\Eloquent\Model;




class Item extends Model

{

    use HasFactory;

    public $timestamps = false;




    public function orders() {

        return $this->belongsToMany(Order::class);

    }

}

از آنجایی که رابطه چند به چند است، حالت معکوس آن نیز دقیقا به همین شکل خواهد بود:

class Order extends Model

{

    /* بقیه کدها */

   

    public function items() {

        return $this->belongsToMany(Item::class);

    }

}

اگر قوانین مربوط به نام گذاری جدول الحاقی را رعایت کرده باشید حالا کارتان تمام شده است و لاراول خودش می داند با این روابط چه کار کند.

از آنجایی که تمام انواع رابطه (یک به یک، یک به چند و چند به چند) را تعریف کردیم بقیه کدها برای دیگر مدل ها همگی تکراری هستند و من برای طولانی نشدن مقاله آن ها را نمی نویسم. این روابط بسیار ساده هستند و خودتان می توانید با مراجعه به مدل های باقی مانده کدها را تکمیل کنید.

پر کردن پایگاه داده و کار با روابط

همانطور که می دانید لاراول قابلیت به نام factory دارد. factory به معنی «کارخانه» است و کارش تولید داده های جعلی برای پایگاه داده شما است تا بتوانید دستورات مربوط به پایگاه داده خود را تست کنید. factory های شما در آدرس database/factories در پروژه قرار دارند و به صورت پیش فرض یک factory برای User به نام UserFactory.php در پروژه های لاراول موجود است:

namespace Database\Factories;




use App\Models\User;

use Illuminate\Database\Eloquent\Factories\Factory;

use Illuminate\Support\Str;




class UserFactory extends Factory

{

    /**

     * The name of the factory's corresponding model.

     *

     * @var string

     */

    protected $model = User::class;




    /**

     * Define the model's default state.

     *

     * @return array

     */

    public function definition()

    {

        return [

            'name' => $this->faker->name(),

            'email' => $this->faker->unique()->safeEmail(),

            'email_verified_at' => now(),

            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password

            'remember_token' => Str::random(10),

        ];

    }

}

در نظر داشته باشید که factory ها نحوه پر شدن پایگاه داده از داده های جعلی را توضیح می دهند و seeder ها داده های جعلی را در پایگاه داده قرار می دهند. seeder ها به صورت پیش فرض در آدرس database/seeders هستند و یک seeder پیش فرض نیز برایتان ساخته شده است.

معمولا از seeder های مختلفی استفاده می شود اما من برای سریع تر شدن کار همه چیز را در همان فایل seeder پیش فرض تعریف می کنم، یعنی مستقیما به فایل DatabaseSeeder.php رفته و آن را بدین شکل ویرایش می کنم:

<?php




namespace Database\Seeders;




use Illuminate\Database\Seeder;

use App\Models\Category;

use App\Models\SubCategory;

use App\Models\Item;

use App\Models\Order;

use App\Models\Invoice;

use App\Models\User;

use Faker;




class DatabaseSeeder extends Seeder

{

    public function run()

    {

        $faker = Faker\Factory::create();




        // در اینجا دو کاربر را می سازیم

        $user1 = User::create(['name' => $faker->name]);

        $user2 = User::create(['name' => $faker->name]);




        // دو دسته بندی می سازیم که هر یک دو زیردسته داشته باشند

        $category1 = Category::create(['name' => $faker->word]);

        $category2 = Category::create(['name' => $faker->word]);




        $subCategory1 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);

        $subCategory2 = SubCategory::create(['name' => $faker->word, 'category_id' => $category1->id]);




        $subCategory3 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);

        $subCategory4 = SubCategory::create(['name' => $faker->word, 'category_id' => $category2->id]);




        // بعد از دسته بندی ها نوبت به آیتم ها می رسد

        // دو آیتم را تعریف می کنیم که متعلق به زیردسته های دو و چهار باشند

        $item1 = Item::create([

            'sub_category_id' => 2,

            'name' => $faker->name,

            'description' => $faker->text,

            'type' => $faker->word,

            'price' => $faker->randomNumber(2),

            'quantity_in_stock' => $faker->randomNumber(2),

        ]);




        $item2 = Item::create([

            'sub_category_id' => 2,

            'name' => $faker->name,

            'description' => $faker->text,

            'type' => $faker->word,

            'price' => $faker->randomNumber(3),

            'quantity_in_stock' => $faker->randomNumber(2),

        ]);




        $item3 = Item::create([

            'sub_category_id' => 4,

            'name' => $faker->name,

            'description' => $faker->text,

            'type' => $faker->word,

            'price' => $faker->randomNumber(4),

            'quantity_in_stock' => $faker->randomNumber(2),

        ]);




        $item4 = Item::create([

            'sub_category_id' => 4,

            'name' => $faker->name,

            'description' => $faker->text,

            'type' => $faker->word,

            'price' => $faker->randomNumber(1),

            'quantity_in_stock' => $faker->randomNumber(3),

        ]);




        // در این بخش کاری می گوییم کاربر اول باید چند سفارش داشته باشد

        $order1 = Order::create([

            'status' => 'confirmed',

            'total_value' => $faker->randomNumber(3),

            'taxes' => $faker->randomNumber(1),

            'shipping_charges' => $faker->randomNumber(2),

            'user_id' => $user1->id

        ]);




        $order2 = Order::create([

            'status' => 'waiting',

            'total_value' => $faker->randomNumber(3),

            'taxes' => $faker->randomNumber(1),

            'shipping_charges' => $faker->randomNumber(2),

            'user_id' => $user1->id

        ]);




        // حالا آیتم ها ساخته شده را به سفارشات متصل می کنیم

        $order1->items()->attach($item1);

        $order1->items()->attach($item2);

        $order1->items()->attach($item3);

       

        $order2->items()->attach($item1);

        $order2->items()->attach($item4);




        // و در نهایت فاکتور ها را تولید می کنیم

        $invoice1 = Invoice::create([

            'raised_at' => $faker->dateTimeThisMonth(),

            'status' => 'settled',

            'totalAmount' => $faker->randomNumber(3),

            'order_id' => $order1->id,

        ]);

    }

}

حالا می توانیم migration را دوباره از صفر اجرا کنیم اما این بار فلگ seed-- را نیز به آن می دهیم تا از فایل seeder بالا استفاده کند:

php artisan migrate:fresh --seed

نتیجه تقریبا به شکل زیر خواهد بود:

Dropped all tables successfully.

Migration table created successfully.

Migrating: 2014_10_12_000000_create_users_table

Migrated:  2014_10_12_000000_create_users_table (43.81ms)

Migrating: 2021_01_26_093326_create_categories_table

Migrated:  2021_01_26_093326_create_categories_table (2.20ms)

Migrating: 2021_01_26_140845_create_sub_categories_table

Migrated:  2021_01_26_140845_create_sub_categories_table (4.56ms)

Migrating: 2021_01_26_141421_create_items_table

Migrated:  2021_01_26_141421_create_items_table (5.79ms)

Migrating: 2021_01_26_144157_create_orders_table

Migrated:  2021_01_26_144157_create_orders_table (6.40ms)

Migrating: 2021_01_27_093127_create_item_order_table

Migrated:  2021_01_27_093127_create_item_order_table (4.66ms)

Migrating: 2021_01_27_101116_create_invoices_table

Migrated:  2021_01_27_101116_create_invoices_table (6.70ms)

Migrating: 2021_01_31_145806_create_transactions_table

Migrated:  2021_01_31_145806_create_transactions_table (6.09ms)

Database seeding completed successfully.

یعنی همه چیز با موفقیت اجرا شده است.

در این مرحله باید به tinker وارد شده و شروع به تست کردن روابط کنیم بنابراین دستور زیر را اجرا کنید:

php artisan tinker

من انتظار دارم شما کار با tinker را بلد باشید. در ابتدا از روابط یک به یک شروع می کنم:

>>> $order = Order::find(1);

[!] Aliasing 'Order' to 'App\Models\Order' for this Tinker session.

=> App\Models\Order {#4108

     id: 1,

     status: "confirmed",

     total_value: 320,

     taxes: 5,

     shipping_charges: 12,

     user_id: 1,

   }

>>> $order->invoice

=> App\Models\Invoice {#4004

     id: 1,

     raised_at: "2021-01-21 19:20:31",

     status: "settled",

     totalAmount: 314,

     order_id: 1,

   }

همانطور که می بینید با استفاده از متد find یک سفارش را دریافت کرده و آن را در متغیر order$ قرار داده ایم. در مرحله بعدی با استفاده از آن متغیر به فاکتور آن دسترسی پیدا کرده ایم. از نتیجه دریافت شده متوجه می شویم که رابطه به درستی کار می کند. این رابطه می توانست از نوع یک به چند باشد و ممکن بود لاراول چندین فاکتور را برایمان برگرداند اما ما برای لاراول مشخص کردیم که رابطه حتما از نوع یک به یک است بنابراین فقط یک شیء برایمان برگردانده شده است. شما می توانید این موضوع را با حالت برعکس رابطه نیز تست کنید:

$invoice = Invoice::find(1);

[!] Aliasing 'Invoice' to 'App\Models\Invoice' for this Tinker session.

=> App\Models\Invoice {#3319

     id: 1,

     raised_at: "2021-01-21 19:20:31",

     status: "settled",

     totalAmount: 314,

     order_id: 1,

   }

>>> $invoice->order

=> App\Models\Order {#4042

     id: 1,

     status: "confirmed",

     total_value: 320,

     taxes: 5,

     shipping_charges: 12,

     user_id: 1,

   }

حالا نوبت به روابط یک به چند می رسد. چنین رابطه ای بین کاربران و سفارشاتشان برقرار است بنابراین در tinker یک کاربر را گرفته و سپس به سراغ سفارشاتش می رویم:

>>> User::find(1)->orders;

[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.

=> Illuminate\Database\Eloquent\Collection {#4291

     all: [

       App\Models\Order {#4284

         id: 1,

         status: "confirmed",

         total_value: 320,

         taxes: 5,

         shipping_charges: 12,

         user_id: 1,

       },

       App\Models\Order {#4280

         id: 2,

         status: "waiting",

         total_value: 713,

         taxes: 4,

         shipping_charges: 80,

         user_id: 1,

       },

     ],

   }

>>> Order::find(1)->user

=> App\Models\User {#4281

     id: 1,

     name: "Dallas Kshlerin",

   }

از آنجایی که رابطه از نوع یک به چند است، هنگام دسترسی به سفارشات یک کاربر مجموعه ای از نتایج را دریافت می کنیم اما هنگام دسترسی به صاحب سفارش فقط یک نفر برایمان برگردانده می شود.

در نهایت کار با روابط چند به چند نیز بسیار ساده است. چنین رابطه ای بین آیتم ها و سفارشات برقرار است:

>>> $item1 = Item::find(1);

[!] Aliasing 'Item' to 'App\Models\Item' for this Tinker session.

=> App\Models\Item {#4253

     id: 1,

     name: "Russ Kutch",

     description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",

     type: "adipisci",

     price: 26,

     quantity_in_stock: 65,

     sub_category_id: 2,

   }

>>> $order1 = Order::find(1);

=> App\Models\Order {#4198

     id: 1,

     status: "confirmed",

     total_value: 320,

     taxes: 5,

     shipping_charges: 12,

     user_id: 1,

   }

>>> $order1->items

=> Illuminate\Database\Eloquent\Collection {#4255

     all: [

       App\Models\Item {#3636

         id: 1,

         name: "Russ Kutch",

         description: "Deserunt voluptatibus omnis ut cupiditate doloremque. Perspiciatis officiis odio et accusantium alias aut. Voluptatum provident aut ut et.",

         type: "adipisci",

         price: 26,

         quantity_in_stock: 65,

         sub_category_id: 2,

         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4264

           order_id: 1,

           item_id: 1,

         },

       },

       App\Models\Item {#3313

         id: 2,

         name: "Mr. Green Cole",

         description: "Maxime beatae porro commodi fugit hic. Et excepturi natus distinctio qui sit qui. Est non non aut necessitatibus aspernatur et aspernatur et. Voluptatem possimus consequatur exercitationem et.",

         type: "pariatur",

         price: 381,

         quantity_in_stock: 82,

         sub_category_id: 2,

         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4260

           order_id: 1,

           item_id: 2,

         },

       },

       App\Models\Item {#4265

         id: 3,

         name: "Brianne Weissnat IV",

         description: "Delectus ducimus quia voluptas fuga sed eos esse. Rerum repudiandae incidunt laboriosam. Ea eius omnis autem. Cum pariatur aut voluptas sint aliquam.",

         type: "non",

         price: 3843,

         quantity_in_stock: 26,

         sub_category_id: 4,

         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4261

           order_id: 1,

           item_id: 3,

         },

       },

     ],

   }

>>> $item1->orders

=> Illuminate\Database\Eloquent\Collection {#4197

     all: [

       App\Models\Order {#4272

         id: 1,

         status: "confirmed",

         total_value: 320,

         taxes: 5,

         shipping_charges: 12,

         user_id: 1,

         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4043

           item_id: 1,

           order_id: 1,

         },

       },

       App\Models\Order {#4274

         id: 2,

         status: "waiting",

         total_value: 713,

         taxes: 4,

         shipping_charges: 80,

         user_id: 1,

         pivot: Illuminate\Database\Eloquent\Relations\Pivot {#4257

           item_id: 1,

           order_id: 2,

         },

       },

     ],

   }

همانطور که در نتیجه بالا می بینید item1 (آیتم ۱) جزئی از آیتم های order1 (سفارش ۱) است و برعکس این موضوع نیز برقرار است. اگر بخواهید این موضوع را بهتر متوجه شوید می توانیم به جدول الحاقی item_order نیز نگاهی بیندازیم:

>>> use DB;

>>> DB::table('item_order')->select('*')->get();

=> Illuminate\Support\Collection {#4290

     all: [

       {#4270

         +"order_id": 1,

         +"item_id": 1,

       },

       {#4276

         +"order_id": 1,

         +"item_id": 2,

       },

       {#4268

         +"order_id": 1,

         +"item_id": 3,

       },

       {#4254

         +"order_id": 2,

         +"item_id": 1,

       },

       {#4267

         +"order_id": 2,

         +"item_id": 4,

       },

     ],

   }

حالا متوجه می شویم که این جدول چطور کار می کند.


منبع: وب سایت geekflare

نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.