Eager loading و ضرورت استفاده از آن در لاراول
اخیرا متوجه شدم برنامه نویس ها کمتر از یکی از امکانات خیلی مهم لاراول استفاده میکنند ، موضوع رو بررسی کردم و فهمیدم حتی مستندات کافی نیز در این مورد در سایت اصلی لاراول موجود نیست و چون استفاده از اون خوب درک نشده ضرورت استفاده از آن هم احساس نشده ! . به همین دلیل قصد دارم امروز در مورد Eager Loading و ضرورت استفاده از آن و خطای معمول N + 1 کمی توضیح بدم .
Eloquent نام ORM قدرتمند لاراول است که علاوه بر امکاناتی خوبی که داره شما رو از نوشتن کوئری های SQL نیز بی نیاز میکنه . حال اگر ندانید پشت سر این غول ORM چی میگذرد ممکنه پروژه شما را تا پای نابودی جلو ببره ! البته این موضوع بیشتر برای وبسایت های با ترافیک بالا صدق میکنه .
خطای معمول N+1
در واقع یک مشکل یا خطای سهوی می باشد . برای مثال شما یک رکورد از روی دیتابیس واکشی میکنید و بدنبال آن فریمورک لاراول N کوئری برای دریافت رابطه های بین جداولی بر روی بانک اطلاعاتی شما به اجرا در می آورد . برای اینکه بیشتر متوجه شوید اجازه بدهید برای شما مثالی بزنم : فرض کنید ما دو جدول با نام students و books داریم و رابطه بین آنها One To Many (یک به چند ) می باشد یعنی هر دانش آموز تعداد زیادی کتاب متعلق به خود دارد .
برای این منظور یک مدل برای Student و یک مدل نیز برای Book می سازیم و یک رابطه hasMany در مدل Student ایجاد میکنیم .
1 2 3 4 |
// Model : Student public function books(){ $this->hasMany('Book'); } |
و طبق معمول برای دریافت اطلاعات از مدل ها به شیوه زیر عمل مینماییم :
1 2 3 4 5 6 |
$students = Student::all(); foreach( $students as $student ) { echo $student->name; $books = $student->books; // Do someting with the $books } |
در مثال بالا ابتدا اطلاعات همه دانش آموزان از بانک دریافت میگردد سپس N کوئری select دیگر برای دریافت لیست تک تک کتابهای یک دانش آموز بر روی دیتابیس اجرا خواهد شد .
فریمورک برای دریافت اطلاعات کلیه دانش آموزان ، کوئری های زیر را بر روی بانک اطلاعاتی اجرا میکند .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
-- Select the students i.e. Student::all(); SELECT * FROM students; -- Foreach of the students, another query to select the books -- i.e. $student->books part of the loop SELECT * FROM books WHERE student_id = 1 SELECT * FROM books WHERE student_id = 2 SELECT * FROM books WHERE student_id = 3 SELECT * FROM books WHERE student_id = 4 SELECT * FROM books WHERE student_id = 5 SELECT * FROM books WHERE student_id = 6 SELECT * FROM books WHERE student_id = 7 SELECT * FROM books WHERE student_id = 8 ... |
این همان خطای N+1 Query است ! که میتواند وبسایت های با ترافیک بالا را به مخاطره بیندازد و در صورتی که گلوگاه این سرباری ها پیدا نشود میتواند مشکلات سروری و در نتیجه ای بین رفتن رتبه سایت شود .
در بهترین حالت بهینه شده آن ابتدا یک کوئری ، اطلاعات کلیه دانش آموزان را دریافت میکند و سپس توسط کوئری دیگری لیست کتابهای مرتبط با کوئری اول از بانک واکشی می شود . در مجموع ۲ کوئری بر روی بانک اطلاعاتی اجرا خواهد شد :
1 2 |
SELECT * FROM students; SELECT * FROM books WHERE student_id IN (1,2,3,4,5,6,7,8..); |
Eager Loading
لاراول برای حل مشکل N+1 ، راه حل Eager loading را پیشنهاد می دهد. که استفاده از آن بسیار آسان است . بگذارید با همان مثال قبل شروع کنیم با این تفاوت که این بار معماری Eager را در نحوه واکشی و تعامل با بانک اطلاعاتی بکار می بریم .
به مثال زیر توجه کنید :
1 2 3 4 5 6 |
$students = Student::with('books')->get(); foreach( $students as $student ) { echo $student->name; $books = $student->books; // Do someting with the $books } |
لاراول با دستور with میفهمد باید همزمان اطلاعات کتاب های مرتبط با لیست دانش آموزان را از بانک اطلاعاتی در یافت کند و به دلیل معماری ORM لاراول و مدل شیء گرایی داخلی خود ، تنها کوئری های زیر را بر روی بانک اطلاعاتی اجرا خواهد شد .
1 2 |
SELECT * FROM students; SELECT * FROM books WHERE student_id IN (1,2,3,4,5,6,7,8..); |
میتوانید ببینید در مقابل مثال قبل که تعداد زیادی کوئری در بانک اطلاعاتی اجرا میشد ، این روش تنها با اجرای ۲ کوئری چقدر بهینه تر شده است .
در صورت لزوم میتوانید روابط بیشتری را Eager لود کنید . مثلا اگر میخواهید اطلاعات کلاس هایی که یک دانش آموز در آن شرکت داره را نیز دریافت کنید به شکل زیر عمل میکنیم :
1 2 3 4 5 6 7 |
$students = Student::with(['books', 'classes'])->get(); foreach( $students as $student ) { echo $student->name; $books = $student->books; $classes = $student->classes(); // Do someting with the $books/$classes } |
رابطه های تو در تو را نیز میتوانید به صورت زیر لود نمایید :
1 2 3 4 5 6 7 8 9 10 |
$students = Student::with('books.authors')->get(); foreach( $students as $student ) { echo $student->name; $books = $student->books; foreach ( $books as $book ) { $authors = $book->authors; } } |
و اگر میخواهید تنها رابطه هایی لود شوند که شرطی خاص را دارا باشند یا بهتر بگوییم (Eager Loading همراه با شرط )
1 2 3 4 5 6 7 8 9 |
$students = Student::with(array('books' => function( $query ){ $query->where('status', '=', 'returned'); }))->get(); foreach( $students as $student ) { echo $student->name; $books = $student->books; // Do someting with the $books } |
در تصویر بالا تنها لیست کتابهایی گرفته می شود که به کتابخانه بازگشته شده باشند و در کتابخانه موجود باشند .
سخن آخر
حال با درک اینکه استفاده دقیق از eager loading ها چه مزیتی دارند باید در موقعیت های مناسب از آنها استنفاده کنید و در واقع آنها ملزومات کار با مدل ها در لارول هستند و استفاده نادرست و نابجا نیز ممکن است نتیجه عکس داشته باشد !
سلام
وقت بخیر
این خیلی خوب هست که می توانیم این طوری از N+1 جلوگیری کنیم ولی چرا به صورت خودکار JOIN نمی کند بعضی وقت ها JOIN سریع تر کار می کند.
دلیلش اینه که لاراول تنها داده ها رو از دیتابیس فراخوانی میکنه و وظیفه مدلینگ و ارتباط مدلها (رکوردها) را به عهده eloquent میزاره. یعنی به عبارتی روابط بین جداول در orm ایجاد میشن.
با سلام و احترام خدمت شما آقای پاکدامن عزیز 🙂
مقاله ی مفیدی بود و استفاده کردم .
یک سوال برام بوجود اومده :
من درون یکی از پروژه هام از مثال مشابهی که بالا توضیح دادین ، استفاده کردم ولی یک مشکل خیلی بزرگ دارم
چطور میشه دیتای خروجی از Eager Loading رو صفحه بندی کرد ؟
خروجی من رکوردهای فوق العاده زیادی داره و از طرف دیگه دارم این این دیتا درون اپلیکیشن اندرویدی ( API ) استفاده میکنم و مشکلی که الان بهش برخورد کردم ، باعث شده سرعت اپلیکیشن تا حد خیلی خیلی زیادی کم بشه !
با سلام . صفحه بندی کردن در Eager Loading طبق روش معمول لاراول انجام میشه و در حقیقت سوال شما اینطور باید مطرح بشه که چطور میتونم دیتای خروجی برای API رو صفحه بندی کنم . خب تقریبا میتونم بگم این هم روش سختی نیست فقط کافیه پارامتر شماره صفحه با نام page به درخواستون اضافه کنید . مثلا :
site.com/api/posts?page=2
و برای نمایش های رکورد ها هم میتونید این کار رو کنید :
$products = Product::paginate(100);
$response = [
'products' => $products->getItems()->toArray(),
'pagination' => [
'total' => $products->getTotal(),
'per_page' => $products->getPerPage(),
'current_page' => $products->getCurrentPage(),
'last_page' => $products->getLastPage(),
'from' => $products->getFrom(),
'to' => $products->getTo()
]
];
return Response::json($response);
چرا برای این موارد از join استفاده نمیشه؟
ممنون بابت مقاله.
لطفا درمورد تکنیک های روتینگ و تکنیکهای کار با کنترلر ها هم بنویسید.
من همش بصورت استاتیک با کنترلر ها کار میکنم و مجبورم الکی صد خط کد بنویسم در صورتی که به نظرم اگه یکم بیشتر بلد بودم مجبور نبودم برای کار های ساده الکی کلی کد بنویسم.