تختلف طريقة ترتيب نتائج الإستعلام من قاعدة البيانات عند إستخدام العلاقات حسب العلاقة التي تربط الجدولين، وكذلك تختلف عند تنفيذ إستعلام لترتيب البيانات من عمود واحد.
على سبيل المثال ربما نرغب في جلب المستخدمين لكن الترتيب يكون حسب أخر سجل دخول، والموجود في جدول login المنفصل عن جدول users.
في هذا المقال سيتم تغطية ترتيب جلب البيانات حسب العلاقات التالية:
Has-one relationships
Belongs-to relationships
Has-many relationships
Belongs-to-many relationships
Hase-one relationships
تعتبر علاقة one-to-one من أبسط العلاقات في لارافيل، على سبيل المثال قد يكون المستخدم له رقم هاتف واحد بالتالي في User Model نضع به دالة phone ونربطها من خلال علاقة hasOne
in User Model
public function phone(){
return $this->hasOne('App\Models\phone');
}
لكن السؤال هنا، لفرض إني أريد جلب جميع المستخدمين لكن أن يكون الترتيب حسب رقم الهاتف (حسب العلاقة).
للقيام بذلك يوجد طريقتين
إستخدام Join
public function index() { $users = User::with('phone') ->select('users.*') ->join('phones', 'phones.user_id', '=', 'users.id') ->orderByDesc('phones.phone_no') ->paginate(); return view('books', compact('users')); }
نلاحظ بالفعل أنه تم ترتيب الأسماء حسب رقم الهاتف، لكن بالنظر إلى debugBar نرى إنه إستهلك وقت 8.1ms.
إستخدام SubQuery
public function index(){ $users=User::with('phone') ->orderBy(Phone::select('phone_no') ->whereColumn('user_id','users.id') ->orderBy('phone_no','ASC') ->take(1) ) ->get(); return view('books',compact('users')); }
بإستخدام SubQuery حصلنا على نفس النتيجة لكن بوقت زمني أعلى 18.8ms من إستخدام join.
من خلال إستخدام الطريقتين نستنتج أنه يجب إستخدام join في علاقة hasOne.
كذلك في علاقة belongsTo يجب إستخدام join.
ترتيب العلاقة حسب علاقة hasMany
بداية سأقوم بكتابة دالة scope بداخل userModel لجلب تاريخ أخر دخول من جدول login
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']); }
إستخدام join
public function index(){ $users=User::query() ->select('users.*') ->join('logins','logins.user_id','=','users.id') ->orderByDesc('logins.created_at') ->withLastLoginAt() ->paginate(); return view('books',compact('users')); }
فنحصل على النتائج التالية
لكن ما نلاحظة بالصوره أنه تم تكرار بعض الأسماء
لو قمنا بإضافة جملة
->groupBy('users.id')
فإننا سوف نحصل على الخطأ التالي
Illuminate\Database\QueryException SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #1 of ORDER BY clause is not in GROUP BY clause and contains nonaggregated column 'test.logins.created_at' which is not functionally dependent on columns in GROUP BY clause
وذلك لأنه يوجد سجلات عديدة لكل مستخدم فكيف ستعرف mySql بأي سجل (login) يجب أن يتم الترتيب، ولحل المشكله يجب إخبار mysql حسب أي سجل يجب أن يتم الترتيب من خلال إضافة orderByRaw
public function index(){ $users=User::query() ->select('users.*') ->join('logins','logins.user_id','=','users.id') ->groupBy('users.id') ->orderByRaw('max(logins.created_at) desc') ->withLastLoginAt() ->paginate(); return view('books',compact('users')); }
فنحصل على النتيجة التالية
لكن ما نلاحظة أنه إستغرق ما يقارب 459ms لجلب النتائح
إستخدام subquery
public function index(){ $users=User::query() ->orderByDesc(Login::select('created_at') ->whereColumn('user_id','users.id') ->latest() ->take(1) ) ->withLastLoginAt() ->paginate(); return view('books',compact('users')); }
يتضح من الصورة إننا حصلنا على نفس النتائج، لكن بمدة زمنية أقل 202ms.
اذا نستنتج أن إستخدام subquery أسرع من join في علاقة hasMany.
لجمالية الكود وحتى يكون clean وأكثر قراءه يفضل نقل subquery إلى userModel وإستخدام scope
In userModel public function logins(){ return $this->hasMany('App\Models\Login'); } 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']); } public function scopeOrderByLastLogin($query){ $query->orderByDesc(Login::select('created_at') ->whereColumn('user_id','users.id') ->latest() ->take(1) ); }
Eloquent public function index(){ $users=User::query() ->orderByLastLogin() ->withLastLoginAt() ->paginate(); return view('books',compact('users')); }