لفرض أن لدينا جدول المستخدمين وفي جدول أخر logins يتم تسجيل الوقت والتاريخ لكل عملية دخول يقوم بها المستخدم وأريد عرض جميع المستخدمين مع أخر عملية دخول.
أبسط طريقة للقيام بذلك
In eloquent
public function index(){ $users=User::query() ->orderBy('name') ->paginate(); return view('books',compact('users')); }
In blade
@foreach($users as $user) <tr> <td>{{ $user->name }}</td> <td>{{ $user->email }}</td> <td>{{ $user->logins()->latest()->first()->created_at->diffForHumans() }}</td> </tr> @endforeach
لكن لو ألقينا نظره على debug bar
نجد أن هناك مشكلة، حيث تم تنفيذ ١٢ جملة إستعلام، ولو ألقينا نظرة على جمل الإستعلام نجد أن منها ١٠ جمل متشابهه، لأنه لكل مستخدم يتم عرضه يتم تنفيذ إستعلام أخر حتى يتم جلب أخر دخول، وهذه يطلق عليها n1 plus issue.
ما الذي يمكننا فعله لتحسين الأداء
الطريقة الأولى
إضافة حقل last_login_id في جدول users وفي كل عملية دخول يتم تحديث هذا الحقل، وتعتبر هذه الطريقة فعالة، لكن ليس هي الحل الأمثل، بالطبع نحن لسنا بحاجة للقيام بذلك.
الطريقة الثانية
إستخدام eager loading
public function index(){ $users=User::query() ->with('logins') ->orderBy('name') ->paginate(); return view('books',compact('users')); }
وفي ملف blade
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->logins()->sortBy('created_at')->first()->created_at->diffForHumans() }}</td>
</tr>
عند إلقاء نظره على debug bar نجد أن جمل الإستعلام أصبحت ٣ لكن تم تحميل الموديل ٨٦ مرة، ومعدل إستهلاك الذاكرة هو 20MB.
الطريقة الثالثة
وهو الحل الذي أفضلة هو إستخدام subquery
حيث تسمح لنا SubQuery بتنفيذ إستعلامات داخل قاعدة بيانات أخرى، وتعتبر هذه طريقة فاله دون أن يتم تنفيذ إستعلامات إضافية.
public function index(){ $users=User::query() ->addSelect(['last_login_at'=>Login::select('created_at') ->whereColumn('user_id','users.id') ->latest() ->take(1) ]) ->orderBy('name') ->paginate(); return view('books',compact('users')); }
In blade
@foreach($users as $user) <tr> <td>{{ $user->name }}</td> <td>{{ $user->email }}</td> <td>{{ $user->last_login_at }}</td> </tr> @endforeach
نلاحظ من شريط debug Bar أن عدد الموديل التي تم تحميلها فقط ٩ بدلا من ٨٦ ، وتم تقليل إستهلاك الذاكرة إلى 4.06MB، وتم تنفيذ إستعلامين فقط.
لو قمنا بأخذ جملة الإستعلام التي تننفيذها الى phpMyAdmin او tablePlus او أي برنامج أخر
نرى إنه تم إضافة جدول جديد باسم last_login_at مع العلم أنه غير موجود مسبقاً كحقل، بل تم إنشاءه مؤقتا أثناء تنفيذ الجمله in the fly.
ولو قمنا بإزالة LIMIT 1 فإننا سوف نحصل على الخطأ
Query 1 ERROR: Subquery returns more than 1 row
ماذا لو أردنا أن يتم عرض التاريخ كـ diffForHumans
<td>{{ $user->last_login_at->diffForHumans() }}</td>
لكن هذا سوف يعطي خطأ
ErrorException Trying to get property 'diffForHumans' of non-object (View: /Library/WebServer/Documents/test/resources/views/books.blade.php)
وذلك لأن last_login_at هي string وليس من تنسيقات Carbon ولروعة لارافيل أنها وفرت لنا عمل cast
حيث يمكن تحويل last_login_at إلى time instance
public function index(){ $users=User::query() ->addSelect(['last_login_at'=>Login::select('created_at') ->whereColumn('user_id','users.id') ->latest() ->take(1) ]) ->withCasts(['last_login_at' => 'datetime']) ->orderBy('name') ->paginate(); return view('books',compact('users')); }
المشكله الأن أن الكود في الكونترولر أصبح كبير، ويفضل أن يتم نقل subquery إلى user model
In users model
public function scopeWithLastLoginAt($query){ $query->addSelect(['last_login_at'=>Login::select('created_at') ->whereColumn('user_id','users.id') ->latest() ->take(1) ]) ->withCasts(['last_login_at' => 'datetime']); }
In eloquent(controller)
public function index(){ $users=User::query() ->withLastLoginAt() ->orderBy('name') ->paginate(); return view('books',compact('users')); }
زائر
يعطيك الف عافيه