Mohamad
Mohamad
خواندن ۱۴ دقیقه·۵ سال پیش

درک روابط دیتابیس در پکیج ORM لاراول با تحلیل پروژه واقعی!


gliacoding.com
gliacoding.com

خب این پست رو مینویسم برای بیشتر یادگرفتن خودم و همینطور کمک به اونایی که برای درک روابط بین جدول های دیتابیس در لاراول یا هر فریم ورک دیگه ای مشکل دارن!

این پست مخصوصا برای لاراول کارا نوشته شده، اما میتونه برای هر کسی مفید باشه و حتی کسایی که با زبان های دیگه ای کار میکنن هم به دردشون میخوره.

اول از همه بریم سراغ شناخت انواع روابط یا Relationship در پایگاه داده.

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

ما میتونیم انواع رابطه ها رو داشته باشیم، بیاید از خودمون شروع کنیم : بین شما و پدر و مادرتون یک رابطه یک به یک وجود داره، بین شما و دوستانتون میتونه یک رابطه یک به چند وجود داشته باشه و بین اعضای دو گروه میشه یک رابطه چند به چند رو در نظر گرفت.

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

حالا که خوب روابط رو درک کردیم و من انتظار دارم دانش کافی برای کار با روابط در دیتابیس رو دارین، حالا بریم سراغ لاراول و اینکه چقدر کار با دیتابیس در لاراول راحت و جذاب انجام میشه.

اگر در درک روابط ساده دیتابیس مشکل دارین این منابع بهتون کمک میکنه یاد بگیرید :

انواع رابطه میان جدول ها در پایگاه داده رابطه ای

مفهوم پایگاه داده رابطه ای relational database

روابطی که در مقاله اول این سری آموزش یاد میگیریم :

  • یک به یک ( one-to-one )
  • یک به چند ( one-to-many )
  • چند به چند ( many-to-many )

برای درک بهتر ما از یک پروژه فرضی استفاده و انواع روابط رو در این پروژه بررسی میکنیم.

برای مثال در نظر بگیرید، ما یک پلتفرمی ساده مارکت پلیس رو داریم که این امکان رو به فروشنده ها میده تا با افراد مختلف تعامل داشته باشن و محصولاتشون رو بصورت عمده یا تکی به فروش بذارن.


برای شروع کار من یک پروژه جدید لاراول با استفاده از پکیج منیجر کامپوزر با نام TopSeller ایجاد میکنم.

composer create-project --prefer-dist laravel/laravel TopSeller

حالا بریم سراغ تحلیل پروژه و قبل از هر چیزی دیاگرام جدول هامون رو رسم میکنیم تا یک دید کلی از نحوه کارکرد پروژه ساده مون داشته باشیم.

برای رسم دیاگرام ابزار های زیاد و متنوعی وجود داره مثل draw.io

من از پلاگین جدید draw io در vscode استفاده میکنم که فوق العاده عالیه و اگر vscode دارین حتما نصب کنید. این پلاگین با ارتباط با وبسایت Draw.io این امکان رو فراهم میکنه تا توو همون vscode دیاگرام هامون رو رسم کنیم.

Draw.io Integration vscode plugin
Draw.io Integration vscode plugin


دیاگرام موجودیت های دیتابیس پروژه
دیاگرام موجودیت های دیتابیس پروژه


در پروژه ما برای شروع MVP میتونیم 7 جدول داشته باشیم که روال کار رو بصورت ساده انجام بدیم.

  • users (کاربران)
  • shops (فروشگاه ها)
  • shops_setting (تنظیمات بیشتر فروشگاه ها)
  • products (محصولات)
  • carts (سبد های خرید)
  • invoices (صورت حساب ها)
  • cart_product (جدول رابط محصولات با سبد های خرید)

اگر دقت کرده باشید، ما پلتفرمی رو داریم که هر کاربر میتونه یک یا چند فروشگاه با محصولات متنوع و مختلف داشته باشه. سبد های خرید رو داریم که یک رابطه چند به چند رو با محصولات برقرار میکنه، که در ادامه درباره همه اینها صحبت و تحلیل میکنیم.

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

