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

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

در بخش اول این سری از آموزش درک پکیج ORM لاراول سه مفهوم رابطه ای رو بخوبی درک کردیم و پروژه محور پیش بردیم.

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

در بخش دوم و پایانی قصد دارم دو مفاهیم through و polymorphic رو شرح بدم و باهم کلی چیزا یاد بگیریم.

  • رابطه میانی (Through)
  • رابطه چند ریختی (PolyMorphic)

برای این قسمت هم، پروژه ابتدایی که بررسی کردیم رو جلو میبریم و روی همون کار میکنیم، اجازه بدید یه نگاه دیگه به دیاگرام هامون بندازیم :

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

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

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

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

رابطه یک به چند میانی (Has Many Through)

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

اینجا جدول اصلیمون users هست و جدول رابطمون shops هستش و ما قصد داریم از طریق جدول فروشگاه ها (shops) به جدول محصولات (products) به کاربر برسیم. در واقع میخوایم هر کاربری با هر تعداد فروشگاهی که داره همه محصولات فروشگاه هاش رو بگیریم.

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

مدل User و استفاده از متود hasManyThrough
مدل User و استفاده از متود hasManyThrough


در تصویر بالا اگه ببینید ما برای گرفتن محصولات یه کاربر از متود hasManyThrough که در کلاس Model اصلی هسته لاراول قرار داره استفاده میکنیم و میتونیم شش پارامتر به متود hasManyThrough پاس بدیم و به ترتیب به این شکل هست :

  1. نام کلاس مدل جدولی که میخوام در نهایت بهش برسیم products هست
  2. نام کلاس مدل جدولی که به عنوان رابط عمل میکنه و جدول users رو به products ارتباط میده
  3. اسم کلید خارجی که جدول اصلی ما در جدول رابط shops داره
  4. نام کلید خارجی که جدول فروشگاه ها (shops) ما در جدول نهاییمون یعنی products داره
  5. نام کلید اصلی که جدول اصلی ما (users) داره، همونطور که میدونیم همه جدول هامون کلید های اصلیشون رو id گذاشتیم
  6. نام کلید اصلی که جدول فروشگاه ها (shops) داره


حالا چه اتفاقی در کوئری sql میوفته و این دستور به چه شکلی اجرا میشه؟! به همین راحتی با استفاده از inner join، پکیج ORM لاراول این کوئری رو اجرا میکنه :

select * from `products` inner join `shops` on `shops`.`id` = `products`.`shop_id` where `shops`.`user_id` = ?


  • در صورتی که میخوایم از رابطه یک به یک میانی استفاده کنیم، متود hasOneThrough رو داریم و همون پارامتر های قبلی که به hasManyThrough پاس دادیم رو میتونیم استفاده کنیم.

رابطه چند ریختی (PolyMorphic)

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

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

فرض کنید ما قصد داریم برای کاربران و فروشگاه هاشون عکس در نظر بگیریم و بیان و عکس مورد نظر خودشون رو بذارن و استفاده کنن.

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

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

pictures table diagram
pictures table diagram

در دیاگرام بالا دو فیلد pictureable_id و pictureable_type مهم ترین بخشی هستن که باید بهش توجه کنید.

ما میخوایم این جدول به صورت متمرکز عمل کنه و هر جدولی که قراره محتوای تصویری هم داشته باشه با این جدول مدیریت بشه.

  • فیلد pictureable_id : آیدی جدولی هست که این عکس به اون تعلق داره، در پروژه ما میتونه یکی از آیدی ها usres یا shops باشه.
  • فیلد pictureable_type : فیلدی هست که بصورت اتوماتیک توسط هسته ORM لاراول شناسایی و ایجاد میشه و نام کلاس مدلی هست که این تصویر به جدول اون تعلق داره.


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

من میام، مدل و فایل migration های جدول pictures رو با دستور زیر ایجاد میکنم.

$ php artisan make:model Picture -m


با دستور بالا دو فایل "Picture.php" (مدل) و "create_pictures_table.php" (مایگریشن) ایجاد میشن؛ بعد میام و طبق دیاگرام بالا فیلد های pictures رو توسط Schema Blueprint لاراول ایجاد میکنم.

Pictures migration
Pictures migration


همونطور که بالا میبینید هیچ نامی از هیچ جدولی آورده نشده و فیلد pictureable_type قرار هست که این کار رو انجام بده و جدول ها متنوعی رو شناسائی کنه.

حالا میخوام از این جدول ابتدا برای جدول کاربران (users) استفاده کنم و به این شکل عمل میکنم :

User picture method using morphOne
User picture method using morphOne



