خب این پست رو مینویسم برای بیشتر یادگرفتن خودم و همینطور کمک به اونایی که برای درک روابط بین جدول های دیتابیس در لاراول یا هر فریم ورک دیگه ای مشکل دارن!
این پست مخصوصا برای لاراول کارا نوشته شده، اما میتونه برای هر کسی مفید باشه و حتی کسایی که با زبان های دیگه ای کار میکنن هم به دردشون میخوره.
اول از همه بریم سراغ شناخت انواع روابط یا Relationship در پایگاه داده.
ما میتونیم انواع رابطه ها رو داشته باشیم، بیاید از خودمون شروع کنیم : بین شما و پدر و مادرتون یک رابطه یک به یک وجود داره، بین شما و دوستانتون میتونه یک رابطه یک به چند وجود داشته باشه و بین اعضای دو گروه میشه یک رابطه چند به چند رو در نظر گرفت.
اینا همش یه مثاله و حتی بعضیاش میتونه دقیقا درست نباشه اما مارو به چیزی که میخوایم یاد بگیریم نزدیک میکنه. خب ما همین روابط رو برای مدیریت بهتر جدول هامون در دیتابیس داریم و خیلی عالین، مثلا همین مقاله ای که دارم مینویسم در ویرگول از دو تا جدول users و posts استفاده میکنه و بین این دو یک رابطه یک به چند وجود داره، منه نوعی که دارم این پست رو مینویسم امکان داره چندین پست دیگه هم بنویسم در آینده و البته قبلا هم نوشته باشم.
حالا که خوب روابط رو درک کردیم و من انتظار دارم دانش کافی برای کار با روابط در دیتابیس رو دارین، حالا بریم سراغ لاراول و اینکه چقدر کار با دیتابیس در لاراول راحت و جذاب انجام میشه.
اگر در درک روابط ساده دیتابیس مشکل دارین این منابع بهتون کمک میکنه یاد بگیرید :
انواع رابطه میان جدول ها در پایگاه داده رابطه ای
مفهوم پایگاه داده رابطه ای relational database
برای درک بهتر ما از یک پروژه فرضی استفاده و انواع روابط رو در این پروژه بررسی میکنیم.
برای مثال در نظر بگیرید، ما یک پلتفرمی ساده مارکت پلیس رو داریم که این امکان رو به فروشنده ها میده تا با افراد مختلف تعامل داشته باشن و محصولاتشون رو بصورت عمده یا تکی به فروش بذارن.
برای شروع کار من یک پروژه جدید لاراول با استفاده از پکیج منیجر کامپوزر با نام TopSeller ایجاد میکنم.
composer create-project --prefer-dist laravel/laravel TopSeller
حالا بریم سراغ تحلیل پروژه و قبل از هر چیزی دیاگرام جدول هامون رو رسم میکنیم تا یک دید کلی از نحوه کارکرد پروژه ساده مون داشته باشیم.
برای رسم دیاگرام ابزار های زیاد و متنوعی وجود داره مثل draw.io
من از پلاگین جدید draw io در vscode استفاده میکنم که فوق العاده عالیه و اگر vscode دارین حتما نصب کنید. این پلاگین با ارتباط با وبسایت Draw.io این امکان رو فراهم میکنه تا توو همون vscode دیاگرام هامون رو رسم کنیم.
در پروژه ما برای شروع MVP میتونیم 7 جدول داشته باشیم که روال کار رو بصورت ساده انجام بدیم.
اگر دقت کرده باشید، ما پلتفرمی رو داریم که هر کاربر میتونه یک یا چند فروشگاه با محصولات متنوع و مختلف داشته باشه. سبد های خرید رو داریم که یک رابطه چند به چند رو با محصولات برقرار میکنه، که در ادامه درباره همه اینها صحبت و تحلیل میکنیم.
رابطه یک به یک همونطور که از نامش پیداست، یعنی بین دو رکورد در دو جدول مختلف تنها یک رابطه میتونه وجود داشته باشه یا اصلا هیچ رابطه ای نباشه.
در پروژه ما هر کاربر فقط میتونه یک سبد خرید داشته باشه یا میتونه هیچ سبد خریدی نداشته باشه و صرفا یه فروشنده باشه.
پس ما برای دریافت سبد خرید یک کاربر در مدل یوزر متود cart رو خواهیم داشت :
در پکیج 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) :
اصلاح (آپدیت 98.6.10) :
در تصویر بالا بجای متود hasOne باید از متود belongsTo استفاده بشه.
دسترسی از طریق سبد خرید به کاربر در برنامه هم مثل همیشگیه، در واقع این مربوط به همه روابط میشه و به همین سادگی میتونیم اطلاعات رکورد رابطمون رو داشته باشیم :
$cart= Cart::find(1); $cart->user;
این رابطه یکی از پر استفاده ترین و پر کاربرد ترین روابط دیتابیسه، در رابطه یک به چند، یک رکورد میتونه با چندین رکورد در ارتباط باشه و هیچ محدودیتی برای رابطه بین رکورد ها نداره. در پروژه ما چندین رابطه یک به چند میتونه وجود داشته باشه.
برای مثال در پروژه ما یک فروشگاه میتونه چندین محصول داشته باشه و این یک رابطه یک به چنده، پس بریم برای پیاده سازی گرفتن محصولات یک فروشگاه در مدل Shop :
همونطور که مشاهده میکنید ما از متود 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 داریم :
متود 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
با این دستور فایل زیر ایجاد میشه و ما طبق دیاگراممون عمل میکنیم :
حالا که جدولمون را با استفاده از دستور زیر در دیتابیس ساختیم، وقتشه که اصل کار رو در Model هامون پیاده سازی کنیم.
php artisan migrate
بریم سراغ مدل Cart و متد گرفتن محصولات لینک شده رو بسازیم :
ما برای گرفتن رکورد ها در رابطه چند به چند در لاراول از متود 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 استفاده کنیم که هر کدومشون رو الان بررسی میکنیم.
در این متود ما میتونیم یک محصول جدید رو وارد سبد خرید کنیم، به این شکل عمل میکنیم :
$cart->products()->attach([ 1 => ['quantity' => 1], 2 => ['quantity' => 2], ]);
همونطور که مشاهده میکنید ما به متود attach یک آرایه دو بعدی رو پاس دادیم.
در بعد اول آرایه ایندکس که داده شده یعنی 1=> مربوط به آیدی منحصر بفرد محصول میشه و آرایه ای که درونش داره اطلاعات اضافه ای هستش که در جدول رابطمون مشخص کردیم که ما یدونه بیشتر نداریم و اون تعداد محصولاتمون (quantity) هستش.
نحوه کار با این متود هم دقیقا مثل متود قبلی یعنی attach هست اما فرقشون اینه که detach بجای اضافه کردن رکورد رو در صورت وجود حذف میکنه. مثل زمانی میمونه که کاربر از خرید یه محصول منصرف میشه و اون رو لغو میکنه.
$cart->products()->detach([ 1, 2, 3 ]);
بعضی وقتا هست که ما میخوایم یک سری محصول رو وارد سبد خرید میکنیم و هر چیز دیگه ای که قبلا در سبد خرید بود حذف بشه، متود sync اینجا به کمکمون میاد :
$cart->products()->sync([ 1 => ['quantity' => 3], 2, 3 ]);
برای داشتن فرآیند مشابه در مدل Product هم چنین چیزی رو خواهیم داشت :
متود 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 ما متود های دیگه ای هم داریم که یکیش رو بالاتر معرفی کردم و این ها مربوط به فیلتر اطلاعات در زمان اجرای کوئری دیتابیس هستش.
حالا اگر بخواهیم محصولاتی که بیشتر از 3 تعداد در یک سبد خرید درخواست شدن رو بگیریم به این شکل عمل میکنیم :
return $this->belongsToMany(Product::class)->wherePivot('quantity','>',3);
با توجه به طولانی شدن مقاله، تصمیم گرفتم دو تا تاپیک مربوط به روابط دیتابیس بعدی و مهم دیگه در لاراول رو تو مقاله دوم بذارم که به زودی به اشتراک گذاشته میشه :
قطعا اطلاعات این مقاله کامل نیست و ممکنه یک سری مشکلات هم داشته باشه، اما تا حد امکان سعی شده از اشتباهات جلوگیری بشه.
برای اطلاعات بیشتر خوندن داکیومنت های لاراول بسیار توصیه میشه تا با امکانات و متود های دیگه که استفاده کمتری دارن هم آشنا بشین.
اگر ایراداتی در این مقاله هست عذرخواهی میکنم و با کمال میل پذیرای انتقادات عزیزان هستم ;)