رابطه یک به یک همونطور که از نامش پیداست، یعنی بین دو رکورد در دو جدول مختلف تنها یک رابطه میتونه وجود داشته باشه یا اصلا هیچ رابطه ای نباشه.

در پروژه ما هر کاربر فقط میتونه یک سبد خرید داشته باشه یا میتونه هیچ سبد خریدی نداشته باشه و صرفا یه فروشنده باشه.

users and carts table diagram
users and carts table diagram


پس ما برای دریافت سبد خرید یک کاربر در مدل یوزر متود cart رو خواهیم داشت :

User Model Class
User Model Class


در پکیج ORM لاراول واقع در کلاس بیس مدل، میتونیم با استفاده از متود hasOne رابطه یک به یک رو داشته باشیم.

متود hasOne کوئری زیر رو در دیتابیس اجرا میکنه :

SELECT * FROM `carts` WHERE `carts`.`user_id` = ? AND `carts`.`user_id` IS NOT NULL

حالا اگر بخوایم در برناممون سبد خرید کاربر رو بگیریم :

$user = User::find(1); $user->cart;

در اینجا در پارامتر اول آدرس کلاس مدلی که جدول ما باهاش ارتباط داره رو قرار میدیم که برای جلوگیری از تکرار از property استاتیک class:: استفاده میکنیم.

در پارامتر دوم foreign_key یا کلید خارجی جدولمون که در جدول مورد نظر قرار داره رو وارد میکنیم که برابر با user_id هستش.

در پارامتر سوم local_key یا کلید داخلی یا همون primary_key که به جدول کنونی خودمون (Users) مربوط میشه وارد میکنیم.

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

درباره نحوه کارکرد الگوریتم پکیج ORM لاراول، آخر مقاله در قسمت رابطه چند به چند کارکرد این الگوریتم رو شرح دادم.

حالا اگر بخوایم از مدل سبد خرید به صاحب دسترسی داشته باشیم از همین متود استفاده میکنیم (در مدل Cart) :

Cart Model Class
Cart Model Class

اصلاح (آپدیت 98.6.10) :

در تصویر بالا بجای متود hasOne باید از متود belongsTo استفاده بشه.


دسترسی از طریق سبد خرید به کاربر در برنامه هم مثل همیشگیه، در واقع این مربوط به همه روابط میشه و به همین سادگی میتونیم اطلاعات رکورد رابطمون رو داشته باشیم :

$cart= Cart::find(1); $cart->user;

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

این رابطه یکی از پر استفاده ترین و پر کاربرد ترین روابط دیتابیسه، در رابطه یک به چند، یک رکورد میتونه با چندین رکورد در ارتباط باشه و هیچ محدودیتی برای رابطه بین رکورد ها نداره. در پروژه ما چندین رابطه یک به چند میتونه وجود داشته باشه.

برای مثال در پروژه ما یک فروشگاه میتونه چندین محصول داشته باشه و این یک رابطه یک به چنده، پس بریم برای پیاده سازی گرفتن محصولات یک فروشگاه در مدل Shop :

Shop Model Class
Shop Model Class


همونطور که مشاهده میکنید ما از متود hasMany لاراول میتونیم رابطه یک به چند رو داشته باشیم و همه محصولات متعلق به یک فروشگاه توسط این متود داده خواهد شد.

$shop = Shop::find(1); $shop->products;

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

متود hasMany کوئری زیر رو در دیتابیس اجرا میکنه :

SELECT * FROM `products` WHERE `products`.`shop_id` = ? AND `products`.`shop_id` IS NOT NULL

اما کار با رابطه ها اینجا ختم نمیشه و میتونیم کارهای جذاب دیگه ای هم انجام بدیم.

مثلا اگر بخواهیم بعد از گرفتن محصولات یه فروشگاه یه سری فیلتر های دیگه مثل گرفتن قیمت های بیشتر ۱۰۰۰ رو بگیریم به این صورت عمل میکنیم :

$cart->products()->where('price','>',1000)->get();

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

با متود belongsTo خیلی راحت میشه این کار رو انجام داد، پس در مدل Prodcut داریم :

Product Model Class
Product Model Class

متود belongsTo فروشگاهی رو برمیکردنه که یک محصول به اون تعلق داره و این کوئری در دیتابیس اجرا میشه :

SELECT * FROM `shops` WHERE `shops`.`id` = ?

