ترتيب العناصر OrderBy حسب العلاقات في الـ laravel

ترتيب العناصر OrderBy حسب العلاقات في الـ laravel

2024-05-15 وقت القراءه : 5 دقائق

تختلف طريقة ترتيب نتائج الإستعلام من قاعدة البيانات عند إستخدام العلاقات حسب العلاقة التي تربط الجدولين، وكذلك تختلف عند تنفيذ إستعلام لترتيب البيانات من عمود واحد.

على سبيل المثال ربما نرغب في جلب المستخدمين لكن الترتيب يكون حسب أخر سجل دخول، والموجود في جدول 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
  • SubQuery

 

إستخدام 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'));
}
إضافة تعليق
Loading...