در تصویر بالا از متود morphOne که یک رابطه یک به یک، چند ریختی هست استفاده کردم (یعنی هر کاربر میتونه یک تصویر پروفایل داشته باشه)، به این متود میتونیم 5 پارامتر پاس بدیم :

  • نام کلاس مدل اون جدول متمرکزمون هست که اینجا App\Picture هست.
  • نام پیشوند فیلد های id و type هست که در صورتی که پارامتر های سوم و چهارم رو بزنیم این تقریبا بلا استفاده میشه.
  • نام کلید type (اسم کلاس) هست که در جدول pictures ایجاد کردیم، در اینجا ما با استاندارد های ORM لاراول پیش رفتیم و pictureable_type رو گذاشتیم.
  • نام کلید id (نام آیدی ردیفی هستش که بهش تعلق داره) مثلا عکس ردیف ایدی 3 که در pictures قرار داره میتونه به آیدی 4 کاربر که در users قرار داره لینک بشه.
  • نام کلید محلی هستش که در جدول های چندریختیمون استفاده میشه، مثلا در جدول کاربران (users) کلید محلی ما id هست.


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

$user->picture

با توجه به اینکه متود picture رو در مدل User استفاده کردیم، میدونیم که برای گرفتن آبجکت مدل جدول مرتبط باید اسم متود رو به عنوان پراپرتی استفاده کنیم تا آبجکت مدل به ما داده بشه.


حالا چطور برعکس اینکار رو انجام بدیم؟! یعنی از طریق آبجکت picture به user دسترسی داشته باشیم؟

به همین راحتی، ابتدا در مدل Picture یه متود به اسم pictureable ایجاد میکنیم و با استفاده از متود morphTo اینکارو انجام میدیم :

Pictures Picturable method
Pictures Picturable method

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

به همین راحتی میتونیم کاربری که این عکس متعلق به اون هست رو بگیریم :

$picture->picturable

اگه بخوایم عکس جدید برای کاربر بذاریم؟!

$user->picture()->create([
'url' => 'user_profile_new.jpg'
])

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

$user->picture()->latest()->first()




حالا میرسیم به اصل ماجرا یعنی بیایم و از همین جدول pictures برای shops هم استفاده کنیم.

اینجا متوجه میشیم که این polymorphic چه موجود جذابیه که در ORM لاراول کارش رو انجام میده! ?

برای استفاده از pictures در shops ابتدا مدل Shop رو باز میکنم و یک متود با نام picture دقیقا مثل متودی که در User ایجاد کردیم درست میکنم. (میتونیم این متود رو به trait تبدیل کنیم تا از ریپیت کردن هم جلوگیری کنیم)

Shop model get picture using morphOne
Shop model get picture using morphOne

همونطور که میبینید دقیقا مثل متودی هست که من در مدل User استفاده کردم و هیچ فرقی نداره.

حالا به همین راحتی میتونم تصویر فروشگاه رو بگیرم که در صورت اینکه هیچ تصویری نباشه، null برگشت داده میشه.

$shop->picture


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


متود morphOne این کوئری رو اجرا میکنه :

select * from `pictures` where `pictures`.`pictureable_id` = ? and `pictures`.`pictureable_id` is not null and `pictures`.`pictureable_type` = ?

اما این بحث جذاب حالا حالا ها ادامه داره...

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

مثلا : رضا یه فروشگاه موبایل داره و میاد و تگ های #موبایل #لوازم_جانبی #فروش_موبایل میزاره و همچنین برای یه محصولی که مثلا گوشی اپل باشه تگ #apple یا #آیفون رو قرار میده.

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

ما برای پیاده سازی این رویکرد نیاز به دو جدول داریم :

tags

taggables

جدول tags هر تگی که ایجاد میشه بدون در نظر گرفتن اینکه چه کسی اون رو ساخته در خودش ذخیره میکنه و جدول taggables اصطلاحا جدول junction ما هست و میاد و تگ هارو با جداولی که میخوان با tags رابطه داشته باشن لینک میکنه و رابطه چند به چند رو اجرا میکنه.

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

Taggables and Tags table
Taggables and Tags table


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

$ php artisan make:model Tag -m

دستور بالا دو کلاس Tag.php (مدل) و create_tags_table.php (مایگریشن) ایجاد میشن.

بعد میرم سراغ ایجاد جدول taggables :

$ php artisan make:migration create_taggables_table

با دستور بالا یک کلاس مایگریشن برای جدول tags ایجاد میشه تا بتونیم اون رو پیکر بندی کنیم.

خب حالا بریم برای ایجاد جدول ها در فایل مایگریشن create_tags_table :

create_tags_table migration
create_tags_table migration

نکته مهم : حواستون باشه زمانی که از فیلد ها updated_at و created_at استفاده نمیکنیم باید پراپرتی $timestamps رو false قرار بدیم که به اررور پیدا نکردن این فیلد ها نخوریم.

Tag model disable timestamps
Tag model disable timestamps


و بعد پیکر بندی جدول taggables :

create_taggables_table migration
create_taggables_table migration


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

همونطور که ابتدا گفتم ما میخوایم فروشگاه ها و محصولات دارای تگ باشن تا بشه بهتر مدیریت و فیلترشون کرد، برای این منظور بجای ایجاد دو جدول shop_tags و product_tags ما میخوایم از یک جدول taggables برای هر جدولی که بخواد با tags ارتباط برقرار کنه خواهیم داشت که با اومدن فیلد های taggable_id و taggable_type این امکان پذیر میشه.