رابطه چند به چند

بسیار خوب حالا میرسیم به قسمت جذاب و البته پیچیده تر ماجرا، جایی که رکورد ها میتونن با چندین رکورد مختلف ارتباط داشته باشن. این در پروژه ما مربوط به سبد خرید کاربر و محصولات ما میشه.

محصولات میتونن به چند سبد خرید لینک بشن و همچنین سبد های خرید میتونن چندین محصول رو داشته باشن، اینجاست که به پیچیدگی ماجرا نزدیک میشیم اما اگر خوب درک کنیم خیلی ساده میشه.

زمانی که کاربر محصولی رو انتخاب میکنه، ما برای ذخیره سازی انتخاب کاربر در سبد خرید به دو تا نکته باید توجه کنیم :

  • چه کسی خرید رو انجام داده؟
  • چه محصولی انتخاب شده و به چه تعداد؟

حالا به این شکل دیاگرام جدول هامون طراحی میکنیم :

دیاگرام جدول سبد های خرید و محصولا
دیاگرام جدول سبد های خرید و محصولا

مهم ترین جدول ما، جدول cart_product هست که رابطه چند به چند رو در سبد خرید و محصولات ایجاد میکنه.

این جدول با استفاده از دو کلید خارجی cart_id و product_id این دو جدول رو به همدیگه وصل میکنه. quantity هم تعداد محصولاتیه که مشتری درخواست میکنه.

حالا ما مدل های Product و Cart رو داریم و میخوایم جدول رابط cart_product رو در دیتابیس وب اپلیکیشن لاراولیمون ایجاد کنیم. ابتدا فایل migration این جدول رو میسازیم :

php artisan make:migration create_cart_product_table

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

cart_product Table
cart_product Table


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

php artisan migrate

بریم سراغ مدل Cart و متد گرفتن محصولات لینک شده رو بسازیم :

Cart Model Class
Cart Model Class


ما برای گرفتن رکورد ها در رابطه چند به چند در لاراول از متود belongsToMany استفاده میکنیم.

در پارامتر اول آدرس کلاس مدل مورد نظر رو میدیم که قبلا درباره اش صحبت کردیم.

پارامتر دوم مربوط به اسم جدول رابطمون میشه که قبلا باهم ساختیم یعنی cart_product.

پارامتر سوم مربوط به نام کلید خارجی مدل اصلیمون یعنی Cart هستش.

پارامتر چهارم مربوط به نام کلید خارجی مدلی هستش که Cart باهاش رابطه داره یعنی Product.


متود belongsToMany این کوئری رو در مدل Cart ما برای پیدا کردن محصولات اجرا میکنه :

select * from `products` inner join `cart_product` on `products`.`id` = `cart_product`.`product_id` where `cart_product`.`cart_id` = ?

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

$cart = Cart::find(1); $cart->products;

اگر بخوایم رکورد های جدید بین دو جدول ایجاد و یا حذف کنیم خیلی راحت میتونیم از سه متود attach, detach, sync استفاده کنیم که هر کدومشون رو الان بررسی میکنیم.

  • متود پیوست (attach)

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

$cart->products()->attach([ 1 => ['quantity' => 1], 2 => ['quantity' => 2], ]);

همونطور که مشاهده میکنید ما به متود attach یک آرایه دو بعدی رو پاس دادیم.

در بعد اول آرایه ایندکس که داده شده یعنی 1=> مربوط به آیدی منحصر بفرد محصول میشه و آرایه ای که درونش داره اطلاعات اضافه ای هستش که در جدول رابطمون مشخص کردیم که ما یدونه بیشتر نداریم و اون تعداد محصولاتمون (quantity) هستش.

  • متود جدا کردن (detach)

نحوه کار با این متود هم دقیقا مثل متود قبلی یعنی attach هست اما فرقشون اینه که detach بجای اضافه کردن رکورد رو در صورت وجود حذف میکنه. مثل زمانی میمونه که کاربر از خرید یه محصول منصرف میشه و اون رو لغو میکنه.

$cart->products()->detach([ 1, 2, 3 ]);


  • متود همگام سازی (sync)