سوالی که ممکنه پیش بیاد primary کردن سه فیلد tag_id و taggable_id و taggable_type هست و این برای این هست که اجازه ندیم تا تگ های تکراری به یک ردیف در یک جدول لینک بشن و اومدیم از بوجود اومدن duplication entry جلوگیری کردیم. (در واقع اجازه نمیدیم که یک فروشگاه از دو تگ تکراری استفاده کنه)


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

Shop model using morphMany for tags
Shop model using morphMany for tags

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

پارامتر هاش ممکنه یخورده گنگ به نظر برسه و با توجه به اینکه توو داکیومنت های لاراول این رو کامل توضیح نداده، من در عمل تستش کردم و کامنت هایی که میبینید جلوی هر پارامتر توسط خودم با بررسی هسته ORM لاراول نوشته شده.

  • پارامتر اول نام کلاس مدلی هستش که قراره مدل ما باهاش ارتباط برقرار کنه و ازش استفاده کنه که اینجا Tag هستش
  • پارامتر دوم نام پیشوند فیلدهایی هستش که در junction table قرار داره؛ که اینجا ما جدول taggables رو ساختیم و باید طوری این رو بسازید که با نام جدول هم خوانی داشته باشه، مثلا اگه اسم جدول ما taggables هست برای اینکه لاراول بتونه اسم جدول رو بدرستی حدس بزنه ما باید پسوند فیلد هارو taggable قرار بدیم در غیر اینصورت واجبه که پارامتر های بعدی رو پاس بدیم...
  • پارامتر سوم نام جدول junction table هست که اختیاری هست به شرطی که پارامتر دوم درست داده بشه.
  • پارامتر چهارم نام کلید خارجی مربوط به جدولی هست که میخواد با tags ارتباط برقرار کنه (اختیاری)
  • پارامتر پنجم نام کلید خارجی هستش که به tags یعنی جدول نهایی که ما میخوایم بهش برسیم تعلق داره. (اختیاری)
  • پارامتر ششم و هفتم هم کلید های اصلی هستن که مربوط به دو جدولی هستن که با هم ارتباط برقرار میکنن مثلا shops میخواد از tags استفاده کنه بنابراین نام کلید اصلی اونها id هست. (اختیاری)

مهم : پارامتر هایی که اختیاری هستن به شرطی درست توسط پکیج ORM لاراول حدس زده میشه که شما طبق اصول این پکیج که در داکیومنتاش اومده جلو برید!


  • متود morphToMany این کوئری رو اجرا میکنه :
select * from `tags` inner join `taggables` on `tags`.`id` = `taggables`.`tag_id` where `taggables`.`taggable_id` = ? and `taggables`.`taggable_type` = ?


حالا همه چی آماده اس تا ما هر تگی که یک فروشگاه نیاز داره رو به اون بدیم.

$shop = Shop::first() ; $tag = new Tag; $tag->name = 'Programming'; $tag->save(); $shop->tags()->attach($tag); $shop->tags;


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

Product model using morphToMany
Product model using morphToMany

همونطور که میبینید یه متود tags در مدل محصولات (Product) ایجاد کردم و با استفاده از متود morphToMany و پاس دادن فقط دو پارامتر میتونم از جدول tags در محصولات هم استفاده کنم.

نحوه کارکردن با آبجکتی که morphToMany بر میگردونه دقیقا مثل رابطه چند به چند ساده اس که در بخش اول مقاله استفاده میکردیم و در داکیومنت های لاراول گفته شده و میتونیم از متود های sync, attach and detach استفاده کنیم :

$product = Product::first(); $tags = Tag::insert([ ['name' => 'programming'], ['name' => 'games'], ]); $product->tags()->sync($tags); $product->tags()->detach(Tag::first()); $product->tags;


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

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

Tag model using morphedByMany
Tag model using morphedByMany


متود morphedByMany این کوئری رو اجرا میکنه :

select * from `products` inner join `taggables` on `products`.`id` = `taggables`.`taggable_id` where `taggables`.`tag_id` = ? and `taggables`.`taggable_type` = ?&quot


حالا براحتی میتونیم از طریق آبجکت tag به محصولات یا فروشگاه هایی که از اون تگ استفاده کردن دسترسی داشته باشیم، برای مثال ما میخوایم بدونیم چه فروشگاه هایی در حوزه programming محصول دارن و اون هارو میفروشن :

$targetTag = Tag::whereName('programming')->first(); $targetTag->shops;

به همین راحتی میتونیم فروشگاه هایی که از این تگ استفاده کردن رو بگیریم.




جمع بندی...

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

در پایان امیدوارم این مقاله براتون مفید بوده باشه و به احتمال زیاد بحث دیگه مربوط به همین سری رو در مقاله های مجزا مثل custom polymorphic types بنویسم.


پیشنهاد من به شما اینه که برید و بیشتر هسته این پکیج جذاب رو نگاه کنید و البته میتونید با استفاده از متود toSql اتفاقاتی که در اجرای کوئری میوفته رو بررسی کنید.

مرسی از همراهیتون،

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

mdhesari.com

twitter.com/mdhesari

https://www.linkedin.com/in/mohamad-fazel-hesari


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