بعضی وقتا هست که ما میخوایم یک سری محصول رو وارد سبد خرید میکنیم و هر چیز دیگه ای که قبلا در سبد خرید بود حذف بشه، متود sync اینجا به کمکمون میاد :

$cart->products()->sync([ 1 => ['quantity' => 3], 2, 3 ]);

برای داشتن فرآیند مشابه در مدل Product هم چنین چیزی رو خواهیم داشت :

Product Model Class
Product Model Class


متود carts تمام سبد های خریدی که محصول مشخصی رو درونشون دارن برمیگردونه.

$product = Product::find(1); $product->carts; // تمام سبد های خرید که دارای این محصول هستن داده میشه.

اگر دقت کرده باشین، در مدل Product دیگه پارامتر های دوم تا چهارم رو پر نکردم و اجازه دادم لاراول بصورت اتوماتیک اینکار رو انجام بده؛ اما باید حواسمون باشه که طبق اصول و الگوریتم لاراول هم باید پیش بریم.

شرح نحوه کارکرد الگوریتم لاراول در تشخیص نام جدول ها

الگوریتم لاراول به این صورت کار میکنه که اسم جدول رابط رو طبق حروف الفبای انگلیسی ABCD... پیدا میکنه.

ما چون طبق اصول پیش رفتیم و جدول رابطمون رو به همین شکل ساختیم پس مشکلی نخواهیم داشت چون پکیج ORM میدونه که بین دو جدول carts و products با توجه به حروف الفبا carts چون با c شروع میشه پس نسبت به products که با p شروع میشه ارجحیت داره.

بعد از تشخیص ارجحیت ها، حالا نوبت برداشتن s و جمع نام جدول هاست، یعنی carts میشه cart و نهایتا ما اسم cart_product رو خواهیم داشت. (خیلی ساده و زیبا)

تا اینجا الگوریتم تشخیص نام جداول رو هم یاد گرفتیم؛ حالا چطوری میتونیم به جدول واسط در هنگام استفاده از رابطه چند به چند دسترسی داشته باشیم؟

برای دسترسی به جدول رابط، لاراول به طور پیش فرض اسم این جدول هارو pivot گذاشته و شما میتونین در آبجکت رابطتون بهش دسترسی داشته باشین. فرآیند زیر رو در نظر داشته باشید :

$product = Product::find(1); foreach($product->carts as $cart) { $cart->pivot; /* [ cart_id => 1, product_id => 1, ] */ }

همونطور که میبینید pivot بطور پیش فرض فقط دو تا ستون دو مدل رو برمیگردونه و ما به quantity یا تعداد محصولاتی که انتخاب شده دسترسی نداریم. برای اینکه به این هم دسترسی داشته باشیم در مدل Product از متود withPivot استفاده میکنیم :

return $this->belongsToMany(Cart::class)->withPivot('quantity');

حالا اگه اسم pivot خیلی برامون جالب نباشه میتونیم با استفاده از متود as براش اسم هم در نظر بگیریم :

return $this->belongsToMany(Cart::class)->as('cart_info');

فیلتر کردن بر اساس اطلاعات جدول رابط

حالا اگر بخوایم محصولات یک سبد خریدی رو بگیریم که بیشتر از 3 تعداد خواسته شده باید چیکار بکنیم؟

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

  • wherePivot('quantity',1)
  • wherePivotIn('quantity',[1,2])
  • wherePivotNotIn('quantity',[3,4])

حالا اگر بخواهیم محصولاتی که بیشتر از 3 تعداد در یک سبد خرید درخواست شدن رو بگیریم به این شکل عمل میکنیم :

return $this->belongsToMany(Product::class)->wherePivot('quantity','>',3);



با توجه به طولانی شدن مقاله، تصمیم گرفتم دو تا تاپیک مربوط به روابط دیتابیس بعدی و مهم دیگه در لاراول رو تو مقاله دوم بذارم که به زودی به اشتراک گذاشته میشه :

  • Has One Through
  • Polymorphic

در آخر...

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

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

اگر ایراداتی در این مقاله هست عذرخواهی میکنم و با کمال میل پذیرای انتقادات عزیزان هستم ;)

laraveldatabaserelationshipphpprogramming
Software Engineer | Blogger | Tech Enthusiast
شاید از این پست‌ها خوشتان بیاید